diff options
author | Kamil Trzcinski <ayufan@ayufan.eu> | 2017-01-24 17:52:50 +0100 |
---|---|---|
committer | Kamil Trzcinski <ayufan@ayufan.eu> | 2017-01-24 17:52:50 +0100 |
commit | 3cd17c9430c7575b0c1f1041947f3cd0d991f00c (patch) | |
tree | 12638383d600ae6d5c8cbf6c27da7bdb2199f5e7 /app | |
parent | 8faabdf7d33b575de11b043cfe6698021d33a973 (diff) | |
parent | 8c0e358a972ca9cb2176ff9289a5a89b0c909a93 (diff) | |
download | gitlab-ce-3cd17c9430c7575b0c1f1041947f3cd0d991f00c.tar.gz |
Merge remote-tracking branch 'origin/master' into 24147-delete-env-button
Diffstat (limited to 'app')
1263 files changed, 25758 insertions, 11512 deletions
diff --git a/app/assets/fonts/OFL.txt b/app/assets/fonts/OFL.txt deleted file mode 100644 index df187637e18..00000000000 --- a/app/assets/fonts/OFL.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. - -This Font Software is licensed under the SIL Open Font License, Version 1.1. - -This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/app/assets/fonts/SourceSansPro-Black.ttf.woff b/app/assets/fonts/SourceSansPro-Black.ttf.woff Binary files differdeleted file mode 100644 index b7e86200927..00000000000 --- a/app/assets/fonts/SourceSansPro-Black.ttf.woff +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-Black.ttf.woff2 b/app/assets/fonts/SourceSansPro-Black.ttf.woff2 Binary files differdeleted file mode 100644 index c90d078406c..00000000000 --- a/app/assets/fonts/SourceSansPro-Black.ttf.woff2 +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff Binary files differdeleted file mode 100644 index c3314b1ef06..00000000000 --- a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 Binary files differdeleted file mode 100644 index b87e22c41b5..00000000000 --- a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf.woff b/app/assets/fonts/SourceSansPro-Bold.ttf.woff Binary files differdeleted file mode 100644 index d1d40f840f8..00000000000 --- a/app/assets/fonts/SourceSansPro-Bold.ttf.woff +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf.woff2 b/app/assets/fonts/SourceSansPro-Bold.ttf.woff2 Binary files differdeleted file mode 100644 index 0f46f3e833a..00000000000 --- a/app/assets/fonts/SourceSansPro-Bold.ttf.woff2 +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff Binary files differdeleted file mode 100644 index ef6ff514d3a..00000000000 --- a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 Binary files differdeleted file mode 100644 index 8007df6df32..00000000000 --- a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff Binary files differdeleted file mode 100644 index 1e6c94d9eb3..00000000000 --- a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 Binary files differdeleted file mode 100644 index b715f274082..00000000000 --- a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff Binary files differdeleted file mode 100644 index 7a408b1ec73..00000000000 --- a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 Binary files differdeleted file mode 100644 index d8f9d29d4aa..00000000000 --- a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-It.ttf.woff b/app/assets/fonts/SourceSansPro-It.ttf.woff Binary files differdeleted file mode 100644 index 4d54bc95718..00000000000 --- a/app/assets/fonts/SourceSansPro-It.ttf.woff +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-It.ttf.woff2 b/app/assets/fonts/SourceSansPro-It.ttf.woff2 Binary files differdeleted file mode 100644 index a00852641f8..00000000000 --- a/app/assets/fonts/SourceSansPro-It.ttf.woff2 +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-Light.ttf.woff b/app/assets/fonts/SourceSansPro-Light.ttf.woff Binary files differdeleted file mode 100644 index 1706d57d3c5..00000000000 --- a/app/assets/fonts/SourceSansPro-Light.ttf.woff +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-Light.ttf.woff2 b/app/assets/fonts/SourceSansPro-Light.ttf.woff2 Binary files differdeleted file mode 100644 index d8b610ad76e..00000000000 --- a/app/assets/fonts/SourceSansPro-Light.ttf.woff2 +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff Binary files differdeleted file mode 100644 index 87378d6c609..00000000000 --- a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 Binary files differdeleted file mode 100644 index e0eebac8273..00000000000 --- a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf.woff b/app/assets/fonts/SourceSansPro-Regular.ttf.woff Binary files differdeleted file mode 100644 index 460ab12a638..00000000000 --- a/app/assets/fonts/SourceSansPro-Regular.ttf.woff +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf.woff2 b/app/assets/fonts/SourceSansPro-Regular.ttf.woff2 Binary files differdeleted file mode 100644 index 0dd3464c74b..00000000000 --- a/app/assets/fonts/SourceSansPro-Regular.ttf.woff2 +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff Binary files differdeleted file mode 100644 index 43379631b2d..00000000000 --- a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 Binary files differdeleted file mode 100644 index 2526d2e1b60..00000000000 --- a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff Binary files differdeleted file mode 100644 index 232c2048ae7..00000000000 --- a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff +++ /dev/null diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 Binary files differdeleted file mode 100644 index 606935af089..00000000000 --- a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 +++ /dev/null diff --git a/app/assets/images/auth_buttons/authentiq_64.png b/app/assets/images/auth_buttons/authentiq_64.png Binary files differnew file mode 100644 index 00000000000..81767bbcc54 --- /dev/null +++ b/app/assets/images/auth_buttons/authentiq_64.png diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6 index 82e526ae0ef..8a260aae1b1 100644 --- a/app/assets/javascripts/abuse_reports.js.es6 +++ b/app/assets/javascripts/abuse_reports.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-param-reassign */ + ((global) => { const MAX_MESSAGE_LENGTH = 500; const MESSAGE_CELL_SELECTOR = '.abuse-reports .message'; diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js deleted file mode 100644 index 919107b8cb9..00000000000 --- a/app/assets/javascripts/activities.js +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable */ -(function() { - this.Activities = (function() { - function Activities() { - Pager.init(20, true, false, this.updateTooltips); - $(".event-filter-link").on("click", (function(_this) { - return function(event) { - event.preventDefault(); - _this.toggleFilter($(event.currentTarget)); - return _this.reloadActivities(); - }; - })(this)); - } - - Activities.prototype.updateTooltips = function() { - gl.utils.localTimeAgo($('.js-timeago', '.content_list')); - }; - - Activities.prototype.reloadActivities = function() { - $(".content_list").html(''); - Pager.init(20, true, false, this.updateTooltips); - }; - - Activities.prototype.toggleFilter = function(sender) { - var filter = sender.attr("id").split("_")[0]; - - $('.event-filter .active').removeClass("active"); - Cookies.set("event_filter", filter); - - sender.closest('li').toggleClass("active"); - }; - - return Activities; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/activities.js.es6 b/app/assets/javascripts/activities.js.es6 new file mode 100644 index 00000000000..648cb4d5d85 --- /dev/null +++ b/app/assets/javascripts/activities.js.es6 @@ -0,0 +1,37 @@ +/* eslint-disable no-param-reassign, class-methods-use-this */ +/* global Pager */ +/* global Cookies */ + +((global) => { + class Activities { + constructor() { + Pager.init(20, true, false, this.updateTooltips); + $('.event-filter-link').on('click', (e) => { + e.preventDefault(); + this.toggleFilter(e.currentTarget); + this.reloadActivities(); + }); + } + + updateTooltips() { + gl.utils.localTimeAgo($('.js-timeago', '.content_list')); + } + + reloadActivities() { + $('.content_list').html(''); + Pager.init(20, true, false, this.updateTooltips); + } + + toggleFilter(sender) { + const $sender = $(sender); + const filter = $sender.attr('id').split('_')[0]; + + $('.event-filter .active').removeClass('active'); + Cookies.set('event_filter', filter); + + $sender.closest('li').toggleClass('active'); + } + } + + global.Activities = Activities; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index 1ef340e4ca1..993f427c9fb 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* 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() { this.Admin = (function() { function Admin() { @@ -59,7 +61,5 @@ } return Admin; - })(); - }).call(this); diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 1cab66e109e..b4a8c827d7f 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -1,6 +1,7 @@ -/* eslint-disable */ +/* 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() { - this.Api = { + var Api = { groupsPath: "/api/:version/groups.json", groupPath: "/api/:version/groups/:id.json", namespacesPath: "/api/:version/namespaces.json", @@ -10,6 +11,7 @@ licensePath: "/api/:version/templates/licenses/:key", gitignorePath: "/api/:version/templates/gitignores/:key", gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key", + dockerfilePath: "/api/:version/dockerfiles/:key", issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key", group: function(group_id, callback) { var url = Api.buildUrl(Api.groupPath) @@ -27,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); @@ -71,7 +73,7 @@ return $.ajax({ url: url, type: "POST", - data: {'label': data}, + data: { 'label': data }, dataType: "json" }).done(function(label) { return callback(label); @@ -119,6 +121,10 @@ return callback(file); }); }, + dockerfileYml: function(key, callback) { + var url = Api.buildUrl(Api.dockerfilePath).replace(':key', key); + $.get(url, callback); + }, issueTemplate: function(namespacePath, projectPath, key, type, callback) { var url = Api.buildUrl(Api.issuableTemplatePath) .replace(':key', key) @@ -140,4 +146,5 @@ } }; + window.Api = Api; }).call(this); diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 33c1708e1a9..f0615481ed2 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,4 +1,11 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len */ +/* global bp */ +/* global Cookies */ +/* global Flash */ +/* global ConfirmDangerModal */ +/* global AwardsHandler */ +/* global Aside */ + // This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js @@ -51,20 +58,24 @@ /*= require_directory ./extensions */ /*= require_directory ./lib/utils */ /*= require_directory ./u2f */ +/*= require_directory ./droplab */ /*= require_directory . */ /*= require fuzzaldrin-plus */ +/*= require es6-promise.auto */ (function () { - document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch); - window.addEventListener('hashchange', gl.utils.shiftWindow); + document.addEventListener('page:fetch', function () { + // Unbind scroll events + $(document).off('scroll'); + // Close any open tooltips + $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy'); + }); - window.onload = function () { - // Scroll the window to avoid the topnav bar - // https://github.com/twitter/bootstrap/issues/1768 - if (location.hash) { - return setTimeout(gl.utils.shiftWindow, 100); - } - }; + window.addEventListener('hashchange', gl.utils.handleLocationHash); + window.addEventListener('load', function onLoad() { + window.removeEventListener('load', onLoad, false); + gl.utils.handleLocationHash(); + }, false); $(function () { var $body = $('body'); @@ -79,7 +90,23 @@ // Set the default path for all cookies to GitLab's root directory Cookies.defaults.path = gon.relative_url_root || '/'; - gl.utils.preventDisabledButtons(); + // `hashchange` is not triggered when link target is already in window.location + $body.on('click', 'a[href^="#"]', function() { + var href = this.getAttribute('href'); + if (href.substr(1) === gl.utils.getLocationHash()) { + setTimeout(gl.utils.handleLocationHash, 1); + } + }); + + // prevent default action for disabled buttons + $('.btn').click(function(e) { + if ($(this).hasClass('disabled')) { + e.preventDefault(); + e.stopImmediatePropagation(); + return false; + } + }); + $('.nav-sidebar').niceScroll({ cursoropacitymax: '0.4', cursorcolor: '#FFF', diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js index c7eff27f971..8438de6cdf1 100644 --- a/app/assets/javascripts/aside.js +++ b/app/assets/javascripts/aside.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* 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 ab09e4475e6..b16a2c0f73a 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* 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 d7cda977845..629dc267337 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,7 +1,9 @@ -/* eslint-disable */ +/* 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) { @@ -132,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); @@ -209,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]; } }; @@ -337,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); } @@ -372,7 +374,5 @@ }; return AwardsHandler; - })(); - }).call(this); diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index 074378b9e52..a6bc262b657 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, max-len */ +/* global autosize */ /*= require jquery.ba-resize */ /*= require autosize */ @@ -25,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 a64cefb62bd..6af8f593872 100644 --- a/app/assets/javascripts/behaviors/details_behavior.js +++ b/app/assets/javascripts/behaviors/details_behavior.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* 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 7ff88ecdcaf..d4895011be7 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* 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 // // When a child field of a form with a `js-quick-submit` class receives a @@ -73,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 4ac343f876c..ccbd6b993cb 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* 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/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 05b213fe3fb..6a49715590c 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, vars-on-top, no-var, max-len */ (function(w) { $(function() { // Toggle button. Show/hide content inside parent container. diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 index 37531aaec9b..d3455fa3d8c 100644 --- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 +++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 @@ -1,7 +1,8 @@ -/* eslint-disable */ +/* 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)); @@ -37,5 +38,4 @@ } global.BlobCiYamlSelectors = BlobCiYamlSelectors; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 new file mode 100644 index 00000000000..bdf95017613 --- /dev/null +++ b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 @@ -0,0 +1,18 @@ +/* global Api */ +/*= require blob/template_selector */ + +(() => { + const global = window.gl || (window.gl = {}); + + class BlobDockerfileSelector extends gl.TemplateSelector { + requestFile(query) { + return Api.dockerfileYml(query.name, this.requestFileSuccess.bind(this)); + } + + requestFileSuccess(file) { + return super.requestFileSuccess(file); + } + } + + global.BlobDockerfileSelector = BlobDockerfileSelector; +})(); diff --git a/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6 new file mode 100644 index 00000000000..9cee79fa5d5 --- /dev/null +++ b/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6 @@ -0,0 +1,27 @@ +(() => { + const global = window.gl || (window.gl = {}); + + class BlobDockerfileSelectors { + constructor({ editor, $dropdowns } = {}) { + this.editor = editor; + this.$dropdowns = $dropdowns || $('.js-dockerfile-selector'); + this.initSelectors(); + } + + initSelectors() { + const editor = this.editor; + this.$dropdowns.each((i, dropdown) => { + const $dropdown = $(dropdown); + return new gl.BlobDockerfileSelector({ + editor, + pattern: /(Dockerfile)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'), + dropdown: $dropdown, + }); + }); + } + } + + global.BlobDockerfileSelectors = BlobDockerfileSelectors; +})(); diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index 33fb4f8185c..04bfe363929 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* 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() { this.BlobFileDropzone = (function() { function BlobFileDropzone(form, method) { @@ -60,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 344fe5dcd94..5fd0857db29 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* 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 */ @@ -18,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 9e992f7913c..8236457f0f1 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selectors.js +++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, comma-dangle, max-len */ +/* global BlobGitignoreSelector */ + (function() { this.BlobGitignoreSelectors = (function() { function BlobGitignoreSelectors(opts) { @@ -20,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 41a83a56146..7a14eb160d0 100644 --- a/app/assets/javascripts/blob/blob_license_selector.js +++ b/app/assets/javascripts/blob/blob_license_selector.js @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* 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 */ @@ -23,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 adeb8ba1318..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,6 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars, no-param-reassign */ +/* global BlobLicenseSelector */ + ((global) => { class BlobLicenseSelectors { constructor({ $dropdowns, editor }) { @@ -18,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 5434a19bcec..7e03ec3b391 100644 --- a/app/assets/javascripts/blob/template_selector.js.es6 +++ b/app/assets/javascripts/blob/template_selector.js.es6 @@ -1,98 +1,101 @@ -/* eslint-disable */ +/* 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 } = {}) { - const oldValue = this.editor.getValue(); - let newValue = file.content; + requestFile(item) { + // This `requestFile` method is an abstract method that should + // be added by all subclasses. + } - this.editor.setValue(newValue, 1); - if (!skipFocus) this.editor.focus(); + // To be implemented on the extending class + // e.g. + // Api.gitignoreText item.name, @requestFileSuccess.bind(@) + requestFileSuccess(file, { skipFocus } = {}) { + if (!file) return; - if (this.editor instanceof jQuery) { - this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); - } - } + const oldValue = this.editor.getValue(); + const newValue = file.content; - startLoadingSpinner() { - this.dropdownIcon - .addClass('fa-spinner fa-spin') - .removeClass('fa-chevron-down'); - } + this.editor.setValue(newValue, 1); + if (!skipFocus) this.editor.focus(); - stopLoadingSpinner() { - this.dropdownIcon - .addClass('fa-chevron-down') - .removeClass('fa-spinner fa-spin'); + if (this.editor instanceof jQuery) { + this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); } } - global.TemplateSelector = TemplateSelector; - })(window.gl || ( window.gl = {})); + startLoadingSpinner() { + this.dropdownIcon + .addClass('fa-spinner fa-spin') + .removeClass('fa-chevron-down'); + } + + stopLoadingSpinner() { + this.dropdownIcon + .addClass('fa-chevron-down') + .removeClass('fa-spinner fa-spin'); + } + } + + 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 b801c10f168..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,7 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */ +/* global EditBlob */ +/* global NewCommitForm */ + /*= require_tree . */ (function() { @@ -9,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 60840560dd3..079445e8278 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -1,6 +1,9 @@ -/* eslint-disable */ +/* 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) { @@ -33,6 +36,9 @@ new gl.BlobCiYamlSelectors({ editor: this.editor }); + new gl.BlobDockerfileSelectors({ + editor: this.editor + }); } EditBlob.prototype.initModePanesAndLinks = function() { @@ -57,7 +63,7 @@ content: this.editor.getValue() }, function(response) { currentPane.empty().append(response); - return currentPane.syntaxHighlight(); + return currentPane.renderGFM(); }); } else { this.$toggleButton.show(); @@ -78,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 efb22d38513..f9766471780 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ +/* global Vue */ +/* global BoardService */ + //= require vue //= require vue-resource //= require Sortable @@ -13,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 || {}; @@ -22,6 +25,8 @@ $(() => { gl.IssueBoardsApp.$destroy(true); } + Store.create(); + gl.IssueBoardsApp = new Vue({ el: $boardApp, components: { @@ -37,16 +42,15 @@ $(() => { issueLinkBase: $boardApp.dataset.issueLinkBase, detailIssue: Store.detail }, - init: Store.create.bind(Store), computed: { detailIssueVisible () { return Object.keys(this.detailIssue.issue).length; - } + }, }, created () { gl.boardService = new BoardService(this.endpoint, this.boardId); }, - ready () { + mounted () { Store.disabled = this.disabled; gl.boardService.all() .then((resp) => { @@ -60,6 +64,8 @@ $(() => { } }); + this.state.lists = _.sortBy(this.state.lists, 'position'); + Store.addBlankState(); this.loading = false; }); @@ -67,9 +73,12 @@ $(() => { }); gl.IssueBoardsSearch = new Vue({ - el: '#js-boards-seach', + el: '#js-boards-search', data: { filters: Store.state.filters + }, + mounted () { + gl.issueBoards.newListDropdownInit(); } }); }); diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 index 0e03d43872b..a32881116d5 100644 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, one-var */ +/* global Vue */ +/* global Sortable */ + //= require ./board_blank_state //= require ./board_delete //= require ./board_list @@ -10,6 +13,7 @@ window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.Board = Vue.extend({ + template: '#js-board-template', components: { 'board-list': gl.issueBoards.BoardList, 'board-delete': gl.issueBoards.BoardDelete, @@ -24,7 +28,6 @@ return { detailIssue: Store.detail, filters: Store.state.filters, - showIssueForm: false }; }, watch: { @@ -42,14 +45,28 @@ const issue = this.list.findIssue(this.detailIssue.issue.id); if (issue) { + const offsetLeft = this.$el.offsetLeft; const boardsList = document.querySelectorAll('.boards-list')[0]; - const right = (this.$el.offsetLeft + this.$el.offsetWidth) - boardsList.offsetWidth; - const left = boardsList.scrollLeft - this.$el.offsetLeft; + const left = boardsList.scrollLeft - offsetLeft; + let right = (offsetLeft + this.$el.offsetWidth); + + if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) { + // -290 here because width of boardsList is animating so therefore + // getting the width here is incorrect + // 290 is the width of the sidebar + right -= (boardsList.offsetWidth - 290); + } else { + right -= boardsList.offsetWidth; + } if (right - boardsList.scrollLeft > 0) { - boardsList.scrollLeft = right; + $(boardsList).animate({ + scrollLeft: right + }, this.sortableOptions.animation); } else if (left > 0) { - boardsList.scrollLeft = this.$el.offsetLeft; + $(boardsList).animate({ + scrollLeft: offsetLeft + }, this.sortableOptions.animation); } } }, @@ -58,11 +75,11 @@ }, methods: { showNewIssueForm() { - this.showIssueForm = !this.showIssueForm; + this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; } }, - ready () { - const options = gl.issueBoards.getBoardSortableDefaultOptions({ + mounted () { + this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ disabled: this.disabled, group: 'boards', draggable: '.is-draggable', @@ -71,24 +88,17 @@ gl.issueBoards.onEnd(); if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { - const order = this.sortable.toArray(), - $board = this.$parent.$refs.board[e.oldIndex + 1], - list = $board.list; - - $board.$destroy(true); + const order = this.sortable.toArray(); + const list = Store.findList('id', parseInt(e.item.dataset.id, 10)); this.$nextTick(() => { - Store.state.lists.splice(e.newIndex, 0, list); Store.moveList(list, order); }); } } }); - this.sortable = Sortable.create(this.$el.parentNode, options); + this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); }, - beforeDestroy () { - Store.state.lists.$remove(this.list); - } }); })(); 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 885553690d3..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,7 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, comma-dangle */ +/* global Vue */ +/* global ListLabel */ + (() => { const Store = gl.issueBoards.BoardsStore; @@ -12,7 +15,7 @@ new ListLabel({ title: 'To Do', color: '#F0AD4E' }), new ListLabel({ title: 'Doing', color: '#5CB85C' }) ] - } + }; }, methods: { addDefaultLists () { @@ -30,6 +33,8 @@ }); }); + Store.state.lists = _.sortBy(Store.state.lists, 'position'); + // Save the labels gl.boardService.generateDefaultLists() .then((resp) => { diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6 index 2f6c03e3538..5fc50280811 100644 --- a/app/assets/javascripts/boards/components/board_card.js.es6 +++ b/app/assets/javascripts/boards/components/board_card.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */ +/* global Vue */ + (() => { const Store = gl.issueBoards.BoardsStore; @@ -6,6 +8,7 @@ window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.BoardCard = Vue.extend({ + template: '#js-board-list-card', props: { list: Object, issue: Object, @@ -53,10 +56,8 @@ mouseDown () { this.showDetail = true; }, - mouseMove () { - if (this.showDetail) { - this.showDetail = false; - } + mouseMove() { + this.showDetail = false; }, showIssue (e) { const targetTagName = e.target.tagName.toLowerCase(); diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js.es6 index c45e1926c5c..861600424a5 100644 --- a/app/assets/javascripts/boards/components/board_delete.js.es6 +++ b/app/assets/javascripts/boards/components/board_delete.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, no-alert */ +/* global Vue */ + (() => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 34fc7694241..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,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, max-len */ +/* global Vue */ +/* global Sortable */ + //= require ./board_card //= require ./board_new_issue @@ -9,6 +12,7 @@ window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.BoardList = Vue.extend({ + template: '#js-board-list-template', components: { 'board-card': gl.issueBoards.BoardCard, 'board-new-issue': gl.issueBoards.BoardNewIssue @@ -19,27 +23,27 @@ issues: Array, loading: Boolean, issueLinkBase: String, - showIssueForm: Boolean }, data () { return { scrollOffset: 250, filters: Store.state.filters, - showCount: false + showCount: false, + showIssueForm: false }; }, watch: { filters: { handler () { this.list.loadingMore = false; - this.$els.list.scrollTop = 0; + this.$refs.list.scrollTop = 0; }, deep: true }, issues () { this.$nextTick(() => { if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) { - this.list.page++; + this.list.page += 1; this.list.getIssues(false); } @@ -51,15 +55,20 @@ }); } }, + computed: { + orderedIssues () { + return _.sortBy(this.issues, 'priority'); + }, + }, methods: { listHeight () { - return this.$els.list.getBoundingClientRect().height; + return this.$refs.list.getBoundingClientRect().height; }, scrollHeight () { - return this.$els.list.scrollHeight; + return this.$refs.list.scrollHeight; }, scrollTop () { - return this.$els.list.scrollTop + this.listHeight(); + return this.$refs.list.scrollTop + this.listHeight(); }, loadNextPage () { const getIssues = this.list.nextPage(); @@ -72,8 +81,9 @@ } }, }, - ready () { + mounted () { const options = gl.issueBoards.getBoardSortableDefaultOptions({ + scroll: document.querySelectorAll('.boards-list')[0], group: 'issues', sort: false, disabled: this.disabled, @@ -81,23 +91,25 @@ onStart: (e) => { const card = this.$refs.issue[e.oldIndex]; - Store.moving.issue = card.issue; + card.showDetail = false; Store.moving.list = card.list; + Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId); gl.issueBoards.onStart(); }, onAdd: (e) => { - gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue); + gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex); + + this.$nextTick(() => { + e.item.remove(); + }); }, - onRemove: (e) => { - this.$refs.issue[e.oldIndex].$destroy(true); - } }); - this.sortable = Sortable.create(this.$els.list, options); + this.sortable = Sortable.create(this.$refs.list, options); // Scroll event on list to load more - this.$els.list.onscroll = () => { + this.$refs.list.onscroll = () => { if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) { this.loadNextPage(); } diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6 index 7fc0bfd56f3..2386d3a613c 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js.es6 +++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, no-unused-vars */ +/* global Vue */ +/* global ListIssue */ + (() => { const Store = gl.issueBoards.BoardsStore; @@ -7,7 +10,6 @@ gl.issueBoards.BoardNewIssue = Vue.extend({ props: { list: Object, - showIssueForm: Boolean }, data() { return { @@ -15,11 +17,6 @@ error: false }; }, - watch: { - showIssueForm () { - this.$els.input.focus(); - } - }, methods: { submit(e) { e.preventDefault(); @@ -37,28 +34,30 @@ this.list.newIssue(issue) .then((data) => { // Need this because our jQuery very kindly disables buttons on ALL form submissions - $(this.$els.submitButton).enable(); + $(this.$refs.submitButton).enable(); Store.detail.issue = issue; }) .catch(() => { // Need this because our jQuery very kindly disables buttons on ALL form submissions - $(this.$els.submitButton).enable(); + $(this.$refs.submitButton).enable(); // Remove the issue this.list.removeIssue(issue); // Show error message this.error = true; - this.showIssueForm = true; }); this.cancel(); }, cancel() { - this.showIssueForm = false; this.title = ''; + this.$parent.showIssueForm = false; } - } + }, + mounted() { + this.$refs.input.focus(); + }, }); })(); diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6 index 4928320d015..02459722bbf 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js.es6 +++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6 @@ -1,4 +1,10 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, no-new */ +/* global Vue */ +/* global IssuableContext */ +/* global MilestoneSelect */ +/* global LabelsSelect */ +/* global Sidebar */ + (() => { const Store = gl.issueBoards.BoardsStore; @@ -41,13 +47,13 @@ this.detail.issue = {}; } }, - ready () { + mounted () { new IssuableContext(this.currentUser); new MilestoneSelect(); new gl.DueDateSelectors(); new LabelsSelect(); new Sidebar(); - new Subscription('.subscription'); + gl.Subscription.bindAll('.subscription'); } }); })(); 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 14f618fd5d5..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,5 +1,9 @@ -/* eslint-disable */ -$(() => { +/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var */ + +(() => { + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + const Store = gl.issueBoards.BoardsStore; $(document).off('created.label').on('created.label', (e, label) => { @@ -15,54 +19,58 @@ $(() => { }); }); - $('.js-new-board-list').each(function () { - const $this = $(this); - new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path')); + gl.issueBoards.newListDropdownInit = () => { + $('.js-new-board-list').each(function () { + const $this = $(this); + new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path')); - $this.glDropdown({ - data(term, callback) { - $.get($this.attr('data-labels')) - .then((resp) => { - callback(resp); + $this.glDropdown({ + data(term, callback) { + $.get($this.attr('data-labels')) + .then((resp) => { + callback(resp); + }); + }, + renderRow (label) { + 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}` }); - }, - 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}` - }); - return $li.append($a.prepend($labelColor)); - }, - search: { - fields: ['title'] - }, - filterable: true, - selectable: true, - multiSelect: true, - clicked (label, $el, e) { - e.preventDefault(); + return $li.append($a.prepend($labelColor)); + }, + search: { + fields: ['title'] + }, + filterable: true, + selectable: true, + multiSelect: true, + clicked (label, $el, e) { + e.preventDefault(); - if (!Store.findList('title', label.title)) { - Store.new({ - title: label.title, - position: Store.state.lists.length - 2, - list_type: 'label', - label: { - id: label.id, + if (!Store.findList('title', label.title)) { + Store.new({ title: label.title, - color: label.color - } - }); + position: Store.state.lists.length - 2, + list_type: 'label', + label: { + id: label.id, + title: label.title, + color: label.color + } + }); + + Store.state.lists = _.sortBy(Store.state.lists, 'position'); + } } - } + }); }); - }); -}); + }; +})(); diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6 index 9eceac4eddd..7e192e90fe6 100644 --- a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 +++ b/app/assets/javascripts/boards/filters/due_date_filters.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* global Vue */ + Vue.filter('due-date', (value) => { const date = new Date(value); return $.datepicker.formatDate('M d, yy', date); 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 db9a5a8e40a..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,6 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */ +/* global DocumentTouch */ + ((w) => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; @@ -17,18 +19,19 @@ gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; gl.issueBoards.getBoardSortableDefaultOptions = (obj) => { - let defaultSortOptions = { + const defaultSortOptions = { + animation: 200, forceFallback: true, fallbackClass: 'is-dragging', fallbackOnBody: true, ghostClass: 'is-ghost', filter: '.board-delete, .btn', - delay: gl.issueBoards.touchEnabled ? 100 : 50, + delay: gl.issueBoards.touchEnabled ? 100 : 0, scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, 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 21d735e8231..31531c3ee34 100644 --- a/app/assets/javascripts/boards/models/issue.js.es6 +++ b/app/assets/javascripts/boards/models/issue.js.es6 @@ -1,4 +1,9 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */ +/* global Vue */ +/* global ListLabel */ +/* global ListMilestone */ +/* global ListUser */ + class ListIssue { constructor (obj) { this.id = obj.iid; @@ -32,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); } } @@ -46,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) { @@ -55,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) } }; @@ -66,3 +71,5 @@ class ListIssue { return Vue.http.patch(url, data); } } + +window.ListIssue = ListIssue; diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6 index 0910fe9a854..9af88d167d6 100644 --- a/app/assets/javascripts/boards/models/label.js.es6 +++ b/app/assets/javascripts/boards/models/label.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars, space-before-function-paren */ + class ListLabel { constructor (obj) { this.id = obj.id; @@ -9,3 +10,5 @@ class ListLabel { this.priority = (obj.priority !== null) ? obj.priority : Infinity; } } + +window.ListLabel = ListLabel; diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index b331a26fed5..3dd5f273057 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* 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 */ + class List { constructor (obj) { this.id = obj.id; @@ -42,7 +45,8 @@ class List { } destroy () { - gl.issueBoards.BoardsStore.state.lists.$remove(this); + const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this); + gl.issueBoards.BoardsStore.state.lists.splice(index, 1); gl.issueBoards.BoardsStore.updateNewListDropdown(this.id); gl.boardService.destroyList(this.id); @@ -54,7 +58,7 @@ class List { nextPage () { if (this.issuesSize > this.issues.length) { - this.page++; + this.page += 1; return this.getIssues(false); } @@ -62,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) { @@ -90,7 +94,7 @@ class List { newIssue (issue) { this.addIssue(issue); - this.issuesSize++; + this.issuesSize += 1; return gl.boardService.newIssue(this.id, issue) .then((resp) => { @@ -105,16 +109,20 @@ class List { }); } - addIssue (issue, listFrom) { + addIssue (issue, listFrom, newIndex) { if (!this.findIssue(issue.id)) { - this.issues.push(issue); + if (newIndex !== undefined) { + this.issues.splice(newIndex, 0, issue); + } else { + this.issues.push(issue); + } if (this.label) { issue.addLabel(this.label); } if (listFrom) { - this.issuesSize++; + this.issuesSize += 1; gl.boardService.moveIssue(issue.id, listFrom.id, this.id) .then(() => { listFrom.getIssues(false); @@ -124,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) { @@ -132,7 +140,7 @@ class List { const matchesRemove = removeIssue.id === issue.id; if (matchesRemove) { - this.issuesSize--; + this.issuesSize -= 1; issue.removeLabel(this.label); } @@ -140,3 +148,5 @@ class List { }); } } + +window.List = List; diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js.es6 index a48969e19c9..c867b06d320 100644 --- a/app/assets/javascripts/boards/models/milestone.js.es6 +++ b/app/assets/javascripts/boards/models/milestone.js.es6 @@ -1,7 +1,10 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars */ + class ListMilestone { - constructor (obj) { + constructor(obj) { this.id = obj.id; this.title = obj.title; } } + +window.ListMilestone = ListMilestone; diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js.es6 index 583a973fc46..8e9de4d4cbb 100644 --- a/app/assets/javascripts/boards/models/user.js.es6 +++ b/app/assets/javascripts/boards/models/user.js.es6 @@ -1,9 +1,12 @@ -/* eslint-disable */ +/* eslint-disable no-unused-vars */ + class ListUser { - constructor (user) { + constructor(user) { this.id = user.id; this.name = user.name; this.username = user.username; this.avatar = user.avatar_url; } } + +window.ListUser = ListUser; diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index f59a2ed7937..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,6 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */ +/* global Vue */ + class BoardService { constructor (root, boardId) { this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { @@ -45,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); @@ -63,4 +65,6 @@ class BoardService { issue }); } -}; +} + +window.BoardService = BoardService; diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index 175e034afed..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,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */ +/* global Cookies */ +/* global List */ + (() => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; @@ -30,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() @@ -39,6 +42,8 @@ // Remove any new issues from the backlog // as they will be visible in the new list list.issues.forEach(backlogList.removeIssue.bind(backlogList)); + + this.state.lists = _.sortBy(this.state.lists, 'position'); }); this.removeBlankState(); }, @@ -47,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; @@ -58,6 +63,8 @@ title: 'Welcome to your Issue Board!', position: 0 }); + + this.state.lists = _.sortBy(this.state.lists, 'position'); }, removeBlankState () { this.removeList('blank'); @@ -75,30 +82,30 @@ 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) { - const issueTo = listTo.findIssue(issue.id), - issueLists = issue.getLists(), - listLabels = issueLists.map( listIssue => listIssue.label ); + moveIssueToList (listFrom, listTo, issue, newIndex) { + 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) { - listTo.addIssue(issue, listFrom); + listTo.addIssue(issue, listFrom, newIndex); } 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 039ca491cf5..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 */ +/* 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 80f137ca12e..54c2b4ad369 100644 --- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 +++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 @@ -1,8 +1,10 @@ -/* eslint-disable */ +/* 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 5d4d23e26c6..eae062a3aa3 100644 --- a/app/assets/javascripts/breakpoints.js +++ b/app/assets/javascripts/breakpoints.js @@ -1,6 +1,7 @@ -/* eslint-disable */ +/* 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() { - this.Breakpoints = (function() { + var Breakpoints = (function() { var BreakpointInstance, instance; function Breakpoints() {} @@ -51,7 +52,6 @@ }; return BreakpointInstance; - })(); Breakpoints.get = function() { @@ -59,7 +59,6 @@ }; return Breakpoints; - })(); $((function(_this) { @@ -68,4 +67,5 @@ }; })(this)); + window.Breakpoints = Breakpoints; }).call(this); diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js index 576f4c76c1e..dbdadc73c3f 100644 --- a/app/assets/javascripts/broadcast_message.js +++ b/app/assets/javascripts/broadcast_message.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* 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 5133e361001..0df84234520 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -1,6 +1,11 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top */ +/* global Breakpoints */ +/* global Turbolinks */ + (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; + var AUTO_SCROLL_OFFSET = 75; + var DOWN_BUILD_TRACE = '#down-build-trace'; this.Build = (function() { Build.interval = null; @@ -12,10 +17,21 @@ this.pageUrl = options.pageUrl; this.buildUrl = options.buildUrl; this.buildStatus = options.buildStatus; - this.state = options.state1; + this.state = options.logState; this.buildStage = options.buildStage; this.updateDropdown = bind(this.updateDropdown, this); this.$document = $(document); + this.$body = $('body'); + this.$buildTrace = $('#build-trace'); + this.$autoScrollContainer = $('.autoscroll-container'); + this.$autoScrollStatus = $('#autoscroll-status'); + this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text'); + this.$upBuildTrace = $('#up-build-trace'); + this.$downBuildTrace = $(DOWN_BUILD_TRACE); + this.$scrollTopBtn = $('#scroll-top'); + this.$scrollBottomBtn = $('#scroll-bottom'); + this.$buildRefreshAnimation = $('.js-build-refresh'); + clearInterval(Build.interval); // Init breakpoint checker this.bp = Breakpoints.get(); @@ -29,6 +45,7 @@ this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this)); this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown); + this.$document.on('scroll', this.initScrollMonitor.bind(this)); $(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this)); $('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace); this.updateArtifactRemoveDate(); @@ -37,18 +54,6 @@ this.initScrollButtonAffix(); } if (this.buildStatus === "running" || this.buildStatus === "pending") { - // Bind autoscroll button to follow build output - $('#autoscroll-button').on('click', function() { - var state; - state = $(this).data("state"); - if ("enabled" === state) { - $(this).data("state", "disabled"); - return $(this).text("Enable autoscroll"); - } else { - $(this).data("state", "enabled"); - return $(this).text("Disable autoscroll"); - } - }); Build.interval = setInterval((function(_this) { // Check for new build output if user still watching build page // Only valid for runnig build when output changes during time @@ -65,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 @@ -80,17 +85,21 @@ }; Build.prototype.getInitialBuildTrace = function() { - var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'] + var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']; return $.ajax({ url: this.buildUrl, dataType: 'json', success: function(buildData) { $('.js-build-output').html(buildData.trace_html); + if (window.location.hash === DOWN_BUILD_TRACE) { + $("html,body").scrollTop(this.$buildTrace.height()); + } if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { - return $('.js-build-refresh').remove(); + this.$buildRefreshAnimation.remove(); + return this.initScrollMonitor(); } - } + }.bind(this) }); }; @@ -100,6 +109,8 @@ dataType: "json", success: (function(_this) { return function(log) { + var pageUrl; + if (log.state) { _this.state = log.state; } @@ -111,7 +122,12 @@ } return _this.checkAutoscroll(); } else if (log.status !== _this.buildStatus) { - return Turbolinks.visit(_this.pageUrl); + pageUrl = _this.pageUrl; + if (_this.$autoScrollStatus.data('state') === 'enabled') { + pageUrl += DOWN_BUILD_TRACE; + } + + return Turbolinks.visit(pageUrl); } }; })(this) @@ -119,22 +135,95 @@ }; Build.prototype.checkAutoscroll = function() { - if ("enabled" === $("#autoscroll-button").data("state")) { - return $("html,body").scrollTop($("#build-trace").height()); + if (this.$autoScrollStatus.data("state") === "enabled") { + return $("html,body").scrollTop(this.$buildTrace.height()); + } + + // Handle a situation where user started new build + // but never scrolled a page + if (!this.$scrollTopBtn.is(':visible') && + !this.$scrollBottomBtn.is(':visible') && + !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { + this.$scrollBottomBtn.show(); } }; Build.prototype.initScrollButtonAffix = function() { - var $body, $buildTrace; - $body = $('body'); - $buildTrace = $('#build-trace'); - return this.$buildScroll.affix({ - offset: { - bottom: function() { - return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top); - } + // Hide everything initially + this.$scrollTopBtn.hide(); + this.$scrollBottomBtn.hide(); + this.$autoScrollContainer.hide(); + }; + + // Page scroll listener to detect if user has scrolling page + // and handle following cases + // 1) User is at Top of Build Log; + // - Hide Top Arrow button + // - Show Bottom Arrow button + // - Disable Autoscroll and hide indicator (when build is running) + // 2) User is at Bottom of Build Log; + // - Show Top Arrow button + // - Hide Bottom Arrow button + // - Enable Autoscroll and show indicator (when build is running) + // 3) User is somewhere in middle of Build Log; + // - Show Top Arrow button + // - Show Bottom Arrow button + // - Disable Autoscroll and hide indicator (when build is running) + Build.prototype.initScrollMonitor = function() { + if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { + // User is somewhere in middle of Build Log + + this.$scrollTopBtn.show(); + + if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed + this.$scrollBottomBtn.show(); + } else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) { + this.$scrollBottomBtn.show(); + } else { + this.$scrollBottomBtn.hide(); } - }); + + // Hide Autoscroll Status Indicator + if (this.$scrollBottomBtn.is(':visible')) { + this.$autoScrollContainer.hide(); + this.$autoScrollStatusText.removeClass('animate'); + } else { + this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show(); + this.$autoScrollStatusText.addClass('animate'); + } + } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { + // User is at Top of Build Log + + this.$scrollTopBtn.hide(); + this.$scrollBottomBtn.show(); + + this.$autoScrollContainer.hide(); + this.$autoScrollStatusText.removeClass('animate'); + } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) || + (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) { + // User is at Bottom of Build Log + + this.$scrollTopBtn.show(); + this.$scrollBottomBtn.hide(); + + // Show and Reposition Autoscroll Status Indicator + this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show(); + this.$autoScrollStatusText.addClass('animate'); + } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) { + // Build Log height is small + + this.$scrollTopBtn.hide(); + this.$scrollBottomBtn.hide(); + + // Hide Autoscroll Status Indicator + this.$autoScrollContainer.hide(); + this.$autoScrollStatusText.removeClass('animate'); + } + + if (this.buildStatus === "running" || this.buildStatus === "pending") { + // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise. + this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled'); + } }; Build.prototype.shouldHideSidebarForViewport = function() { @@ -172,7 +261,7 @@ $date = $('.js-artifacts-remove'); if ($date.length) { date = $date.text(); - return $date.text(gl.utils.timefor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' ')); + return $date.text(gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' ')); } }; @@ -193,6 +282,7 @@ }; Build.prototype.stepTrace = function(e) { + var $currentTarget; e.preventDefault(); $currentTarget = $(e.currentTarget); $.scrollTo($currentTarget.attr('href'), { @@ -201,7 +291,5 @@ }; return Build; - })(); - }).call(this); diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js index 49f84581650..083448552b6 100644 --- a/app/assets/javascripts/build_artifacts.js +++ b/app/assets/javascripts/build_artifacts.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* 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 0ecd20bc11e..99082b412e2 100644 --- a/app/assets/javascripts/build_variables.js.es6 +++ b/app/assets/javascripts/build_variables.js.es6 @@ -1,6 +1,7 @@ -/* eslint-disable */ -$(function(){ - $('.reveal-variables').off('click').on('click',function(){ +/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren */ + +$(function() { + $('.reveal-variables').off('click').on('click', function() { $('.js-build').toggle().niceScroll(); $(this).hide(); }); diff --git a/app/assets/javascripts/ci_lint_editor.js.es6 b/app/assets/javascripts/ci_lint_editor.js.es6 new file mode 100644 index 00000000000..56ffaa765a8 --- /dev/null +++ b/app/assets/javascripts/ci_lint_editor.js.es6 @@ -0,0 +1,18 @@ +(() => { + window.gl = window.gl || {}; + + class CILintEditor { + constructor() { + this.editor = window.ace.edit('ci-editor'); + this.textarea = document.querySelector('#content'); + + this.editor.getSession().setMode('ace/mode/yaml'); + this.editor.on('input', () => { + const content = this.editor.getSession().getValue(); + this.textarea.value = content; + }); + } + } + + gl.CILintEditor = CILintEditor; +})(); diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js index fac5b4f17da..c656ae4e241 100644 --- a/app/assets/javascripts/commit.js +++ b/app/assets/javascripts/commit.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife */ +/* global CommitFile */ + (function() { this.Commit = (function() { function Commit() { @@ -8,7 +10,5 @@ } return Commit; - })(); - }).call(this); diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js index 16d63729d31..184b4561d2e 100644 --- a/app/assets/javascripts/commit/file.js +++ b/app/assets/javascripts/commit/file.js @@ -1,14 +1,14 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new */ +/* global ImageFile */ + (function() { this.CommitFile = (function() { function CommitFile(file) { if ($('.image', file).length) { - new ImageFile(file); + new gl.ImageFile(file); } } return CommitFile; - })(); - }).call(this); diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index ffddce1297b..f09a6b1e676 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -1,6 +1,6 @@ -/* eslint-disable */ +/* 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() { - this.ImageFile = (function() { + gl.ImageFile = (function() { var prepareFrames; // Width where images must fits in, for 2-up this gets divided by 2 @@ -172,7 +172,5 @@ }; return ImageFile; - })(); - }).call(this); diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index c765d233831..cabeae74ae3 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, max-len, prefer-arrow-callback */ +/* global Pager */ + (function() { this.CommitsList = (function() { function CommitsList() {} @@ -6,14 +8,16 @@ CommitsList.timer = null; CommitsList.init = function(limit) { - $("body").on("click", ".day-commits-table li.commit", function(event) { - if (event.target.nodeName !== "A") { + $("body").on("click", ".day-commits-table li.commit", function(e) { + if (e.target.nodeName !== "A") { location.href = $(this).attr("url"); e.stopPropagation(); return false; } }); - Pager.init(limit, false); + Pager.init(limit, false, false, function() { + gl.utils.localTimeAgo($('.js-timeago')); + }); this.content = $("#commits-list"); this.searchField = $("#commits-search"); return this.initSearch(); @@ -54,7 +58,5 @@ }; return CommitsList; - })(); - }).call(this); diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js index 61cc91c524b..9591df70e9c 100644 --- a/app/assets/javascripts/compare.js +++ b/app/assets/javascripts/compare.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* 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 bd980f87e72..3587431ab69 100644 --- a/app/assets/javascripts/compare_autocomplete.js.es6 +++ b/app/assets/javascripts/compare_autocomplete.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* 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() { function CompareAutocomplete() { @@ -27,7 +28,6 @@ selectable: true, filterable: true, filterByText: true, - toggleLabel: true, fieldName: $dropdown.data('field-name'), filterInput: 'input[type="search"]', renderRow: function(ref) { @@ -54,11 +54,16 @@ $('.dropdown-toggle-text', $dropdown).text(text); $dropdownContainer.removeClass('open'); }); + + $dropdownContainer.on('click', '.dropdown-content a', (e) => { + $dropdown.prop('title', e.target.text.replace(/_+?/g, '-')); + if ($dropdown.hasClass('has-tooltip')) { + $dropdown.tooltip('fixTitle'); + } + }); }); }; return CompareAutocomplete; - })(); - }).call(this); diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js index 143d21adb37..35d98492012 100644 --- a/app/assets/javascripts/confirm_danger_modal.js +++ b/app/assets/javascripts/confirm_danger_modal.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* 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 7808d7fe313..3485f8f91ed 100644 --- a/app/assets/javascripts/copy_to_clipboard.js +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* 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 */ @@ -6,7 +7,7 @@ var genericError, genericSuccess, showTooltip; genericSuccess = function(e) { - showTooltip(e.trigger, 'Copied!'); + showTooltip(e.trigger, 'Copied'); // Clear the selection and blur the trigger so it loses its border e.clearSelection(); return $(e.trigger).blur(); @@ -31,7 +32,7 @@ var originalTitle = $target.data('original-title'); $target - .attr('title', 'Copied!') + .attr('title', 'Copied') .tooltip('fixTitle') .tooltip('show') .attr('title', originalTitle) @@ -45,5 +46,4 @@ clipboard.on('success', genericSuccess); return clipboard.on('error', genericError); }); - }).call(this); diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6 index 744aa0afa03..947c129d5b5 100644 --- a/app/assets/javascripts/create_label.js.es6 +++ b/app/assets/javascripts/create_label.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */ +/* global Api */ + (function (w) { class CreateLabelDropdown { constructor ($el, namespacePath, projectPath) { diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 new file mode 100644 index 00000000000..b83a4c63fad --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 @@ -0,0 +1,45 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageCodeComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + </div> + <ul class="stage-event-list"> + <li v-for="mergeRequest in items" class="stage-event-item"> + <div class="item-details"> + <img class="avatar" :src="mergeRequest.author.avatarUrl"> + <h5 class="item-title merge-merquest-title"> + <a :href="mergeRequest.url"> + {{ mergeRequest.title }} + </a> + </h5> + <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> + · + <span> + Opened + <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> + </span> + <span> + by + <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> + </span> + </div> + <div class="item-time"> + <total-time :time="mergeRequest.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 new file mode 100644 index 00000000000..cb1687dcc7a --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 @@ -0,0 +1,47 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageIssueComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + </div> + <ul class="stage-event-list"> + <li v-for="issue in items" class="stage-event-item"> + <div class="item-details"> + <img class="avatar" :src="issue.author.avatarUrl"> + <h5 class="item-title issue-title"> + <a class="issue-title" :href="issue.url"> + {{ issue.title }} + </a> + </h5> + <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> + · + <span> + Opened + <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> + </span> + <span> + by + <a :href="issue.author.webUrl" class="issue-author-link"> + {{ issue.author.name }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="issue.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 new file mode 100644 index 00000000000..513298ba4e7 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 @@ -0,0 +1,44 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StagePlanComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + </div> + <ul class="stage-event-list"> + <li v-for="commit in items" class="stage-event-item"> + <div class="item-details item-conmmit-component"> + <img class="avatar" :src="commit.author.avatarUrl"> + <h5 class="item-title commit-title"> + <a :href="commit.commitUrl"> + {{ commit.title }} + </a> + </h5> + <span> + First + <span class="commit-icon">${global.cycleAnalytics.svgs.iconCommit}</span> + <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a> + pushed by + <a :href="commit.author.webUrl" class="commit-author-link"> + {{ commit.author.name }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="commit.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 new file mode 100644 index 00000000000..73f4205b578 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 @@ -0,0 +1,47 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageProductionComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + </div> + <ul class="stage-event-list"> + <li v-for="issue in items" class="stage-event-item"> + <div class="item-details"> + <img class="avatar" :src="issue.author.avatarUrl"> + <h5 class="item-title issue-title"> + <a class="issue-title" :href="issue.url"> + {{ issue.title }} + </a> + </h5> + <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> + · + <span> + Opened + <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> + </span> + <span> + by + <a :href="issue.author.webUrl" class="issue-author-link"> + {{ issue.author.name }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="issue.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 new file mode 100644 index 00000000000..501ffb1fac9 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 @@ -0,0 +1,57 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageReviewComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + </div> + <ul class="stage-event-list"> + <li v-for="mergeRequest in items" class="stage-event-item"> + <div class="item-details"> + <img class="avatar" :src="mergeRequest.author.avatarUrl"> + <h5 class="item-title merge-merquest-title"> + <a :href="mergeRequest.url"> + {{ mergeRequest.title }} + </a> + </h5> + <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> + · + <span> + Opened + <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> + </span> + <span> + by + <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> + </span> + <template v-if="mergeRequest.state === 'closed'"> + <span class="merge-request-state"> + <i class="fa fa-ban"></i> + {{ mergeRequest.state.toUpperCase() }} + </span> + </template> + <template v-else> + <span class="merge-request-branch" v-if="mergeRequest.branch"> + <i class= "fa fa-code-fork"></i> + <a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a> + </span> + </template> + </div> + <div class="item-time"> + <total-time :time="mergeRequest.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 new file mode 100644 index 00000000000..82622232f64 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 @@ -0,0 +1,44 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageStagingComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + </div> + <ul class="stage-event-list"> + <li v-for="build in items" class="stage-event-item item-build-component"> + <div class="item-details"> + <img class="avatar" :src="build.author.avatarUrl"> + <h5 class="item-title"> + <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> + <i class="fa fa-code-fork"></i> + <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a> + <span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span> + <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a> + </h5> + <span> + <a :href="build.url" class="build-date">{{ build.date }}</a> + by + <a :href="build.author.webUrl" class="issue-author-link"> + {{ build.author.name }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="build.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 new file mode 100644 index 00000000000..4bfd363a1f1 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 @@ -0,0 +1,44 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageTestComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + </div> + <ul class="stage-event-list"> + <li v-for="build in items" class="stage-event-item item-build-component"> + <div class="item-details"> + <h5 class="item-title"> + <span class="icon-build-status">${global.cycleAnalytics.svgs.iconBuildStatus}</span> + <a :href="build.url" class="item-build-name">{{ build.name }}</a> + · + <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> + <i class="fa fa-code-fork"></i> + <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a> + <span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span> + <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a> + </h5> + <span> + <a :href="build.url" class="issue-date"> + {{ build.date }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="build.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 new file mode 100644 index 00000000000..0d85e1a4678 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 @@ -0,0 +1,25 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.TotalTimeComponent = Vue.extend({ + props: { + time: Object, + }, + template: ` + <span class="total-time"> + <template v-if="Object.keys(time).length"> + <template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template> + <template v-if="time.hours">{{ time.hours }} <span>hr</span></template> + <template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template> + <template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template> + </template> + <template v-else> + -- + </template> + </span> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 index 331f0209888..2f810a69758 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 @@ -1,98 +1,125 @@ -/* eslint-disable */ -//= require vue - -((global) => { - - const COOKIE_NAME = 'cycle_analytics_help_dismissed'; - const store = gl.cycleAnalyticsStore = { - isLoading: true, - hasError: false, - isHelpDismissed: Cookies.get(COOKIE_NAME), - analytics: {} - }; +/* global Vue */ +/* global Cookies */ +/* global Flash */ - gl.CycleAnalytics = class CycleAnalytics { - constructor() { - const that = this; - - this.vue = new Vue({ - el: '#cycle-analytics', - name: 'CycleAnalytics', - created: this.fetchData(), - data: store, - methods: { - dismissLanding() { - that.dismissLanding(); - } - } - }); - } - - fetchData(options) { - store.isLoading = true; - options = options || { startDate: 30 }; - - $.ajax({ - url: $('#cycle-analytics').data('request-path'), - method: 'GET', - dataType: 'json', - contentType: 'application/json', - data: { - cycle_analytics: { - start_date: options.startDate - } +//= require vue +//= require_tree ./svg +//= require_tree . + +$(() => { + const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; + const cycleAnalyticsEl = document.querySelector('#cycle-analytics'); + const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore; + const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({ + requestPath: cycleAnalyticsEl.dataset.requestPath, + }); + + gl.cycleAnalyticsApp = new Vue({ + el: '#cycle-analytics', + name: 'CycleAnalytics', + data: { + state: cycleAnalyticsStore.state, + isLoading: false, + isLoadingStage: false, + isEmptyStage: false, + hasError: false, + startDate: 30, + isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE), + }, + computed: { + currentStage() { + return cycleAnalyticsStore.currentActiveStage(); + }, + }, + components: { + 'stage-issue-component': gl.cycleAnalytics.StageIssueComponent, + 'stage-plan-component': gl.cycleAnalytics.StagePlanComponent, + 'stage-code-component': gl.cycleAnalytics.StageCodeComponent, + 'stage-test-component': gl.cycleAnalytics.StageTestComponent, + 'stage-review-component': gl.cycleAnalytics.StageReviewComponent, + 'stage-staging-component': gl.cycleAnalytics.StageStagingComponent, + 'stage-production-component': gl.cycleAnalytics.StageProductionComponent, + }, + created() { + this.fetchCycleAnalyticsData(); + }, + methods: { + handleError() { + cycleAnalyticsStore.setErrorState(true); + return new Flash('There was an error while fetching cycle analytics data.'); + }, + initDropdown() { + const $dropdown = $('.js-ca-dropdown'); + const $label = $dropdown.find('.dropdown-label'); + + $dropdown.find('li a').off('click').on('click', (e) => { + e.preventDefault(); + const $target = $(e.currentTarget); + this.startDate = $target.data('value'); + + $label.text($target.text().trim()); + this.fetchCycleAnalyticsData({ startDate: this.startDate }); + }); + }, + fetchCycleAnalyticsData(options) { + const fetchOptions = options || { startDate: this.startDate }; + + this.isLoading = true; + + cycleAnalyticsService + .fetchCycleAnalyticsData(fetchOptions) + .done((response) => { + cycleAnalyticsStore.setCycleAnalyticsData(response); + this.selectDefaultStage(); + this.initDropdown(); + }) + .error(() => { + this.handleError(); + }) + .always(() => { + this.isLoading = false; + }); + }, + selectDefaultStage() { + const stage = this.state.stages.first(); + this.selectStage(stage); + }, + selectStage(stage) { + if (this.isLoadingStage) return; + if (this.currentStage === stage) return; + + if (!stage.isUserAllowed) { + cycleAnalyticsStore.setActiveStage(stage); + return; } - }).done((data) => { - this.decorateData(data); - this.initDropdown(); - }) - .error((data) => { - this.handleError(data); - }) - .always(() => { - store.isLoading = false; - }) - } - - decorateData(data) { - data.summary = data.summary || []; - data.stats = data.stats || []; - - data.summary.forEach((item) => { - item.value = item.value || '-'; - }); - - data.stats.forEach((item) => { - item.value = item.value || '- - -'; - }); - - store.analytics = data; - } - - handleError(data) { - store.hasError = true; - new Flash('There was an error while fetching cycle analytics data.', 'alert'); - } - - dismissLanding() { - store.isHelpDismissed = true; - Cookies.set(COOKIE_NAME, true); - } - - initDropdown() { - const $dropdown = $('.js-ca-dropdown'); - const $label = $dropdown.find('.dropdown-label'); - - $dropdown.find('li a').off('click').on('click', (e) => { - e.preventDefault(); - const $target = $(e.currentTarget); - const value = $target.data('value'); - - $label.text($target.text().trim()); - this.fetchData({ startDate: value }); - }) - } - - } -})(window.gl || (window.gl = {})); + this.isLoadingStage = true; + cycleAnalyticsStore.setStageEvents([]); + cycleAnalyticsStore.setActiveStage(stage); + + cycleAnalyticsService + .fetchStageData({ + stage, + startDate: this.startDate, + }) + .done((response) => { + this.isEmptyStage = !response.events.length; + cycleAnalyticsStore.setStageEvents(response.events); + }) + .error(() => { + this.isEmptyStage = true; + }) + .always(() => { + this.isLoadingStage = false; + }); + }, + dismissOverviewDialog() { + this.isOverviewDialogDismissed = true; + Cookies.set(OVERVIEW_DIALOG_COOKIE, '1'); + }, + }, + }); + + // Register global components + Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent); +}); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 new file mode 100644 index 00000000000..9f74b14c4b9 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 @@ -0,0 +1,41 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + class CycleAnalyticsService { + constructor(options) { + this.requestPath = options.requestPath; + } + + fetchCycleAnalyticsData(options) { + options = options || { startDate: 30 }; + + return $.ajax({ + url: this.requestPath, + method: 'GET', + dataType: 'json', + contentType: 'application/json', + data: { + cycle_analytics: { + start_date: options.startDate, + }, + }, + }); + } + + fetchStageData(options) { + const { + stage, + startDate, + } = options; + + return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, { + cycle_analytics: { + start_date: startDate, + }, + }); + } + } + + global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 new file mode 100644 index 00000000000..be732971c7f --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 @@ -0,0 +1,94 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + const EMPTY_STAGE_TEXTS = { + issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.', + plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.', + code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.', + test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.', + review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.', + staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.', + production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.', + }; + + global.cycleAnalytics.CycleAnalyticsStore = { + state: { + summary: '', + stats: '', + analytics: '', + events: [], + stages: [], + }, + setCycleAnalyticsData(data) { + this.state = Object.assign(this.state, this.decorateData(data)); + }, + decorateData(data) { + const newData = {}; + + newData.stages = data.stats || []; + newData.summary = data.summary || []; + + newData.summary.forEach((item) => { + item.value = item.value || '-'; + }); + + newData.stages.forEach((item) => { + const stageName = item.title.toLowerCase(); + item.active = false; + item.isUserAllowed = data.permissions[stageName]; + item.emptyStageText = EMPTY_STAGE_TEXTS[stageName]; + item.component = `stage-${stageName}-component`; + }); + newData.analytics = data; + return newData; + }, + setLoadingState(state) { + this.state.isLoading = state; + }, + setErrorState(state) { + this.state.hasError = state; + }, + deactivateAllStages() { + this.state.stages.forEach((stage) => { + stage.active = false; + }); + }, + setActiveStage(stage) { + this.deactivateAllStages(); + stage.active = true; + }, + setStageEvents(events) { + this.state.events = this.decorateEvents(events); + }, + decorateEvents(events) { + const newEvents = []; + + events.forEach((item) => { + if (!item) return; + + item.totalTime = item.total_time; + item.author.webUrl = item.author.web_url; + item.author.avatarUrl = item.author.avatar_url; + + if (item.created_at) item.createdAt = item.created_at; + if (item.short_sha) item.shortSha = item.short_sha; + if (item.commit_url) item.commitUrl = item.commit_url; + + delete item.author.web_url; + delete item.author.avatar_url; + delete item.total_time; + delete item.created_at; + delete item.short_sha; + delete item.commit_url; + + newEvents.push(item); + }); + + return newEvents; + }, + currentActiveStage() { + return this.state.stages.find(stage => stage.active); + }, + }; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 new file mode 100644 index 00000000000..5d486bcaf66 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 @@ -0,0 +1,7 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; + + global.cycleAnalytics.svgs.iconBranch = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><path fill="#8C8C8C" fill-rule="evenodd" d="M9.678 6.722C9.353 5.167 8.053 4 6.5 4S3.647 5.167 3.322 6.722h-2.6c-.397 0-.722.35-.722.778 0 .428.325.778.722.778h2.6C3.647 9.833 4.947 11 6.5 11s2.853-1.167 3.178-2.722h2.6c.397 0 .722-.35.722-.778 0-.428-.325-.778-.722-.778h-2.6zM4.694 7.5c0-1.09.795-1.944 1.806-1.944 1.01 0 1.806.855 1.806 1.944 0 1.09-.795 1.944-1.806 1.944-1.01 0-1.806-.855-1.806-1.944z"/></svg>'; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 new file mode 100644 index 00000000000..661bf9e9f1c --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 @@ -0,0 +1,7 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; + + global.cycleAnalytics.svgs.iconBuildStatus = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><g fill="#31AF64" fill-rule="evenodd"><path d="M12.5 7c0-3.038-2.462-5.5-5.5-5.5S1.5 3.962 1.5 7s2.462 5.5 5.5 5.5 5.5-2.462 5.5-5.5zM0 7c0-3.866 3.134-7 7-7s7 3.134 7 7-3.134 7-7 7-7-3.134-7-7z"/><path d="M6.28 7.697L5.045 6.464c-.117-.117-.305-.117-.42-.002l-.614.614c-.11.113-.11.303.007.42l1.91 1.91c.19.19.51.197.703.004l.264-.265L9.997 6.04c.108-.107.107-.293-.01-.408l-.612-.614c-.114-.113-.298-.12-.41-.01L6.28 7.7z"/></g></svg>'; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 new file mode 100644 index 00000000000..2208c27a619 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 @@ -0,0 +1,7 @@ +/* eslint-disable no-param-reassign */ +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; + + global.cycleAnalytics.svgs.iconCommit = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path fill="#8F8F8F" fill-rule="evenodd" d="M28.777 18c-.91-4.008-4.494-7-8.777-7-4.283 0-7.868 2.992-8.777 7H4.01C2.9 18 2 18.895 2 20c0 1.112.9 2 2.01 2h7.213c.91 4.008 4.494 7 8.777 7 4.283 0 7.868-2.992 8.777-7h7.214C37.1 22 38 21.105 38 20c0-1.112-.9-2-2.01-2h-7.213zM20 25c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5z"/></svg>'; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js deleted file mode 100644 index 4ddafff428f..00000000000 --- a/app/assets/javascripts/diff.js +++ /dev/null @@ -1,77 +0,0 @@ -/* eslint-disable */ -(function() { - this.Diff = (function() { - var UNFOLD_COUNT; - - UNFOLD_COUNT = 20; - - function Diff() { - $('.files .diff-file').singleFileDiff(); - this.filesCommentButton = $('.files .diff-file').filesCommentButton(); - if (this.diffViewType() === 'parallel') { - $('.content-wrapper .container-fluid').removeClass('container-limited'); - } - $(document).off('click', '.js-unfold'); - $(document).on('click', '.js-unfold', (function(_this) { - return function(event) { - var line_number, link, file, offset, old_line, params, prev_new_line, prev_old_line, ref, ref1, since, target, to, unfold, unfoldBottom; - target = $(event.target); - unfoldBottom = target.hasClass('js-unfold-bottom'); - unfold = true; - ref = _this.lineNumbers(target.parent()), old_line = ref[0], line_number = ref[1]; - offset = line_number - old_line; - if (unfoldBottom) { - line_number += 1; - since = line_number; - to = line_number + UNFOLD_COUNT; - } else { - ref1 = _this.lineNumbers(target.parent().prev()), prev_old_line = ref1[0], prev_new_line = ref1[1]; - line_number -= 1; - to = line_number; - if (line_number - UNFOLD_COUNT > prev_new_line + 1) { - since = line_number - UNFOLD_COUNT; - } else { - since = prev_new_line + 1; - unfold = false; - } - } - file = target.parents('.diff-file'); - link = file.data('blob-diff-path'); - params = { - since: since, - to: to, - bottom: unfoldBottom, - offset: offset, - unfold: unfold, - // indent is used to compensate for single space indent to fit - // '+' and '-' prepended to diff lines, - // see https://gitlab.com/gitlab-org/gitlab-ce/issues/707 - indent: 1, - view: file.data('view') - }; - return $.get(link, params, function(response) { - return target.parent().replaceWith(response); - }); - }; - })(this)); - } - - Diff.prototype.diffViewType = function() { - return $('.inline-parallel-buttons a.active').data('view-type'); - } - - Diff.prototype.lineNumbers = function(line) { - if (!line.children().length) { - return [0, 0]; - } - - return line.find('.diff-line-num').map(function() { - return parseInt($(this).data('linenumber')); - }); - }; - - return Diff; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 new file mode 100644 index 00000000000..5e1a4c948aa --- /dev/null +++ b/app/assets/javascripts/diff.js.es6 @@ -0,0 +1,121 @@ +/* eslint-disable class-methods-use-this */ + +(() => { + const UNFOLD_COUNT = 20; + + class Diff { + constructor() { + const $diffFile = $('.files .diff-file'); + $diffFile.singleFileDiff(); + $diffFile.filesCommentButton(); + + $diffFile.each((index, file) => new gl.ImageFile(file)); + + if (this.diffViewType() === 'parallel') { + $('.content-wrapper .container-fluid').removeClass('container-limited'); + } + + $(document) + .off('click', '.js-unfold, .diff-line-num a') + .on('click', '.js-unfold', this.handleClickUnfold.bind(this)) + .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); + + this.openAnchoredDiff(); + } + + handleClickUnfold(e) { + const $target = $(e.target); + // current babel config relies on iterators implementation, so we cannot simply do: + // const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent()); + const ref = this.lineNumbers($target.parent()); + const oldLineNumber = ref[0]; + const newLineNumber = ref[1]; + const offset = newLineNumber - oldLineNumber; + const bottom = $target.hasClass('js-unfold-bottom'); + let since; + let to; + let unfold = true; + + if (bottom) { + const lineNumber = newLineNumber + 1; + since = lineNumber; + to = lineNumber + UNFOLD_COUNT; + } else { + const lineNumber = newLineNumber - 1; + since = lineNumber - UNFOLD_COUNT; + to = lineNumber; + + // make sure we aren't loading more than we need + const prevNewLine = this.lineNumbers($target.parent().prev())[1]; + if (since <= prevNewLine + 1) { + since = prevNewLine + 1; + unfold = false; + } + } + + const file = $target.parents('.diff-file'); + const link = file.data('blob-diff-path'); + const view = file.data('view'); + + const params = { since, to, bottom, offset, unfold, view }; + $.get(link, params, response => $target.parent().replaceWith(response)); + } + + openAnchoredDiff(cb) { + const locationHash = gl.utils.getLocationHash(); + const anchoredDiff = locationHash && locationHash.split('_')[0]; + + if (!anchoredDiff) return; + + const diffTitle = $(`#${anchoredDiff}`); + const diffFile = diffTitle.closest('.diff-file'); + const nothingHereBlock = $('.nothing-here-block:visible', diffFile); + if (nothingHereBlock.length) { + const clickTarget = $('.file-title, .click-to-expand', diffFile); + diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => { + this.highlighSelectedLine(); + if (cb) cb(); + }); + } else if (cb) { + cb(); + } + } + + handleClickLineNum(e) { + const hash = $(e.currentTarget).attr('href'); + e.preventDefault(); + if (window.history.pushState) { + window.history.pushState(null, null, hash); + } else { + window.location.hash = hash; + } + this.highlighSelectedLine(); + } + + diffViewType() { + return $('.inline-parallel-buttons a.active').data('view-type'); + } + + lineNumbers(line) { + if (!line.children().length) { + return [0, 0]; + } + return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10)); + } + + highlighSelectedLine() { + const $diffFiles = $('.diff-file'); + $diffFiles.find('.hll').removeClass('hll'); + + if (window.location.hash !== '') { + const hash = window.location.hash.replace('#', ''); + $diffFiles + .find(`tr#${hash}:not(.match) td, td#${hash}, td[data-line-code="${hash}"]`) + .addClass('hll'); + } + } + } + + window.gl = window.gl || {}; + window.gl.Diff = Diff; +})(); 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 29a12a2395b..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,9 +1,16 @@ -/* eslint-disable */ -((w) => { - w.CommentAndResolveBtn = Vue.extend({ +/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, max-len */ +/* global Vue */ +/* global CommentsStore */ + +(() => { + const CommentAndResolveBtn = Vue.extend({ props: { discussionId: String, - textareaIsEmpty: Boolean + }, + data() { + return { + textareaIsEmpty: true + }; }, computed: { discussion: function () { @@ -35,7 +42,7 @@ } } }, - ready: function () { + mounted: function () { const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`); this.textareaIsEmpty = $textarea.val() === ''; @@ -47,4 +54,6 @@ $(`#new-discussion-note-form-${this.discussionId} .note-textarea`).off('input.comment-and-resolve-btn'); } }); + + Vue.component('comment-and-resolve-btn', CommentAndResolveBtn); })(window); 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 983e554b9c1..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,6 +1,10 @@ -/* eslint-disable */ +/* 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 */ + (() => { - JumpToDiscussion = Vue.extend({ + const JumpToDiscussion = Vue.extend({ mixins: [DiscussionMixins], props: { discussionId: String @@ -42,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() { @@ -64,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; } } @@ -105,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]; @@ -152,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. @@ -166,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/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 index bcc052c7c8c..5852b8bbdb7 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 @@ -1,6 +1,11 @@ -/* eslint-disable */ -((w) => { - w.ResolveBtn = Vue.extend({ +/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */ +/* global Vue */ +/* global CommentsStore */ +/* global ResolveService */ +/* global Flash */ + +(() => { + const ResolveBtn = Vue.extend({ props: { noteId: Number, discussionId: String, @@ -54,9 +59,11 @@ }, methods: { updateTooltip: function () { - $(this.$els.button) - .tooltip('hide') - .tooltip('fixTitle'); + this.$nextTick(() => { + $(this.$refs.button) + .tooltip('hide') + .tooltip('fixTitle'); + }); }, resolve: function () { if (!this.canResolve) return; @@ -85,12 +92,12 @@ new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert'); } - this.$nextTick(this.updateTooltip); + this.updateTooltip(); }); } }, - compiled: function () { - $(this.$els.button).tooltip({ + mounted: function () { + $(this.$refs.button).tooltip({ container: 'body' }); }, @@ -101,4 +108,6 @@ CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy); } }); -})(window); + + Vue.component('resolve-btn', ResolveBtn); +})(); diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 index 24a99e23132..72cdae812bc 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 @@ -1,4 +1,8 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */ +/* global Vue */ +/* global DiscussionMixins */ +/* global CommentsStore */ + ((w) => { w.ResolveCount = Vue.extend({ mixins: [DiscussionMixins], @@ -13,6 +17,9 @@ computed: { allResolved: function () { return this.resolvedDiscussionCount === this.discussionCount; + }, + resolvedCountText() { + return this.discussionCount === 1 ? 'discussion' : 'discussions'; } } }); diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 index 060034f049b..ee5f62b2d9e 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 @@ -1,6 +1,10 @@ -/* eslint-disable */ -((w) => { - w.ResolveDiscussionBtn = Vue.extend({ +/* eslint-disable object-shorthand, func-names, space-before-function-paren, comma-dangle, no-else-return, quotes, max-len */ +/* global Vue */ +/* global CommentsStore */ +/* global ResolveService */ + +(() => { + const ResolveDiscussionBtn = Vue.extend({ props: { discussionId: String, mergeRequestId: Number, @@ -54,4 +58,6 @@ CommentsStore.createDiscussion(this.discussionId, this.canResolve); } }); -})(window); + + Vue.component('resolve-discussion-btn', ResolveDiscussionBtn); +})(); 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 6149bfd052a..1b3a57d0962 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 @@ -1,6 +1,7 @@ -/* eslint-disable */ -//= require vue -//= require vue-resource +/* eslint-disable func-names, comma-dangle, new-cap, no-new */ +/* global Vue */ +/* global ResolveCount */ + //= require_directory ./models //= require_directory ./stores //= require_directory ./services @@ -8,24 +9,35 @@ //= require_directory ./components $(() => { - window.DiffNotesApp = new Vue({ - el: '#diff-notes-app', - components: { - 'resolve-btn': ResolveBtn, - 'resolve-discussion-btn': ResolveDiscussionBtn, - 'comment-and-resolve-btn': CommentAndResolveBtn - }, - methods: { - compileComponents: function () { - const $components = $('resolve-btn, resolve-discussion-btn, jump-to-discussion'); - if ($components.length) { - $components.each(function () { - DiffNotesApp.$compile($(this).get(0)); - }); + const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn'; + + window.gl = window.gl || {}; + window.gl.diffNoteApps = {}; + + gl.diffNotesCompileComponents = () => { + const $components = $(COMPONENT_SELECTOR).filter(function () { + return $(this).closest('resolve-count').length !== 1; + }); + + if ($components) { + $components.each(function () { + const $this = $(this); + const noteId = $this.attr(':note-id'); + const tmp = Vue.extend({ + template: $this.get(0).outerHTML + }); + const tmpApp = new tmp().$mount(); + + if (noteId) { + gl.diffNoteApps[`note_${noteId}`] = tmpApp; } - } + + $this.replaceWith(tmpApp.$el); + }); } - }); + }; + + gl.diffNotesCompileComponents(); new Vue({ el: '#resolve-count-app', diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 index 7a929017f36..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,5 @@ -/* eslint-disable */ +/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-param-reassign, max-len */ + ((w) => { w.DiscussionMixins = { computed: { @@ -12,7 +13,7 @@ const discussion = this.discussions[discussionId]; if (discussion.isResolved()) { - resolvedCount++; + resolvedCount += 1; } } @@ -25,7 +26,7 @@ const discussion = this.discussions[discussionId]; if (!discussion.isResolved()) { - unresolvedCount++; + unresolvedCount += 1; } } diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6 index 439f55520ef..fa518ba4d33 100644 --- a/app/assets/javascripts/diff_notes/models/discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/models/discussion.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */ +/* global Vue */ +/* global NoteModel */ + class DiscussionModel { constructor (discussionId) { this.id = discussionId; @@ -57,16 +60,19 @@ class DiscussionModel { } updateHeadline (data) { - const $discussionHeadline = $(`.discussion[data-discussion-id="${this.id}"] .js-discussion-headline`); + const discussionSelector = `.discussion[data-discussion-id="${this.id}"]`; + const $discussionHeadline = $(`${discussionSelector} .js-discussion-headline`); if (data.discussion_headline_html) { if ($discussionHeadline.length) { $discussionHeadline.replaceWith(data.discussion_headline_html); } else { - $(`.discussion[data-discussion-id="${this.id}"] .discussion-header`).append(data.discussion_headline_html); + $(`${discussionSelector} .discussion-header`).append(data.discussion_headline_html); } + + gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`)); } else { - $discussionHeadline.remove(); + $discussionHeadline.remove(); } } @@ -74,7 +80,7 @@ class DiscussionModel { if (!this.canResolve) { return false; } - + for (const noteId in this.notes) { const note = this.notes[noteId]; @@ -86,3 +92,5 @@ class DiscussionModel { return false; } } + +window.DiscussionModel = DiscussionModel; diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6 index d0541b02632..f3a7cba5ef6 100644 --- a/app/assets/javascripts/diff_notes/models/note.js.es6 +++ b/app/assets/javascripts/diff_notes/models/note.js.es6 @@ -1,6 +1,7 @@ -/* eslint-disable */ +/* eslint-disable camelcase, no-unused-vars */ + class NoteModel { - constructor (discussionId, noteId, canResolve, resolved, resolved_by) { + constructor(discussionId, noteId, canResolve, resolved, resolved_by) { this.discussionId = discussionId; this.id = noteId; this.canResolve = canResolve; @@ -8,3 +9,5 @@ class NoteModel { this.resolved_by = resolved_by; } } + +window.NoteModel = NoteModel; diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6 index 86953ce7ffb..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,8 @@ -/* eslint-disable */ +/* eslint-disable class-methods-use-this, one-var, camelcase, no-new, comma-dangle, no-param-reassign, max-len */ +/* global Vue */ +/* global Flash */ +/* global CommentsStore */ + ((w) => { class ResolveServiceClass { constructor() { @@ -28,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) { @@ -55,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 f42ca406bb1..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,7 @@ -/* eslint-disable */ +/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */ +/* global Vue */ +/* global DiscussionModel */ + ((w) => { w.CommentsStore = { state: {}, @@ -38,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 756a24cc0fc..dcf67a8fd68 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -1,4 +1,42 @@ -/* eslint-disable */ +/* 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 */ +/* global Build */ +/* global Issuable */ +/* global Issue */ +/* global ShortcutsIssuable */ +/* global ZenMode */ +/* global Milestone */ +/* global GLForm */ +/* global IssuableForm */ +/* global LabelsSelect */ +/* global MilestoneSelect */ +/* global MergedButtons */ +/* global Commit */ +/* global NotificationsForm */ +/* global TreeView */ +/* global NotificationsDropdown */ +/* global UsersSelect */ +/* global GroupAvatar */ +/* global LineHighlighter */ +/* global ShortcutsBlob */ +/* global ProjectFork */ +/* global BuildArtifacts */ +/* global GroupsSelect */ +/* global Search */ +/* global Admin */ +/* global NamespaceSelects */ +/* global ShortcutsDashboardNavigation */ +/* global Project */ +/* global ProjectAvatar */ +/* global CompareAutocomplete */ +/* global ProjectNew */ +/* global Star */ +/* global ProjectShow */ +/* global Labels */ +/* global Shortcuts */ + (function() { var Dispatcher; @@ -24,6 +62,18 @@ switch (page) { case 'sessions:new': new UsernameValidator(); + new ActiveTabMemoizer(); + break; + case 'sessions:create': + if (!gon.u2f) break; + window.gl.u2fAuthenticate = new gl.U2FAuthenticate( + $("#js-authenticate-u2f"), + '#js-login-u2f-form', + gon.u2f, + document.querySelector('#js-login-2fa-device'), + document.querySelector('.js-2fa-form'), + ); + window.gl.u2fAuthenticate.start(); break; case 'projects:boards:show': case 'projects:boards:index': @@ -34,8 +84,13 @@ break; case 'projects:merge_requests:index': case 'projects:issues:index': + if (gl.FilteredSearchManager) { + new gl.FilteredSearchManager(); + } Issuable.init(); - new gl.IssuableBulkActions(); + new gl.IssuableBulkActions({ + prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_', + }); shortcut_handler = new ShortcutsNavigation(); break; case 'projects:issues:show': @@ -61,7 +116,7 @@ new ZenMode(); break; case 'projects:compare:show': - new Diff(); + new gl.Diff(); break; case 'projects:issues:new': case 'projects:issues:edit': @@ -74,7 +129,7 @@ break; case 'projects:merge_requests:new': case 'projects:merge_requests:edit': - new Diff(); + new gl.Diff(); shortcut_handler = new ShortcutsNavigation(); new GLForm($('.merge-request-form')); new IssuableForm($('.merge-request-form')); @@ -91,38 +146,35 @@ new GLForm($('.release-form')); break; case 'projects:merge_requests:show': - new Diff(); + new gl.Diff(); shortcut_handler = new ShortcutsIssuable(true); new ZenMode(); new MergedButtons(); break; case 'projects:merge_requests:commits': - case 'projects:merge_requests:builds': new MergedButtons(); break; case "projects:merge_requests:diffs": - new Diff(); + new gl.Diff(); new ZenMode(); new MergedButtons(); break; - case 'projects:merge_requests:index': - shortcut_handler = new ShortcutsNavigation(); - Issuable.init(); - break; case 'dashboard:activity': - new Activities(); + new gl.Activities(); break; case 'dashboard:projects:starred': - new Activities(); + new gl.Activities(); break; case 'projects:commit:show': new Commit(); - new Diff(); + new gl.Diff(); new ZenMode(); shortcut_handler = new ShortcutsNavigation(); break; - case 'projects:commit:builds': - new gl.Pipelines(); + case 'projects:commit:pipelines': + new gl.MiniPipelineGraph({ + container: '.js-pipeline-table', + }); break; case 'projects:commits:show': case 'projects:activity': @@ -135,11 +187,21 @@ new TreeView(); } break; + case 'projects:pipelines:builds': case 'projects:pipelines:show': - new gl.Pipelines(); + const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; + + new gl.Pipelines({ + initTabs: true, + tabsOptions: { + action: controllerAction, + defaultAction: 'pipelines', + parentEl: '.pipelines-tabs', + }, + }); break; case 'groups:activity': - new Activities(); + new gl.Activities(); break; case 'groups:show': shortcut_handler = new ShortcutsNavigation(); @@ -151,7 +213,9 @@ new gl.Members(); new UsersSelect(); break; - case 'projects:project_members:index': + case 'projects:members:show': + new gl.MemberExpirationDate('.js-access-expiration-date-groups'); + new GroupsSelect(); new gl.MemberExpirationDate(); new gl.Members(); new UsersSelect(); @@ -197,10 +261,6 @@ case 'projects:artifacts:browse': new BuildArtifacts(); break; - case 'projects:group_links:index': - new gl.MemberExpirationDate(); - new GroupsSelect(); - break; case 'search:show': new Search(); break; @@ -208,8 +268,12 @@ new gl.ProtectedBranchCreate(); new gl.ProtectedBranchEditList(); break; - case 'projects:cycle_analytics:show': - new gl.CycleAnalytics(); + case 'projects:variables:index': + new gl.ProjectVariables(); + break; + case 'ci:lints:create': + case 'ci:lints:show': + new gl.CILintEditor(); break; } switch (path.first()) { @@ -262,7 +326,7 @@ new NotificationsDropdown(); break; case 'wikis': - new Wikis(); + new gl.Wikis(); shortcut_handler = new ShortcutsNavigation(); new ZenMode(); new GLForm($('.wiki-form')); @@ -308,7 +372,5 @@ }; return Dispatcher; - })(); - }).call(this); diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js new file mode 100644 index 00000000000..c79f0230951 --- /dev/null +++ b/app/assets/javascripts/droplab/droplab.js @@ -0,0 +1,710 @@ +/* eslint-disable */ +// Determine where to place this +if (typeof Object.assign != 'function') { + Object.assign = function (target, varArgs) { // .length of function is 2 + 'use strict'; + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + var to = Object(target); + + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }; +} + +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.droplab = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +var DATA_TRIGGER = 'data-dropdown-trigger'; +var DATA_DROPDOWN = 'data-dropdown'; + +module.exports = { + DATA_TRIGGER: DATA_TRIGGER, + DATA_DROPDOWN: DATA_DROPDOWN, +} + +},{}],2:[function(require,module,exports){ +// Custom event support for IE +if ( typeof CustomEvent === "function" ) { + module.exports = CustomEvent; +} else { + require('./window')(function(w){ + var CustomEvent = function ( event, params ) { + params = params || { bubbles: false, cancelable: false, detail: undefined }; + var evt = document.createEvent( 'CustomEvent' ); + evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); + return evt; + } + CustomEvent.prototype = w.Event.prototype; + + w.CustomEvent = CustomEvent; + }); + module.exports = CustomEvent; +} + +},{"./window":11}],3:[function(require,module,exports){ +var CustomEvent = require('./custom_event_polyfill'); +var utils = require('./utils'); + +var DropDown = function(list) { + this.hidden = true; + this.list = list; + this.items = []; + this.getItems(); + this.initTemplateString(); + this.addEvents(); + this.initialState = list.innerHTML; +}; + +Object.assign(DropDown.prototype, { + getItems: function() { + this.items = [].slice.call(this.list.querySelectorAll('li')); + return this.items; + }, + + initTemplateString: function() { + var items = this.items || this.getItems(); + + var templateString = ''; + if(items.length > 0) { + templateString = items[items.length - 1].outerHTML; + } + this.templateString = templateString; + return this.templateString; + }, + + clickEvent: function(e) { + // climb up the tree to find the LI + var selected = utils.closest(e.target, 'LI'); + + if(selected) { + e.preventDefault(); + this.hide(); + var listEvent = new CustomEvent('click.dl', { + detail: { + list: this, + selected: selected, + data: e.target.dataset, + }, + }); + this.list.dispatchEvent(listEvent); + } + }, + + addEvents: function() { + this.clickWrapper = this.clickEvent.bind(this); + // event delegation. + this.list.addEventListener('click', this.clickWrapper); + }, + + toggle: function() { + if(this.hidden) { + this.show(); + } else { + this.hide(); + } + }, + + setData: function(data) { + this.data = data; + this.render(data); + }, + + addData: function(data) { + this.data = (this.data || []).concat(data); + this.render(this.data); + }, + + // call render manually on data; + render: function(data){ + // debugger + // empty the list first + var templateString = this.templateString; + var newChildren = []; + var toAppend; + + newChildren = (data ||[]).map(function(dat){ + var html = utils.t(templateString, dat); + var template = document.createElement('div'); + template.innerHTML = html; + + // Help set the image src template + var imageTags = template.querySelectorAll('img[data-src]'); + // debugger + for(var i = 0; i < imageTags.length; i++) { + var imageTag = imageTags[i]; + imageTag.src = imageTag.getAttribute('data-src'); + imageTag.removeAttribute('data-src'); + } + + if(dat.hasOwnProperty('droplab_hidden') && dat.droplab_hidden){ + template.firstChild.style.display = 'none' + }else{ + template.firstChild.style.display = 'block'; + } + return template.firstChild.outerHTML; + }); + toAppend = this.list.querySelector('ul[data-dynamic]'); + if(toAppend) { + toAppend.innerHTML = newChildren.join(''); + } else { + this.list.innerHTML = newChildren.join(''); + } + }, + + show: function() { + // debugger + this.list.style.display = 'block'; + this.hidden = false; + }, + + hide: function() { + // debugger + this.list.style.display = 'none'; + this.hidden = true; + }, + + destroy: function() { + this.hide(); + this.list.removeEventListener('click', this.clickWrapper); + } +}); + +module.exports = DropDown; + +},{"./custom_event_polyfill":2,"./utils":10}],4:[function(require,module,exports){ +require('./window')(function(w){ + module.exports = function(deps) { + deps = deps || {}; + var window = deps.window || w; + var document = deps.document || window.document; + var CustomEvent = deps.CustomEvent || require('./custom_event_polyfill'); + var HookButton = deps.HookButton || require('./hook_button'); + var HookInput = deps.HookInput || require('./hook_input'); + var utils = deps.utils || require('./utils'); + var DATA_TRIGGER = require('./constants').DATA_TRIGGER; + + var DropLab = function(hook){ + if (!(this instanceof DropLab)) return new DropLab(hook); + this.ready = false; + this.hooks = []; + this.queuedData = []; + this.config = {}; + this.loadWrapper; + if(typeof hook !== 'undefined'){ + this.addHook(hook); + } + }; + + + Object.assign(DropLab.prototype, { + load: function() { + this.loadWrapper(); + }, + + loadWrapper: function(){ + var dropdownTriggers = [].slice.apply(document.querySelectorAll('['+DATA_TRIGGER+']')); + this.addHooks(dropdownTriggers).init(); + }, + + addData: function () { + var args = [].slice.apply(arguments); + this.applyArgs(args, '_addData'); + }, + + setData: function() { + var args = [].slice.apply(arguments); + this.applyArgs(args, '_setData'); + }, + + destroy: function() { + for(var i = 0; i < this.hooks.length; i++) { + this.hooks[i].destroy(); + } + this.hooks = []; + this.removeEvents(); + }, + + applyArgs: function(args, methodName) { + if(this.ready) { + this[methodName].apply(this, args); + } else { + this.queuedData = this.queuedData || []; + this.queuedData.push(args); + } + }, + + _addData: function(trigger, data) { + this._processData(trigger, data, 'addData'); + }, + + _setData: function(trigger, data) { + this._processData(trigger, data, 'setData'); + }, + + _processData: function(trigger, data, methodName) { + for(var i = 0; i < this.hooks.length; i++) { + var hook = this.hooks[i]; + if(hook.trigger.dataset.hasOwnProperty('id')) { + if(hook.trigger.dataset.id === trigger) { + hook.list[methodName](data); + } + } + } + }, + + addEvents: function() { + var self = this; + this.windowClickedWrapper = function(e){ + var thisTag = e.target; + if(thisTag.tagName !== 'UL'){ + // climb up the tree to find the UL + thisTag = utils.closest(thisTag, 'UL'); + } + if(utils.isDropDownParts(thisTag)){ return } + if(utils.isDropDownParts(e.target)){ return } + for(var i = 0; i < self.hooks.length; i++) { + self.hooks[i].list.hide(); + } + }.bind(this); + document.addEventListener('click', this.windowClickedWrapper); + }, + + removeEvents: function(){ + w.removeEventListener('click', this.windowClickedWrapper); + w.removeEventListener('load', this.loadWrapper); + }, + + changeHookList: function(trigger, list, plugins, config) { + trigger = document.querySelector('[data-id="'+trigger+'"]'); + // list = document.querySelector(list); + this.hooks.every(function(hook, i) { + if(hook.trigger === trigger) { + hook.destroy(); + this.hooks.splice(i, 1); + this.addHook(trigger, list, plugins, config); + return false; + } + return true + }.bind(this)); + }, + + addHook: function(hook, list, plugins, config) { + if(!(hook instanceof HTMLElement) && typeof hook === 'string'){ + hook = document.querySelector(hook); + } + if(!list){ + list = document.querySelector(hook.dataset[utils.toDataCamelCase(DATA_TRIGGER)]); + } + + if(hook) { + if(hook.tagName === 'A' || hook.tagName === 'BUTTON') { + this.hooks.push(new HookButton(hook, list, plugins, config)); + } else if(hook.tagName === 'INPUT') { + this.hooks.push(new HookInput(hook, list, plugins, config)); + } + } + return this; + }, + + addHooks: function(hooks, plugins, config) { + for(var i = 0; i < hooks.length; i++) { + var hook = hooks[i]; + this.addHook(hook, null, plugins, config); + } + return this; + }, + + setConfig: function(obj){ + this.config = obj; + }, + + init: function () { + this.addEvents(); + var readyEvent = new CustomEvent('ready.dl', { + detail: { + dropdown: this, + }, + }); + window.dispatchEvent(readyEvent); + this.ready = true; + for(var i = 0; i < this.queuedData.length; i++) { + this.addData.apply(this, this.queuedData[i]); + } + this.queuedData = []; + return this; + }, + }); + + return DropLab; + }; +}); + +},{"./constants":1,"./custom_event_polyfill":2,"./hook_button":6,"./hook_input":7,"./utils":10,"./window":11}],5:[function(require,module,exports){ +var DropDown = require('./dropdown'); + +var Hook = function(trigger, list, plugins, config){ + this.trigger = trigger; + this.list = new DropDown(list); + this.type = 'Hook'; + this.event = 'click'; + this.plugins = plugins || []; + this.config = config || {}; + this.id = trigger.dataset.id; +}; + +Object.assign(Hook.prototype, { + + addEvents: function(){}, + + constructor: Hook, +}); + +module.exports = Hook; + +},{"./dropdown":3}],6:[function(require,module,exports){ +var CustomEvent = require('./custom_event_polyfill'); +var Hook = require('./hook'); + +var HookButton = function(trigger, list, plugins, config) { + Hook.call(this, trigger, list, plugins, config); + this.type = 'button'; + this.event = 'click'; + this.addEvents(); + this.addPlugins(); +}; + +HookButton.prototype = Object.create(Hook.prototype); + +Object.assign(HookButton.prototype, { + addPlugins: function() { + for(var i = 0; i < this.plugins.length; i++) { + this.plugins[i].init(this); + } + }, + + clicked: function(e){ + var buttonEvent = new CustomEvent('click.dl', { + detail: { + hook: this, + }, + bubbles: true, + cancelable: true + }); + this.list.show(); + e.target.dispatchEvent(buttonEvent); + }, + + addEvents: function(){ + this.clickedWrapper = this.clicked.bind(this); + this.trigger.addEventListener('click', this.clickedWrapper); + }, + + removeEvents: function(){ + this.trigger.removeEventListener('click', this.clickedWrapper); + }, + + restoreInitialState: function() { + this.list.list.innerHTML = this.list.initialState; + }, + + removePlugins: function() { + for(var i = 0; i < this.plugins.length; i++) { + this.plugins[i].destroy(); + } + }, + + destroy: function() { + this.restoreInitialState(); + this.removeEvents(); + this.removePlugins(); + }, + + + constructor: HookButton, +}); + + +module.exports = HookButton; + +},{"./custom_event_polyfill":2,"./hook":5}],7:[function(require,module,exports){ +var CustomEvent = require('./custom_event_polyfill'); +var Hook = require('./hook'); + +var HookInput = function(trigger, list, plugins, config) { + Hook.call(this, trigger, list, plugins, config); + this.type = 'input'; + this.event = 'input'; + this.addPlugins(); + this.addEvents(); +}; + +Object.assign(HookInput.prototype, { + addPlugins: function() { + var self = this; + for(var i = 0; i < this.plugins.length; i++) { + this.plugins[i].init(self); + } + }, + + addEvents: function(){ + var self = this; + + this.mousedown = function mousedown(e) { + if(self.hasRemovedEvents) return; + + var mouseEvent = new CustomEvent('mousedown.dl', { + detail: { + hook: self, + text: e.target.value, + }, + bubbles: true, + cancelable: true + }); + e.target.dispatchEvent(mouseEvent); + } + + this.input = function input(e) { + if(self.hasRemovedEvents) return; + + var inputEvent = new CustomEvent('input.dl', { + detail: { + hook: self, + text: e.target.value, + }, + bubbles: true, + cancelable: true + }); + e.target.dispatchEvent(inputEvent); + self.list.show(); + } + + this.keyup = function keyup(e) { + if(self.hasRemovedEvents) return; + + keyEvent(e, 'keyup.dl'); + } + + this.keydown = function keydown(e) { + if(self.hasRemovedEvents) return; + + keyEvent(e, 'keydown.dl'); + } + + function keyEvent(e, keyEventName){ + var keyEvent = new CustomEvent(keyEventName, { + detail: { + hook: self, + text: e.target.value, + which: e.which, + key: e.key, + }, + bubbles: true, + cancelable: true + }); + e.target.dispatchEvent(keyEvent); + self.list.show(); + } + + this.events = this.events || {}; + this.events.mousedown = this.mousedown; + this.events.input = this.input; + this.events.keyup = this.keyup; + this.events.keydown = this.keydown; + this.trigger.addEventListener('mousedown', this.mousedown); + this.trigger.addEventListener('input', this.input); + this.trigger.addEventListener('keyup', this.keyup); + this.trigger.addEventListener('keydown', this.keydown); + }, + + removeEvents: function() { + this.hasRemovedEvents = true; + this.trigger.removeEventListener('mousedown', this.mousedown); + this.trigger.removeEventListener('input', this.input); + this.trigger.removeEventListener('keyup', this.keyup); + this.trigger.removeEventListener('keydown', this.keydown); + }, + + restoreInitialState: function() { + this.list.list.innerHTML = this.list.initialState; + }, + + removePlugins: function() { + for(var i = 0; i < this.plugins.length; i++) { + this.plugins[i].destroy(); + } + }, + + destroy: function() { + this.restoreInitialState(); + this.removeEvents(); + this.removePlugins(); + this.list.destroy(); + } +}); + +module.exports = HookInput; + +},{"./custom_event_polyfill":2,"./hook":5}],8:[function(require,module,exports){ +var DropLab = require('./droplab')(); +var DATA_TRIGGER = require('./constants').DATA_TRIGGER; +var keyboard = require('./keyboard')(); +var setup = function() { + window.DropLab = DropLab; +}; + + +module.exports = setup(); + +},{"./constants":1,"./droplab":4,"./keyboard":9}],9:[function(require,module,exports){ +require('./window')(function(w){ + module.exports = function(){ + var currentKey; + var currentFocus; + var currentIndex = 0; + var isUpArrow = false; + var isDownArrow = false; + var removeHighlight = function removeHighlight(list) { + var listItems = list.list.querySelectorAll('li'); + for(var i = 0; i < listItems.length; i++) { + listItems[i].classList.remove('dropdown-active'); + } + return listItems; + }; + + var setMenuForArrows = function setMenuForArrows(list) { + var listItems = removeHighlight(list); + if(currentIndex>0){ + if(!listItems[currentIndex-1]){ + currentIndex = currentIndex-1; + } + listItems[currentIndex-1].classList.add('dropdown-active'); + } + }; + + var mousedown = function mousedown(e) { + var list = e.detail.hook.list; + removeHighlight(list); + list.show(); + currentIndex = 0; + isUpArrow = false; + isDownArrow = false; + }; + var selectItem = function selectItem(list) { + var listItems = removeHighlight(list); + var currentItem = listItems[currentIndex-1]; + var listEvent = new CustomEvent('click.dl', { + detail: { + list: list, + selected: currentItem, + data: currentItem.dataset, + }, + }); + list.list.dispatchEvent(listEvent); + list.hide(); + } + + var keydown = function keydown(e){ + var typedOn = e.target; + isUpArrow = false; + isDownArrow = false; + + if(e.detail.which){ + currentKey = e.detail.which; + if(currentKey === 13){ + selectItem(e.detail.hook.list); + return; + } + if(currentKey === 38) { + isUpArrow = true; + } + if(currentKey === 40) { + isDownArrow = true; + } + } else if(e.detail.key) { + currentKey = e.detail.key; + if(currentKey === 'Enter'){ + selectItem(e.detail.hook.list); + return; + } + if(currentKey === 'ArrowUp') { + isUpArrow = true; + } + if(currentKey === 'ArrowDown') { + isDownArrow = true; + } + } + if(isUpArrow){ currentIndex--; } + if(isDownArrow){ currentIndex++; } + if(currentIndex < 0){ currentIndex = 0; } + setMenuForArrows(e.detail.hook.list); + }; + + w.addEventListener('mousedown.dl', mousedown); + w.addEventListener('keydown.dl', keydown); + }; +}); +},{"./window":11}],10:[function(require,module,exports){ +var DATA_TRIGGER = require('./constants').DATA_TRIGGER; +var DATA_DROPDOWN = require('./constants').DATA_DROPDOWN; + +var toDataCamelCase = function(attr){ + return this.camelize(attr.split('-').slice(1).join(' ')); +}; + +// the tiniest damn templating I can do +var t = function(s,d){ + for(var p in d) + s=s.replace(new RegExp('{{'+p+'}}','g'), d[p]); + return s; +}; + +var camelize = function(str) { + return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function(letter, index) { + return index == 0 ? letter.toLowerCase() : letter.toUpperCase(); + }).replace(/\s+/g, ''); +}; + +var closest = function(thisTag, stopTag) { + while(thisTag && thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){ + thisTag = thisTag.parentNode; + } + return thisTag; +}; + +var isDropDownParts = function(target) { + if(!target || target.tagName === 'HTML') { return false; } + return ( + target.hasAttribute(DATA_TRIGGER) || + target.hasAttribute(DATA_DROPDOWN) + ); +}; + +module.exports = { + toDataCamelCase: toDataCamelCase, + t: t, + camelize: camelize, + closest: closest, + isDropDownParts: isDropDownParts, +}; + +},{"./constants":1}],11:[function(require,module,exports){ +module.exports = function(callback) { + return (function() { + callback(this); + }).call(null); +}; + +},{}]},{},[8])(8) +}); diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js new file mode 100644 index 00000000000..f20610b3811 --- /dev/null +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -0,0 +1,79 @@ +/* eslint-disable */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +/* global droplab */ + +require('../window')(function(w){ + function droplabAjaxException(message) { + this.message = message; + } + + w.droplabAjax = { + _loadUrlData: function _loadUrlData(url) { + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest; + xhr.open('GET', url, true); + xhr.onreadystatechange = function () { + if(xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + var data = JSON.parse(xhr.responseText); + return resolve(data); + } else { + return reject([xhr.responseText, xhr.status]); + } + } + }; + xhr.send(); + }); + }, + + init: function init(hook) { + var self = this; + var config = hook.config.droplabAjax; + + if (!config || !config.endpoint || !config.method) { + return; + } + + if (config.method !== 'setData' && config.method !== 'addData') { + return; + } + + if (config.loadingTemplate) { + var dynamicList = hook.list.list.querySelector('[data-dynamic]'); + + var loadingTemplate = document.createElement('div'); + loadingTemplate.innerHTML = config.loadingTemplate; + loadingTemplate.setAttribute('data-loading-template', ''); + + this.listTemplate = dynamicList.outerHTML; + dynamicList.outerHTML = loadingTemplate.outerHTML; + } + + this._loadUrlData(config.endpoint) + .then(function(d) { + if (config.loadingTemplate) { + var dataLoadingTemplate = hook.list.list.querySelector('[data-loading-template]'); + + if (dataLoadingTemplate) { + dataLoadingTemplate.outerHTML = self.listTemplate; + } + } + hook.list[config.method].call(hook.list, d); + }).catch(function(e) { + throw new droplabAjaxException(e.message || e); + }); + }, + + destroy: function() { + } + }; +}); +},{"../window":2}],2:[function(require,module,exports){ +module.exports = function(callback) { + return (function() { + callback(this); + }).call(null); +}; + +},{}]},{},[1])(1) +});
\ No newline at end of file diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js new file mode 100644 index 00000000000..af163f76851 --- /dev/null +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -0,0 +1,145 @@ +/* eslint-disable */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +/* global droplab */ + +require('../window')(function(w){ + w.droplabAjaxFilter = { + init: function(hook) { + this.destroyed = false; + this.hook = hook; + this.notLoading(); + + this.debounceTriggerWrapper = this.debounceTrigger.bind(this); + this.hook.trigger.addEventListener('keydown.dl', this.debounceTriggerWrapper); + this.hook.trigger.addEventListener('focus', this.debounceTriggerWrapper); + this.trigger(true); + }, + + notLoading: function notLoading() { + this.loading = false; + }, + + debounceTrigger: function debounceTrigger(e) { + var NON_CHARACTER_KEYS = [16, 17, 18, 20, 37, 38, 39, 40, 91, 93]; + var invalidKeyPressed = NON_CHARACTER_KEYS.indexOf(e.detail.which || e.detail.keyCode) > -1; + var focusEvent = e.type === 'focus'; + + if (invalidKeyPressed || this.loading) { + return; + } + + if (this.timeout) { + clearTimeout(this.timeout); + } + + this.timeout = setTimeout(this.trigger.bind(this, focusEvent), 200); + }, + + trigger: function trigger(getEntireList) { + var config = this.hook.config.droplabAjaxFilter; + var searchValue = this.trigger.value; + + if (!config || !config.endpoint || !config.searchKey) { + return; + } + + if (config.searchValueFunction) { + searchValue = config.searchValueFunction(); + } + + if (config.loadingTemplate && this.hook.list.data === undefined || + this.hook.list.data.length === 0) { + var dynamicList = this.hook.list.list.querySelector('[data-dynamic]'); + + var loadingTemplate = document.createElement('div'); + loadingTemplate.innerHTML = config.loadingTemplate; + loadingTemplate.setAttribute('data-loading-template', true); + + this.listTemplate = dynamicList.outerHTML; + dynamicList.outerHTML = loadingTemplate.outerHTML; + } + + if (getEntireList) { + searchValue = ''; + } + + if (config.searchKey === searchValue) { + return this.list.show(); + } + + this.loading = true; + + var params = config.params || {}; + params[config.searchKey] = searchValue; + var self = this; + this._loadUrlData(config.endpoint + this.buildParams(params)).then(function(data) { + if (config.loadingTemplate && self.hook.list.data === undefined || + self.hook.list.data.length === 0) { + const dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]'); + + if (dataLoadingTemplate) { + dataLoadingTemplate.outerHTML = self.listTemplate; + } + } + + if (!self.destroyed) { + var hookListChildren = self.hook.list.list.children; + var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic'); + + if (onlyDynamicList && data.length === 0) { + self.hook.list.hide(); + } + + self.hook.list.setData.call(self.hook.list, data); + } + self.notLoading(); + }); + }, + + _loadUrlData: function _loadUrlData(url) { + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest; + xhr.open('GET', url, true); + xhr.onreadystatechange = function () { + if(xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + var data = JSON.parse(xhr.responseText); + return resolve(data); + } else { + return reject([xhr.responseText, xhr.status]); + } + } + }; + xhr.send(); + }); + }, + + buildParams: function(params) { + if (!params) return ''; + var paramsArray = Object.keys(params).map(function(param) { + return param + '=' + (params[param] || ''); + }); + return '?' + paramsArray.join('&'); + }, + + destroy: function destroy() { + if (this.timeout) { + clearTimeout(this.timeout); + } + + this.destroyed = true; + + this.hook.trigger.removeEventListener('keydown.dl', this.debounceTriggerWrapper); + this.hook.trigger.removeEventListener('focus', this.debounceTriggerWrapper); + } + }; +}); +},{"../window":2}],2:[function(require,module,exports){ +module.exports = function(callback) { + return (function() { + callback(this); + }).call(null); +}; + +},{}]},{},[1])(1) +});
\ No newline at end of file diff --git a/app/assets/javascripts/droplab/droplab_filter.js b/app/assets/javascripts/droplab/droplab_filter.js new file mode 100644 index 00000000000..41a220831f9 --- /dev/null +++ b/app/assets/javascripts/droplab/droplab_filter.js @@ -0,0 +1,60 @@ +/* eslint-disable */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.filter||(g.filter = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +/* global droplab */ + +require('../window')(function(w){ + w.droplabFilter = { + + keydownWrapper: function(e){ + var list = e.detail.hook.list; + var data = list.data; + var value = e.detail.hook.trigger.value.toLowerCase(); + var config = e.detail.hook.config.droplabFilter; + var matches = []; + var filterFunction; + // will only work on dynamically set data + if(!data){ + return; + } + + if (config && config.filterFunction && typeof config.filterFunction === 'function') { + filterFunction = config.filterFunction; + } else { + filterFunction = function(o){ + // cheap string search + o.droplab_hidden = o[config.template].toLowerCase().indexOf(value) === -1; + return o; + }; + } + + matches = data.map(function(o) { + return filterFunction(o, value); + }); + list.render(matches); + }, + + init: function init(hookInput) { + var config = hookInput.config.droplabFilter; + + if (!config || (!config.template && !config.filterFunction)) { + return; + } + + this.hookInput = hookInput; + this.hookInput.trigger.addEventListener('keyup.dl', this.keydownWrapper); + }, + + destroy: function destroy(){ + this.hookInput.trigger.removeEventListener('keyup.dl', this.keydownWrapper); + } + }; +}); +},{"../window":2}],2:[function(require,module,exports){ +module.exports = function(callback) { + return (function() { + callback(this); + }).call(null); +}; + +},{}]},{},[1])(1) +});
\ No newline at end of file diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 1a0aa9757ba..3d183f4ecb4 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* 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 */ @@ -119,7 +120,7 @@ if (item.type.indexOf("image") !== -1) { return item; } - i++; + i += 1; } return false; }; @@ -214,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 fd7f961aab9..d81d4cf8425 100644 --- a/app/assets/javascripts/due_date_select.js.es6 +++ b/app/assets/javascripts/due_date_select.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* 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 { constructor({ $dropdown, $loading } = {}) { @@ -15,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; @@ -79,9 +80,12 @@ } parseSelectedDate() { - this.rawSelectedDate = $("input[name='" + this.fieldName + "']").val(); + this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val(); + if (this.rawSelectedDate.length) { - let dateObj = new Date(this.rawSelectedDate); + // Construct Date object manually to avoid buggy dateString support within Date constructor + const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10)); + const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]); this.displayedDate = $.datepicker.formatDate('M d, yy', dateObj); } else { this.displayedDate = 'No due date'; @@ -131,7 +135,6 @@ return selectedDateValue.length ? $('.js-remove-due-date-holder').removeClass('hidden') : $('.js-remove-due-date-holder').addClass('hidden'); - } }).done((data) => { if (isDropdown) { @@ -145,25 +148,19 @@ class DueDateSelectors { constructor() { - this.initMilestoneDueDate(); + this.initMilestoneDatePicker(); this.initIssuableSelect(); } - initMilestoneDueDate() { - const $datePicker = $('.datepicker'); + initMilestoneDatePicker() { + $('.datepicker').datepicker({ + dateFormat: 'yy-mm-dd' + }); - if ($datePicker.length) { - const $dueDate = $('#milestone_due_date'); - $datePicker.datepicker({ - dateFormat: 'yy-mm-dd', - onSelect: (dateText, inst) => { - $dueDate.val(dateText); - } - }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val())); - } - $('.js-clear-due-date').on('click', (e) => { + $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => { e.preventDefault(); - $.datepicker._clearDate($datePicker); + const datepicker = $(e.target).siblings('.datepicker'); + $.datepicker._clearDate(datepicker); }); } @@ -181,5 +178,4 @@ } global.DueDateSelectors = DueDateSelectors; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 new file mode 100644 index 00000000000..fea642467fa --- /dev/null +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -0,0 +1,223 @@ +/* eslint-disable no-param-reassign, no-new */ +/* global Vue */ +/* global EnvironmentsService */ +/* global Flash */ + +//= require vue +//= require vue-resource +//= require_tree ../services/ +//= require ./environment_item + +(() => { + window.gl = window.gl || {}; + + gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', { + props: { + store: { + type: Object, + required: true, + default: () => ({}), + }, + }, + + components: { + 'environment-item': gl.environmentsList.EnvironmentItem, + }, + + data() { + const environmentsData = document.querySelector('#environments-list-view').dataset; + + return { + state: this.store.state, + visibility: 'available', + isLoading: false, + cssContainerClass: environmentsData.cssClass, + endpoint: environmentsData.environmentsDataEndpoint, + canCreateDeployment: environmentsData.canCreateDeployment, + canReadEnvironment: environmentsData.canReadEnvironment, + canCreateEnvironment: environmentsData.canCreateEnvironment, + projectEnvironmentsPath: environmentsData.projectEnvironmentsPath, + projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath, + newEnvironmentPath: environmentsData.newEnvironmentPath, + helpPagePath: environmentsData.helpPagePath, + commitIconSvg: environmentsData.commitIconSvg, + playIconSvg: environmentsData.playIconSvg, + terminalIconSvg: environmentsData.terminalIconSvg, + }; + }, + + computed: { + scope() { + return this.$options.getQueryParameter('scope'); + }, + + canReadEnvironmentParsed() { + return this.$options.convertPermissionToBoolean(this.canReadEnvironment); + }, + + canCreateDeploymentParsed() { + return this.$options.convertPermissionToBoolean(this.canCreateDeployment); + }, + + canCreateEnvironmentParsed() { + return this.$options.convertPermissionToBoolean(this.canCreateEnvironment); + }, + }, + + /** + * Fetches all the environments and stores them. + * Toggles loading property. + */ + created() { + gl.environmentsService = new EnvironmentsService(this.endpoint); + + const scope = this.$options.getQueryParameter('scope'); + if (scope) { + this.store.storeVisibility(scope); + } + + this.isLoading = true; + + return gl.environmentsService.all() + .then(resp => resp.json()) + .then((json) => { + this.store.storeEnvironments(json); + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the environments.', 'alert'); + }); + }, + + /** + * Transforms the url parameter into an object and + * returns the one requested. + * + * @param {String} param + * @returns {String} The value of the requested parameter. + */ + getQueryParameter(parameter) { + return window.location.search.substring(1).split('&').reduce((acc, param) => { + const paramSplited = param.split('='); + acc[paramSplited[0]] = paramSplited[1]; + return acc; + }, {})[parameter]; + }, + + /** + * Converts permission provided as strings to booleans. + * @param {String} string + * @returns {Boolean} + */ + convertPermissionToBoolean(string) { + return string === 'true'; + }, + + methods: { + toggleRow(model) { + return this.store.toggleFolder(model.name); + }, + }, + + template: ` + <div :class="cssContainerClass"> + <div class="top-area"> + <ul v-if="!isLoading" class="nav-links"> + <li v-bind:class="{ 'active': scope === undefined }"> + <a :href="projectEnvironmentsPath"> + Available + <span class="badge js-available-environments-count"> + {{state.availableCounter}} + </span> + </a> + </li><li v-bind:class="{ 'active' : scope === 'stopped' }"> + <a :href="projectStoppedEnvironmentsPath"> + Stopped + <span class="badge js-stopped-environments-count"> + {{state.stoppedCounter}} + </span> + </a> + </li> + </ul> + <div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls"> + <a :href="newEnvironmentPath" class="btn btn-create"> + New environment + </a> + </div> + </div> + + <div class="environments-container"> + <div class="environments-list-loading text-center" v-if="isLoading"> + <i class="fa fa-spinner fa-spin"></i> + </div> + + <div class="blank-state blank-state-no-icon" + v-if="!isLoading && state.environments.length === 0"> + <h2 class="blank-state-title js-blank-state-title"> + You don't have any environments right now. + </h2> + <p class="blank-state-text"> + Environments are places where code gets deployed, such as staging or production. + <br /> + <a :href="helpPagePath"> + Read more about environments + </a> + </p> + + <a + v-if="canCreateEnvironmentParsed" + :href="newEnvironmentPath" + class="btn btn-create js-new-environment-button"> + New Environment + </a> + </div> + + <div class="table-holder" + v-if="!isLoading && state.filteredEnvironments.length > 0"> + <table class="table ci-table environments"> + <thead> + <tr> + <th class="environments-name">Environment</th> + <th class="environments-deploy">Last deployment</th> + <th class="environments-build">Build</th> + <th class="environments-commit">Commit</th> + <th class="environments-date">Created</th> + <th class="hidden-xs environments-actions"></th> + </tr> + </thead> + <tbody> + <template v-for="model in state.filteredEnvironments" + v-bind:model="model"> + + <tr + is="environment-item" + :model="model" + :toggleRow="toggleRow.bind(model)" + :can-create-deployment="canCreateDeploymentParsed" + :can-read-environment="canReadEnvironmentParsed" + :play-icon-svg="playIconSvg" + :terminal-icon-svg="terminalIconSvg" + :commit-icon-svg="commitIconSvg"></tr> + + <tr v-if="model.isOpen && model.children && model.children.length > 0" + is="environment-item" + v-for="children in model.children" + :model="children" + :toggleRow="toggleRow.bind(children)" + :can-create-deployment="canCreateDeploymentParsed" + :can-read-environment="canReadEnvironmentParsed" + :play-icon-svg="playIconSvg" + :terminal-icon-svg="terminalIconSvg" + :commit-icon-svg="commitIconSvg"> + </tr> + + </template> + </tbody> + </table> + </div> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 new file mode 100644 index 00000000000..81468f4d3bc --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_actions.js.es6 @@ -0,0 +1,49 @@ +/*= require vue */ +/* global Vue */ + +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + gl.environmentsList.ActionsComponent = Vue.component('actions-component', { + props: { + actions: { + type: Array, + required: false, + default: () => [], + }, + + playIconSvg: { + type: String, + required: false, + }, + }, + + template: ` + <div class="inline"> + <div class="dropdown"> + <a class="dropdown-new btn btn-default" data-toggle="dropdown"> + <span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span> + <i class="fa fa-caret-down"></i> + </a> + + <ul class="dropdown-menu dropdown-menu-align-right"> + <li v-for="action in actions"> + <a :href="action.play_path" + data-method="post" + rel="nofollow" + class="js-manual-action-link"> + + <span class="js-action-play-icon-container" v-html="playIconSvg"></span> + + <span> + {{action.name}} + </span> + </a> + </li> + </ul> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6 new file mode 100644 index 00000000000..6592c1b5f0f --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_external_url.js.es6 @@ -0,0 +1,22 @@ +/*= require vue */ +/* global Vue */ + +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', { + props: { + externalUrl: { + type: String, + default: '', + }, + }, + + template: ` + <a class="btn external_url" :href="externalUrl" target="_blank"> + <i class="fa fa-external-link"></i> + </a> + `, + }); +})(); diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 new file mode 100644 index 00000000000..0e6bc3fdb2c --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -0,0 +1,537 @@ +/* global Vue */ +/* global timeago */ + +/*= require timeago */ +/*= require lib/utils/text_utility */ +/*= require vue_common_component/commit */ +/*= require ./environment_actions */ +/*= require ./environment_external_url */ +/*= require ./environment_stop */ +/*= require ./environment_rollback */ +/*= require ./environment_terminal_button */ + +(() => { + /** + * Envrionment Item Component + * + * Used in a hierarchical structure to show folders with children + * in a table. + * Recursive component based on [Tree View](https://vuejs.org/examples/tree-view.html) + * + * See this [issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/22539) + * for more information.15 + */ + + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + window.gl.environmentsList.timeagoInstance = new timeago(); // eslint-disable-line + + gl.environmentsList.EnvironmentItem = Vue.component('environment-item', { + + components: { + 'commit-component': gl.CommitComponent, + 'actions-component': gl.environmentsList.ActionsComponent, + 'external-url-component': gl.environmentsList.ExternalUrlComponent, + 'stop-component': gl.environmentsList.StopComponent, + 'rollback-component': gl.environmentsList.RollbackComponent, + 'terminal-button-component': gl.environmentsList.TerminalButtonComponent, + }, + + props: { + model: { + type: Object, + required: true, + default: () => ({}), + }, + + toggleRow: { + type: Function, + required: false, + }, + + canCreateDeployment: { + type: Boolean, + required: false, + default: false, + }, + + canReadEnvironment: { + type: Boolean, + required: false, + default: false, + }, + + commitIconSvg: { + type: String, + required: false, + }, + + playIconSvg: { + type: String, + required: false, + }, + + terminalIconSvg: { + type: String, + required: false, + }, + + }, + + data() { + return { + rowClass: { + 'children-row': this.model['vue-isChildren'], + }, + }; + }, + + computed: { + + /** + * If an item has a `children` entry it means it is a folder. + * Folder items have different behaviours - it is possible to toggle + * them and show their children. + * + * @returns {Boolean|Undefined} + */ + isFolder() { + return this.model.children && this.model.children.length > 0; + }, + + /** + * If an item is inside a folder structure will return true. + * Used for css purposes. + * + * @returns {Boolean|undefined} + */ + isChildren() { + return this.model['vue-isChildren']; + }, + + /** + * Counts the number of environments in each folder. + * Used to show a badge with the counter. + * + * @returns {Number|Undefined} The number of environments for the current folder. + */ + childrenCounter() { + return this.model.children && this.model.children.length; + }, + + /** + * Verifies if `last_deployment` key exists in the current Envrionment. + * This key is required to render most of the html - this method works has + * an helper. + * + * @returns {Boolean} + */ + hasLastDeploymentKey() { + if (this.model.last_deployment && + !this.$options.isObjectEmpty(this.model.last_deployment)) { + return true; + } + return false; + }, + + /** + * Verifies is the given environment has manual actions. + * Used to verify if we should render them or nor. + * + * @returns {Boolean|Undefined} + */ + hasManualActions() { + return this.model.last_deployment && this.model.last_deployment.manual_actions && + this.model.last_deployment.manual_actions.length > 0; + }, + + /** + * Returns the value of the `stoppable?` key provided in the response. + * + * @returns {Boolean} + */ + isStoppable() { + return this.model['stoppable?']; + }, + + /** + * Verifies if the `deployable` key is present in `last_deployment` key. + * Used to verify whether we should or not render the rollback partial. + * + * @returns {Boolean|Undefined} + */ + canRetry() { + return this.hasLastDeploymentKey && + this.model.last_deployment && + this.model.last_deployment.deployable; + }, + + /** + * Verifies if the date to be shown is present. + * + * @returns {Boolean|Undefined} + */ + canShowDate() { + return this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable !== undefined; + }, + + /** + * Human readable date. + * + * @returns {String} + */ + createdDate() { + return gl.environmentsList.timeagoInstance.format( + this.model.last_deployment.deployable.created_at, + ); + }, + + /** + * Returns the manual actions with the name parsed. + * + * @returns {Array.<Object>|Undefined} + */ + manualActions() { + if (this.hasManualActions) { + return this.model.last_deployment.manual_actions.map((action) => { + const parsedAction = { + name: gl.text.humanize(action.name), + play_path: action.play_path, + }; + return parsedAction; + }); + } + return []; + }, + + /** + * Builds the string used in the user image alt attribute. + * + * @returns {String} + */ + userImageAltDescription() { + if (this.model.last_deployment && + this.model.last_deployment.user && + this.model.last_deployment.user.username) { + return `${this.model.last_deployment.user.username}'s avatar'`; + } + return ''; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {String|Undefined} + */ + commitTag() { + if (this.model.last_deployment && + this.model.last_deployment.tag) { + return this.model.last_deployment.tag; + } + return undefined; + }, + + /** + * If provided, returns the commit ref. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.model.last_deployment && this.model.last_deployment.ref) { + return this.model.last_deployment.ref; + } + return undefined; + }, + + /** + * If provided, returns the commit url. + * + * @returns {String|Undefined} + */ + commitUrl() { + if (this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.commit_path) { + return this.model.last_deployment.commit.commit_path; + } + return undefined; + }, + + /** + * If provided, returns the commit short sha. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if (this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.short_id) { + return this.model.last_deployment.commit.short_id; + } + return undefined; + }, + + /** + * If provided, returns the commit title. + * + * @returns {String|Undefined} + */ + commitTitle() { + if (this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.title) { + return this.model.last_deployment.commit.title; + } + return undefined; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {Object|Undefined} + */ + commitAuthor() { + if (this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.author) { + return this.model.last_deployment.commit.author; + } + + return undefined; + }, + + /** + * Verifies if the `retry_path` key is present and returns its value. + * + * @returns {String|Undefined} + */ + retryUrl() { + if (this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.retry_path) { + return this.model.last_deployment.deployable.retry_path; + } + return undefined; + }, + + /** + * Verifies if the `last?` key is present and returns its value. + * + * @returns {Boolean|Undefined} + */ + isLastDeployment() { + return this.model.last_deployment && this.model.last_deployment['last?']; + }, + + /** + * Builds the name of the builds needed to display both the name and the id. + * + * @returns {String} + */ + buildName() { + if (this.model.last_deployment && + this.model.last_deployment.deployable) { + return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`; + } + return ''; + }, + + /** + * Builds the needed string to show the internal id. + * + * @returns {String} + */ + deploymentInternalId() { + if (this.model.last_deployment && + this.model.last_deployment.iid) { + return `#${this.model.last_deployment.iid}`; + } + return ''; + }, + + /** + * Verifies if the user object is present under last_deployment object. + * + * @returns {Boolean} + */ + deploymentHasUser() { + return !this.$options.isObjectEmpty(this.model.last_deployment) && + !this.$options.isObjectEmpty(this.model.last_deployment.user); + }, + + /** + * Returns the user object nested with the last_deployment object. + * Used to render the template. + * + * @returns {Object} + */ + deploymentUser() { + if (!this.$options.isObjectEmpty(this.model.last_deployment) && + !this.$options.isObjectEmpty(this.model.last_deployment.user)) { + return this.model.last_deployment.user; + } + return {}; + }, + + /** + * Verifies if the build name column should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderBuildName() { + return !this.isFolder && + !this.$options.isObjectEmpty(this.model.last_deployment) && + !this.$options.isObjectEmpty(this.model.last_deployment.deployable); + }, + + /** + * Verifies if deplyment internal ID should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderDeploymentID() { + return !this.isFolder && + !this.$options.isObjectEmpty(this.model.last_deployment) && + this.model.last_deployment.iid !== undefined; + }, + }, + + /** + * Helper to verify if certain given object are empty. + * Should be replaced by lodash _.isEmpty - https://lodash.com/docs/4.17.2#isEmpty + * @param {Object} object + * @returns {Bollean} + */ + isObjectEmpty(object) { + for (const key in object) { // eslint-disable-line + if (hasOwnProperty.call(object, key)) { + return false; + } + } + return true; + }, + + template: ` + <tr> + <td v-bind:class="{ 'children-row': isChildren}"> + <a v-if="!isFolder" + class="environment-name" + :href="model.environment_path"> + {{model.name}} + </a> + <span v-else v-on:click="toggleRow(model)" class="folder-name"> + <span class="folder-icon"> + <i v-show="model.isOpen" class="fa fa-caret-down"></i> + <i v-show="!model.isOpen" class="fa fa-caret-right"></i> + </span> + + <span> + {{model.name}} + </span> + + <span class="badge"> + {{childrenCounter}} + </span> + </span> + </td> + + <td class="deployment-column"> + <span v-if="shouldRenderDeploymentID"> + {{deploymentInternalId}} + </span> + + <span v-if="!isFolder && deploymentHasUser"> + by + <a :href="deploymentUser.web_url" class="js-deploy-user-container"> + <img class="avatar has-tooltip s20" + :src="deploymentUser.avatar_url" + :alt="userImageAltDescription" + :title="deploymentUser.username" /> + </a> + </span> + </td> + + <td class="environments-build-cell"> + <a v-if="shouldRenderBuildName" + class="build-link" + :href="model.last_deployment.deployable.build_path"> + {{buildName}} + </a> + </td> + + <td> + <div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component"> + <commit-component + :tag="commitTag" + :commit-ref="commitRef" + :commit-url="commitUrl" + :short-sha="commitShortSha" + :title="commitTitle" + :author="commitAuthor" + :commit-icon-svg="commitIconSvg"> + </commit-component> + </div> + <p v-if="!isFolder && !hasLastDeploymentKey" class="commit-title"> + No deployments yet + </p> + </td> + + <td> + <span + v-if="!isFolder && canShowDate" + class="environment-created-date-timeago"> + {{createdDate}} + </span> + </td> + + <td class="hidden-xs"> + <div v-if="!isFolder"> + <div v-if="hasManualActions && canCreateDeployment" + class="inline js-manual-actions-container"> + <actions-component + :play-icon-svg="playIconSvg" + :actions="manualActions"> + </actions-component> + </div> + + <div v-if="model.external_url && canReadEnvironment" + class="inline js-external-url-container"> + <external-url-component + :external-url="model.external_url"> + </external-url-component> + </div> + + <div v-if="isStoppable && canCreateDeployment" + class="inline js-stop-component-container"> + <stop-component + :stop-url="model.stop_path"> + </stop-component> + </div> + + <div v-if="model.terminal_path" + class="inline js-terminal-button-container"> + <terminal-button-component + :terminal-icon-svg="terminalIconSvg" + :terminal-path="model.terminal_path"> + </terminal-button-component> + </div> + + <div v-if="canRetry && canCreateDeployment" + class="inline js-rollback-component-container"> + <rollback-component + :is-last-deployment="isLastDeployment" + :retry-url="retryUrl"> + </rollback-component> + </div> + </div> + </td> + </tr> + `, + }); +})(); diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6 new file mode 100644 index 00000000000..b52298b4a88 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_rollback.js.es6 @@ -0,0 +1,32 @@ +/*= require vue */ +/* global Vue */ + +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + gl.environmentsList.RollbackComponent = Vue.component('rollback-component', { + props: { + retryUrl: { + type: String, + default: '', + }, + + isLastDeployment: { + type: Boolean, + default: true, + }, + }, + + template: ` + <a class="btn" :href="retryUrl" data-method="post" rel="nofollow"> + <span v-if="isLastDeployment"> + Re-deploy + </span> + <span v-else> + Rollback + </span> + </a> + `, + }); +})(); diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6 new file mode 100644 index 00000000000..0a29f2f36e9 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_stop.js.es6 @@ -0,0 +1,26 @@ +/*= require vue */ +/* global Vue */ + +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + gl.environmentsList.StopComponent = Vue.component('stop-component', { + props: { + stopUrl: { + type: String, + default: '', + }, + }, + + template: ` + <a class="btn stop-env-link" + :href="stopUrl" + data-confirm="Are you sure you want to stop this environment?" + data-method="post" + rel="nofollow"> + <i class="fa fa-stop stop-env-icon"></i> + </a> + `, + }); +})(); diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 new file mode 100644 index 00000000000..050184ba497 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 @@ -0,0 +1,27 @@ +/*= require vue */ +/* global Vue */ + +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', { + props: { + terminalPath: { + type: String, + default: '', + }, + terminalIconSvg: { + type: String, + default: '', + }, + }, + + template: ` + <a class="btn terminal-button" + :href="terminalPath"> + <span class="js-terminal-icon-container" v-html="terminalIconSvg"></span> + </a> + `, + }); +})(); diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 new file mode 100644 index 00000000000..9f24a6a4f88 --- /dev/null +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -0,0 +1,23 @@ +//= require vue +//= require_tree ./stores/ +//= require ./components/environment +//= require ./vue_resource_interceptor + + +$(() => { + window.gl = window.gl || {}; + + if (gl.EnvironmentsListApp) { + gl.EnvironmentsListApp.$destroy(true); + } + const Store = gl.environmentsList.EnvironmentsStore; + + gl.EnvironmentsListApp = new gl.environmentsList.EnvironmentsComponent({ + el: document.querySelector('#environments-list-view'), + + propsData: { + store: Store.create(), + }, + + }); +}); diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6 new file mode 100644 index 00000000000..575a45d9802 --- /dev/null +++ b/app/assets/javascripts/environments/services/environments_service.js.es6 @@ -0,0 +1,24 @@ +/* globals Vue */ +/* eslint-disable no-unused-vars, no-param-reassign */ +class EnvironmentsService { + + constructor(root) { + Vue.http.options.root = root; + + this.environments = Vue.resource(root); + + Vue.http.interceptors.push((request, next) => { + // needed in order to not break the tests. + if ($.rails) { + request.headers['X-CSRF-Token'] = $.rails.csrfToken(); + } + next(); + }); + } + + all() { + return this.environments.get(); + } +} + +window.EnvironmentsService = EnvironmentsService; diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6 new file mode 100644 index 00000000000..9b4090100da --- /dev/null +++ b/app/assets/javascripts/environments/stores/environments_store.js.es6 @@ -0,0 +1,190 @@ +/* eslint-disable no-param-reassign */ +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + gl.environmentsList.EnvironmentsStore = { + state: {}, + + create() { + this.state.environments = []; + this.state.stoppedCounter = 0; + this.state.availableCounter = 0; + this.state.visibility = 'available'; + this.state.filteredEnvironments = []; + + return this; + }, + + /** + * In order to display a tree view we need to modify the received + * data in to a tree structure based on `environment_type` + * sorted alphabetically. + * In each children a `vue-` property will be added. This property will be + * used to know if an item is a children mostly for css purposes. This is + * needed because the children row is a fragment instance and therfore does + * not accept non-prop attributes. + * + * + * @example + * it will transform this: + * [ + * { name: "environment", environment_type: "review" }, + * { name: "environment_1", environment_type: null } + * { name: "environment_2, environment_type: "review" } + * ] + * into this: + * [ + * { name: "review", children: + * [ + * { name: "environment", environment_type: "review", vue-isChildren: true}, + * { name: "environment_2", environment_type: "review", vue-isChildren: true} + * ] + * }, + * {name: "environment_1", environment_type: null} + * ] + * + * + * @param {Array} environments List of environments. + * @returns {Array} Tree structured array with the received environments. + */ + storeEnvironments(environments = []) { + this.state.stoppedCounter = this.countByState(environments, 'stopped'); + this.state.availableCounter = this.countByState(environments, 'available'); + + const environmentsTree = environments.reduce((acc, environment) => { + if (environment.environment_type !== null) { + const occurs = acc.filter(element => element.children && + element.name === environment.environment_type); + + environment['vue-isChildren'] = true; + + if (occurs.length) { + acc[acc.indexOf(occurs[0])].children.push(environment); + acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName); + } else { + acc.push({ + name: environment.environment_type, + children: [environment], + isOpen: false, + 'vue-isChildren': environment['vue-isChildren'], + }); + } + } else { + acc.push(environment); + } + + return acc; + }, []).slice().sort(this.sortByName); + + this.state.environments = environmentsTree; + + this.filterEnvironmentsByVisibility(this.state.environments); + + return environmentsTree; + }, + + storeVisibility(visibility) { + this.state.visibility = visibility; + }, + /** + * Given the visibility prop provided by the url query parameter and which + * changes according to the active tab we need to filter which environments + * should be visible. + * + * The environments array is a recursive tree structure and we need to filter + * both root level environments and children environments. + * + * In order to acomplish that, both `filterState` and `filterEnvironmentsByVisibility` + * functions work together. + * The first one works as the filter that verifies if the given environment matches + * the given state. + * The second guarantees both root level and children elements are filtered as well. + * + * Given array of environments will return only + * the environments that match the state stored. + * + * @param {Array} array + * @return {Array} + */ + filterEnvironmentsByVisibility(arr) { + const filteredEnvironments = arr.map((item) => { + if (item.children) { + const filteredChildren = this.filterEnvironmentsByVisibility( + item.children, + ).filter(Boolean); + + if (filteredChildren.length) { + item.children = filteredChildren; + return item; + } + } + + return this.filterState(this.state.visibility, item); + }).filter(Boolean); + + this.state.filteredEnvironments = filteredEnvironments; + return filteredEnvironments; + }, + + /** + * Given the state and the environment, + * returns only if the environment state matches the one provided. + * + * @param {String} state + * @param {Object} environment + * @return {Object} + */ + filterState(state, environment) { + return environment.state === state && environment; + }, + + /** + * Toggles folder open property given the environment type. + * + * @param {String} envType + * @return {Array} + */ + toggleFolder(envType) { + const environments = this.state.environments; + + const environmentsCopy = environments.map((env) => { + if (env['vue-isChildren'] && env.name === envType) { + env.isOpen = !env.isOpen; + } + + return env; + }); + + this.state.environments = environmentsCopy; + + return environmentsCopy; + }, + + /** + * Given an array of environments, returns the number of environments + * that have the given state. + * + * @param {Array} environments + * @param {String} state + * @returns {Number} + */ + countByState(environments, state) { + return environments.filter(env => env.state === state).length; + }, + + /** + * Sorts the two objects provided by their name. + * + * @param {Object} a + * @param {Object} b + * @returns {Number} + */ + sortByName(a, b) { + const nameA = a.name.toUpperCase(); + const nameB = b.name.toUpperCase(); + + return nameA < nameB ? -1 : nameA > nameB ? 1 : 0; // eslint-disable-line + }, + }; +})(); diff --git a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 new file mode 100644 index 00000000000..406bdbc1c7d --- /dev/null +++ b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 @@ -0,0 +1,12 @@ +/* global Vue */ +Vue.http.interceptors.push((request, next) => { + Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; + + next((response) => { + if (typeof response.data === 'string') { + response.data = JSON.parse(response.data); // eslint-disable-line + } + + Vue.activeResources--; // eslint-disable-line + }); +}); diff --git a/app/assets/javascripts/extensions/array.js b/app/assets/javascripts/extensions/array.js deleted file mode 100644 index 4c9e219aa43..00000000000 --- a/app/assets/javascripts/extensions/array.js +++ /dev/null @@ -1,8 +0,0 @@ -/* eslint-disable */ -Array.prototype.first = function() { - return this[0]; -} - -Array.prototype.last = function() { - return this[this.length-1]; -} diff --git a/app/assets/javascripts/extensions/array.js.es6 b/app/assets/javascripts/extensions/array.js.es6 new file mode 100644 index 00000000000..cd401277689 --- /dev/null +++ b/app/assets/javascripts/extensions/array.js.es6 @@ -0,0 +1,24 @@ +/* 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'); + if (typeof predicate !== 'function') throw new TypeError('predicate must be a function'); + + const list = Object(this); + const thisArg = args[1]; + let value = {}; + + for (let i = 0; i < list.length; i += 1) { + value = list[i]; + if (predicate.call(thisArg, value, i, list)) return value; + } + + return undefined; +}; diff --git a/app/assets/javascripts/extensions/custom_event.js.es6 b/app/assets/javascripts/extensions/custom_event.js.es6 new file mode 100644 index 00000000000..abedae4c1c7 --- /dev/null +++ b/app/assets/javascripts/extensions/custom_event.js.es6 @@ -0,0 +1,12 @@ +/* global CustomEvent */ +/* eslint-disable no-global-assign */ + +// Custom event support for IE +CustomEvent = function CustomEvent(event, parameters) { + const params = parameters || { bubbles: false, cancelable: false, detail: undefined }; + const evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; +}; + +CustomEvent.prototype = window.Event.prototype; diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6 index afb2f0d6956..90ab79305a7 100644 --- a/app/assets/javascripts/extensions/element.js.es6 +++ b/app/assets/javascripts/extensions/element.js.es6 @@ -1,9 +1,20 @@ /* global Element */ -/* eslint-disable consistent-return, max-len */ +/* eslint-disable consistent-return, max-len, no-empty, func-names */ -Element.prototype.matches = Element.prototype.matches || Element.prototype.msMatchesSelector; - -Element.prototype.closest = function closest(selector, selectedElement = this) { +Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) { if (!selectedElement) return; return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement); }; + +Element.prototype.matches = Element.prototype.matches || + Element.prototype.matchesSelector || + Element.prototype.mozMatchesSelector || + Element.prototype.msMatchesSelector || + Element.prototype.oMatchesSelector || + Element.prototype.webkitMatchesSelector || + function (s) { + const matches = (this.document || this.ownerDocument).querySelectorAll(s); + 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 623a80b7053..d3b58b2707a 100644 --- a/app/assets/javascripts/extensions/jquery.js +++ b/app/assets/javascripts/extensions/jquery.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* 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/extensions/object.js.es6 b/app/assets/javascripts/extensions/object.js.es6 new file mode 100644 index 00000000000..70a2d765abd --- /dev/null +++ b/app/assets/javascripts/extensions/object.js.es6 @@ -0,0 +1,26 @@ +/* eslint-disable no-restricted-syntax */ + +// Adapted from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill +if (typeof Object.assign !== 'function') { + Object.assign = function assign(target, ...args) { + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + const to = Object(target); + + for (let index = 0; index < args.length; index += 1) { + const nextSource = args[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (const nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }; +} diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 732136f1f2c..895a872568d 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -1,6 +1,8 @@ -/* eslint-disable */ +/* 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; @@ -130,7 +132,6 @@ }; return FilesCommentButton; - })(); $.fn.filesCommentButton = function() { @@ -143,5 +144,4 @@ } }); }; - }).call(this); diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 new file mode 100644 index 00000000000..7d297b8eee8 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -0,0 +1,69 @@ +/*= require filtered_search/filtered_search_dropdown */ + +/* global droplabFilter */ + +(() => { + class DropdownHint extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter) { + super(droplab, dropdown, input, filter); + this.config = { + droplabFilter: { + template: 'hint', + filterFunction: gl.DropdownUtils.filterHint.bind(null, input), + }, + }; + } + + itemClicked(e) { + const { selected } = e.detail; + + if (selected.tagName === 'LI') { + if (selected.hasAttribute('data-value')) { + this.dismissDropdown(); + } else if (selected.getAttribute('data-action') === 'submit') { + this.dismissDropdown(); + this.dispatchFormSubmitEvent(); + } else { + const token = selected.querySelector('.js-filter-hint').innerText.trim(); + const tag = selected.querySelector('.js-filter-tag').innerText.trim(); + + if (tag.length) { + gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', '')); + } + this.dismissDropdown(); + this.dispatchInputEvent(); + } + } + } + + renderContent() { + const dropdownData = [{ + icon: 'fa-pencil', + hint: 'author:', + tag: '<@author>', + }, { + icon: 'fa-user', + hint: 'assignee:', + tag: '<@assignee>', + }, { + icon: 'fa-clock-o', + hint: 'milestone:', + tag: '<%milestone>', + }, { + icon: 'fa-tag', + hint: 'label:', + tag: '<~label>', + }]; + + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); + this.droplab.setData(this.hookId, dropdownData); + } + + init() { + this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init(); + } + } + + window.gl = window.gl || {}; + gl.DropdownHint = DropdownHint; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 new file mode 100644 index 00000000000..13cbec1be4a --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -0,0 +1,44 @@ +/*= require filtered_search/filtered_search_dropdown */ + +/* global droplabAjax */ +/* global droplabFilter */ + +(() => { + class DropdownNonUser extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter, endpoint, symbol) { + super(droplab, dropdown, input, filter); + this.symbol = symbol; + this.config = { + droplabAjax: { + endpoint, + method: 'setData', + loadingTemplate: this.loadingTemplate, + }, + droplabFilter: { + filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input), + }, + }; + } + + itemClicked(e) { + super.itemClicked(e, (selected) => { + const title = selected.querySelector('.js-data-value').innerText.trim(); + return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`; + }); + } + + renderContent(forceShowList = false) { + this.droplab + .changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); + super.renderContent(forceShowList); + } + + init() { + this.droplab + .addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); + } + } + + window.gl = window.gl || {}; + gl.DropdownNonUser = DropdownNonUser; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 new file mode 100644 index 00000000000..7bf199d9274 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -0,0 +1,53 @@ +/*= require filtered_search/filtered_search_dropdown */ + +/* global droplabAjaxFilter */ + +(() => { + class DropdownUser extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter) { + super(droplab, dropdown, input, filter); + this.config = { + droplabAjaxFilter: { + endpoint: '/autocomplete/users.json', + searchKey: 'search', + params: { + per_page: 20, + active: true, + project_id: this.getProjectId(), + current_user: true, + }, + searchValueFunction: this.getSearchInput.bind(this), + loadingTemplate: this.loadingTemplate, + }, + }; + } + + itemClicked(e) { + super.itemClicked(e, + selected => selected.querySelector('.dropdown-light-content').innerText.trim()); + } + + renderContent(forceShowList = false) { + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); + super.renderContent(forceShowList); + } + + getProjectId() { + return this.input.getAttribute('data-project-id'); + } + + getSearchInput() { + const query = gl.DropdownUtils.getSearchInput(this.input); + const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + + return lastToken.value || ''; + } + + init() { + this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init(); + } + } + + window.gl = window.gl || {}; + gl.DropdownUser = DropdownUser; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 new file mode 100644 index 00000000000..443ac222f70 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 @@ -0,0 +1,120 @@ +(() => { + class DropdownUtils { + static getEscapedText(text) { + let escapedText = text; + const hasSpace = text.indexOf(' ') !== -1; + const hasDoubleQuote = text.indexOf('"') !== -1; + + // Encapsulate value with quotes if it has spaces + // Known side effect: values's with both single and double quotes + // won't escape properly + if (hasSpace) { + if (hasDoubleQuote) { + escapedText = `'${text}'`; + } else { + // Encapsulate singleQuotes or if it hasSpace + escapedText = `"${text}"`; + } + } + + return escapedText; + } + + static filterWithSymbol(filterSymbol, input, item) { + const updatedItem = item; + const query = gl.DropdownUtils.getSearchInput(input); + const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query); + + if (lastToken !== searchToken) { + const title = updatedItem.title.toLowerCase(); + let value = lastToken.value.toLowerCase(); + value = value.replace(/"(.*?)"/g, str => str.slice(1).slice(0, -1)); + + // Eg. filterSymbol = ~ for labels + const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1; + const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1; + + updatedItem.droplab_hidden = !match && !matchWithoutSymbol; + } else { + updatedItem.droplab_hidden = false; + } + + return updatedItem; + } + + static filterHint(input, item) { + const updatedItem = item; + const query = gl.DropdownUtils.getSearchInput(input); + let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + lastToken = lastToken.key || lastToken || ''; + + if (!lastToken || query.split('').last() === ' ') { + updatedItem.droplab_hidden = false; + } else if (lastToken) { + const split = lastToken.split(':'); + const tokenName = split[0].split(' ').last(); + + const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1; + updatedItem.droplab_hidden = tokenName ? match : false; + } + + return updatedItem; + } + + static setDataValueIfSelected(filter, selected) { + const dataValue = selected.getAttribute('data-value'); + + if (dataValue) { + gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue); + } + + // Return boolean based on whether it was set + return dataValue !== null; + } + + static getSearchInput(filteredSearchInput) { + const inputValue = filteredSearchInput.value; + const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput); + + return inputValue.slice(0, right); + } + + static getInputSelectionPosition(input) { + const selectionStart = input.selectionStart; + let inputValue = input.value; + // Replace all spaces inside quote marks with underscores + // This helps with matching the beginning & end of a token:key + inputValue = inputValue.replace(/"(.*?)"/g, str => str.replace(/\s/g, '_')); + + // Get the right position for the word selected + // Regex matches first space + let right = inputValue.slice(selectionStart).search(/\s/); + + if (right >= 0) { + right += selectionStart; + } else if (right < 0) { + right = inputValue.length; + } + + // Get the left position for the word selected + // Regex matches last non-whitespace character + let left = inputValue.slice(0, right).search(/\S+$/); + + if (selectionStart === 0) { + left = 0; + } else if (selectionStart === inputValue.length && left < 0) { + left = inputValue.length; + } else if (left < 0) { + left = selectionStart; + } + + return { + left, + right, + }; + } + } + + window.gl = window.gl || {}; + gl.DropdownUtils = DropdownUtils; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js new file mode 100644 index 00000000000..d188718c5f3 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js @@ -0,0 +1,7 @@ + // This is a manifest file that'll be compiled into including all the files listed below. + // Add new JavaScript code in separate files in this directory and they'll automatically + // be included in the compiled file accessible from http://example.com/assets/application.js + // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the + // the compiled file. + // + /*= require_tree . */ diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 new file mode 100644 index 00000000000..859d6515531 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -0,0 +1,112 @@ +(() => { + const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; + + class FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter) { + this.droplab = droplab; + this.hookId = input.getAttribute('data-id'); + this.input = input; + this.filter = filter; + this.dropdown = dropdown; + this.loadingTemplate = `<div class="filter-dropdown-loading"> + <i class="fa fa-spinner fa-spin"></i> + </div>`; + this.bindEvents(); + } + + bindEvents() { + this.itemClickedWrapper = this.itemClicked.bind(this); + this.dropdown.addEventListener('click.dl', this.itemClickedWrapper); + } + + unbindEvents() { + this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); + } + + getCurrentHook() { + return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null; + } + + itemClicked(e, getValueFunction) { + const { selected } = e.detail; + + if (selected.tagName === 'LI' && selected.innerHTML) { + const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected); + + if (!dataValueSet) { + const value = getValueFunction(selected); + gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value); + } + + this.dismissDropdown(); + this.dispatchInputEvent(); + } + } + + setAsDropdown() { + this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`); + } + + setOffset(offset = 0) { + this.dropdown.style.left = `${offset}px`; + } + + renderContent(forceShowList = false) { + if (forceShowList && this.getCurrentHook().list.hidden) { + this.getCurrentHook().list.show(); + } + } + + render(forceRenderContent = false, forceShowList = false) { + this.setAsDropdown(); + + const currentHook = this.getCurrentHook(); + const firstTimeInitialized = currentHook === null; + + if (firstTimeInitialized || forceRenderContent) { + this.renderContent(forceShowList); + } else if (currentHook.list.list.id !== this.dropdown.id) { + this.renderContent(forceShowList); + } + } + + dismissDropdown() { + // Focusing on the input will dismiss dropdown + // (default droplab functionality) + this.input.focus(); + } + + dispatchInputEvent() { + // Propogate input change to FilteredSearchDropdownManager + // so that it can determine which dropdowns to open + this.input.dispatchEvent(new CustomEvent('input', { + bubbles: true, + cancelable: true, + })); + } + + dispatchFormSubmitEvent() { + // dispatchEvent() is necessary as form.submit() does not + // trigger event handlers + this.input.form.dispatchEvent(new Event('submit')); + } + + hideDropdown() { + this.getCurrentHook().list.hide(); + } + + resetFilters() { + const hook = this.getCurrentHook(); + const data = hook.list.data; + const results = data.map((o) => { + const updated = o; + updated.droplab_hidden = false; + return updated; + }); + hook.list.render(results); + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchDropdown = FilteredSearchDropdown; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 new file mode 100644 index 00000000000..00e1c28692f --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -0,0 +1,207 @@ +/* global DropLab */ + +(() => { + class FilteredSearchDropdownManager { + constructor() { + this.tokenizer = gl.FilteredSearchTokenizer; + this.filteredSearchInput = document.querySelector('.filtered-search'); + + this.setupMapping(); + + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('page:fetch', this.cleanupWrapper); + } + + cleanup() { + if (this.droplab) { + this.droplab.destroy(); + this.droplab = null; + } + + this.setupMapping(); + + document.removeEventListener('page:fetch', this.cleanupWrapper); + } + + setupMapping() { + this.mapping = { + author: { + reference: null, + gl: 'DropdownUser', + element: document.querySelector('#js-dropdown-author'), + }, + assignee: { + reference: null, + gl: 'DropdownUser', + element: document.querySelector('#js-dropdown-assignee'), + }, + milestone: { + reference: null, + gl: 'DropdownNonUser', + extraArguments: ['milestones.json', '%'], + element: document.querySelector('#js-dropdown-milestone'), + }, + label: { + reference: null, + gl: 'DropdownNonUser', + extraArguments: ['labels.json', '~'], + element: document.querySelector('#js-dropdown-label'), + }, + hint: { + reference: null, + gl: 'DropdownHint', + element: document.querySelector('#js-dropdown-hint'), + }, + }; + } + + static addWordToInput(tokenName, tokenValue = '') { + const input = document.querySelector('.filtered-search'); + const inputValue = input.value; + const word = `${tokenName}:${tokenValue}`; + + // Get the string to replace + let newCaretPosition = input.selectionStart; + const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input); + + input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`; + + // If we have added a tokenValue at the end of the input, + // add a space and set selection to the end + if (right >= inputValue.length && tokenValue !== '') { + input.value += ' '; + newCaretPosition = input.value.length; + } + + gl.FilteredSearchDropdownManager.updateInputCaretPosition(newCaretPosition, input); + } + + static updateInputCaretPosition(selectionStart, input) { + // Reset the position + // Sometimes can end up at end of input + input.setSelectionRange(selectionStart, selectionStart); + + const { right } = gl.DropdownUtils.getInputSelectionPosition(input); + + input.setSelectionRange(right, right); + } + + updateCurrentDropdownOffset() { + this.updateDropdownOffset(this.currentDropdown); + } + + updateDropdownOffset(key) { + if (!this.font) { + this.font = window.getComputedStyle(this.filteredSearchInput).font; + } + + const input = this.filteredSearchInput; + const inputText = input.value.slice(0, input.selectionStart); + const filterIconPadding = 27; + let offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding; + + const currentDropdownWidth = this.mapping[key].element.clientWidth === 0 ? 200 : + this.mapping[key].element.clientWidth; + const offsetMaxWidth = this.filteredSearchInput.clientWidth - currentDropdownWidth; + + if (offsetMaxWidth < offset) { + offset = offsetMaxWidth; + } + + this.mapping[key].reference.setOffset(offset); + } + + load(key, firstLoad = false) { + const mappingKey = this.mapping[key]; + const glClass = mappingKey.gl; + const element = mappingKey.element; + let forceShowList = false; + + if (!mappingKey.reference) { + const dl = this.droplab; + const defaultArguments = [null, dl, element, this.filteredSearchInput, key]; + const glArguments = defaultArguments.concat(mappingKey.extraArguments || []); + + // Passing glArguments to `new gl[glClass](<arguments>)` + mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))(); + } + + if (firstLoad) { + mappingKey.reference.init(); + } + + if (this.currentDropdown === 'hint') { + // Force the dropdown to show if it was clicked from the hint dropdown + forceShowList = true; + } + + this.updateDropdownOffset(key); + mappingKey.reference.render(firstLoad, forceShowList); + + this.currentDropdown = key; + } + + loadDropdown(dropdownName = '') { + let firstLoad = false; + + if (!this.droplab) { + firstLoad = true; + this.droplab = new DropLab(); + } + + const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); + const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key + && this.mapping[match.key]; + const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; + + if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { + const key = match && match.key ? match.key : 'hint'; + this.load(key, firstLoad); + } + } + + setDropdown() { + const { lastToken, searchToken } = this.tokenizer + .processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput)); + + if (this.currentDropdown) { + this.updateCurrentDropdownOffset(); + } + + if (lastToken === searchToken && lastToken !== null) { + // Token is not fully initialized yet because it has no value + // Eg. token = 'label:' + + const split = lastToken.split(':'); + const dropdownName = split[0].split(' ').last(); + this.loadDropdown(split.length > 1 ? dropdownName : ''); + } else if (lastToken) { + // Token has been initialized into an object because it has a value + this.loadDropdown(lastToken.key); + } else { + this.loadDropdown('hint'); + } + } + + resetDropdowns() { + // Force current dropdown to hide + this.mapping[this.currentDropdown].reference.hideDropdown(); + + // Re-Load dropdown + this.setDropdown(); + + // Reset filters for current dropdown + this.mapping[this.currentDropdown].reference.resetFilters(); + + // Reposition dropdown so that it is aligned with cursor + this.updateDropdownOffset(this.currentDropdown); + } + + destroyDroplab() { + this.droplab.destroy(); + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 new file mode 100644 index 00000000000..ae19bb68360 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -0,0 +1,216 @@ +/* global Turbolinks */ + +(() => { + class FilteredSearchManager { + constructor() { + this.filteredSearchInput = document.querySelector('.filtered-search'); + this.clearSearchButton = document.querySelector('.clear-search'); + + if (this.filteredSearchInput) { + this.tokenizer = gl.FilteredSearchTokenizer; + this.dropdownManager = new gl.FilteredSearchDropdownManager(); + + this.bindEvents(); + this.loadSearchParamsFromURL(); + this.dropdownManager.setDropdown(); + + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('page:fetch', this.cleanupWrapper); + } + } + + cleanup() { + this.unbindEvents(); + document.removeEventListener('page:fetch', this.cleanupWrapper); + } + + bindEvents() { + this.handleFormSubmit = this.handleFormSubmit.bind(this); + this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); + this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this); + this.checkForEnterWrapper = this.checkForEnter.bind(this); + this.clearSearchWrapper = this.clearSearch.bind(this); + this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); + this.tokenChange = this.tokenChange.bind(this); + + this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit); + this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); + this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); + this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper); + this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper); + this.filteredSearchInput.addEventListener('click', this.tokenChange); + this.filteredSearchInput.addEventListener('keyup', this.tokenChange); + this.clearSearchButton.addEventListener('click', this.clearSearchWrapper); + } + + unbindEvents() { + this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit); + this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); + this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); + this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper); + this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper); + this.filteredSearchInput.removeEventListener('click', this.tokenChange); + this.filteredSearchInput.removeEventListener('keyup', this.tokenChange); + this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); + } + + checkForBackspace(e) { + // 8 = Backspace Key + // 46 = Delete Key + if (e.keyCode === 8 || e.keyCode === 46) { + // Reposition dropdown so that it is aligned with cursor + this.dropdownManager.updateCurrentDropdownOffset(); + } + } + + checkForEnter(e) { + if (e.keyCode === 13) { + e.preventDefault(); + + // Prevent droplab from opening dropdown + this.dropdownManager.destroyDroplab(); + + this.search(); + } + } + + toggleClearSearchButton(e) { + if (e.target.value) { + this.clearSearchButton.classList.remove('hidden'); + } else { + this.clearSearchButton.classList.add('hidden'); + } + } + + clearSearch(e) { + e.preventDefault(); + + this.filteredSearchInput.value = ''; + this.clearSearchButton.classList.add('hidden'); + + this.dropdownManager.resetDropdowns(); + } + + handleFormSubmit(e) { + e.preventDefault(); + this.search(); + } + + loadSearchParamsFromURL() { + const params = gl.utils.getUrlParamsArray(); + const usernameParams = this.getUsernameParams(); + const inputValues = []; + + params.forEach((p) => { + const split = p.split('='); + const keyParam = decodeURIComponent(split[0]); + const value = split[1]; + + // Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys + const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(p); + + if (condition) { + inputValues.push(`${condition.tokenKey}:${condition.value}`); + } else { + // Sanitize value since URL converts spaces into + + // Replace before decode so that we know what was originally + versus the encoded + + const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; + const match = gl.FilteredSearchTokenKeys.searchByKeyParam(keyParam); + + if (match) { + const indexOf = keyParam.indexOf('_'); + const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam; + const symbol = match.symbol; + let quotationsToUse = ''; + + if (sanitizedValue.indexOf(' ') !== -1) { + // Prefer ", but use ' if required + quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; + } + + inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); + } else if (!match && keyParam === 'assignee_id') { + const id = parseInt(value, 10); + if (usernameParams[id]) { + inputValues.push(`assignee:@${usernameParams[id]}`); + } + } else if (!match && keyParam === 'author_id') { + const id = parseInt(value, 10); + if (usernameParams[id]) { + inputValues.push(`author:@${usernameParams[id]}`); + } + } else if (!match && keyParam === 'search') { + inputValues.push(sanitizedValue); + } + } + }); + + // Trim the last space value + this.filteredSearchInput.value = inputValues.join(' '); + + if (inputValues.length > 0) { + this.clearSearchButton.classList.remove('hidden'); + } + } + + search() { + const paths = []; + const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); + const currentState = gl.utils.getParameterByName('state') || 'opened'; + paths.push(`state=${currentState}`); + + tokens.forEach((token) => { + const condition = gl.FilteredSearchTokenKeys + .searchByConditionKeyValue(token.key, token.value.toLowerCase()); + const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key); + const keyParam = param ? `${token.key}_${param}` : token.key; + let tokenPath = ''; + + if (condition) { + tokenPath = condition.url; + } else { + let tokenValue = token.value; + + if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || + (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { + tokenValue = tokenValue.slice(1, tokenValue.length - 1); + } + + tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`; + } + + paths.push(tokenPath); + }); + + if (searchToken) { + paths.push(`search=${encodeURIComponent(searchToken)}`); + } + + Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`); + } + + getUsernameParams() { + const usernamesById = {}; + try { + const attribute = this.filteredSearchInput.getAttribute('data-username-params'); + JSON.parse(attribute).forEach((user) => { + usernamesById[user.id] = user.username; + }); + } catch (e) { + // do nothing + } + return usernamesById; + } + + tokenChange() { + const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; + const currentDropdownRef = dropdown.reference; + + this.setDropdownWrapper(); + currentDropdownRef.dispatchInputEvent(); + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchManager = FilteredSearchManager; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 new file mode 100644 index 00000000000..e46373024b6 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 @@ -0,0 +1,83 @@ +(() => { + const tokenKeys = [{ + key: 'author', + type: 'string', + param: 'username', + symbol: '@', + }, { + key: 'assignee', + type: 'string', + param: 'username', + symbol: '@', + }, { + key: 'milestone', + type: 'string', + param: 'title', + symbol: '%', + }, { + key: 'label', + type: 'array', + param: 'name[]', + symbol: '~', + }]; + + const conditions = [{ + url: 'assignee_id=0', + tokenKey: 'assignee', + value: 'none', + }, { + url: 'milestone_title=No+Milestone', + tokenKey: 'milestone', + value: 'none', + }, { + url: 'milestone_title=%23upcoming', + tokenKey: 'milestone', + value: 'upcoming', + }, { + url: 'label_name[]=No+Label', + tokenKey: 'label', + value: 'none', + }]; + + class FilteredSearchTokenKeys { + static get() { + return tokenKeys; + } + + static getConditions() { + return conditions; + } + + static searchByKey(key) { + return tokenKeys.find(tokenKey => tokenKey.key === key) || null; + } + + static searchBySymbol(symbol) { + return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; + } + + static searchByKeyParam(keyParam) { + return tokenKeys.find((tokenKey) => { + let tokenKeyParam = tokenKey.key; + + if (tokenKey.param) { + tokenKeyParam += `_${tokenKey.param}`; + } + + return keyParam === tokenKeyParam; + }) || null; + } + + static searchByConditionUrl(url) { + return conditions.find(condition => condition.url === url) || null; + } + + static searchByConditionKeyValue(key, value) { + return conditions + .find(condition => condition.tokenKey === key && condition.value === value) || null; + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 new file mode 100644 index 00000000000..cf53845a48b --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 @@ -0,0 +1,45 @@ +(() => { + class FilteredSearchTokenizer { + static processTokens(input) { + // Regex extracts `(token):(symbol)(value)` + // Values that start with a double quote must end in a double quote (same for single) + const tokenRegex = /(\w+):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\S+))/g; + const tokens = []; + let lastToken = null; + const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { + let tokenValue = v1 || v2 || v3; + let tokenSymbol = symbol; + + if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { + tokenSymbol = tokenValue; + tokenValue = ''; + } + + tokens.push({ + key, + value: tokenValue || '', + symbol: tokenSymbol || '', + }); + return ''; + }).replace(/\s{2,}/g, ' ').trim() || ''; + + if (tokens.length > 0) { + const last = tokens[tokens.length - 1]; + const lastString = `${last.key}:${last.symbol}${last.value}`; + lastToken = input.lastIndexOf(lastString) === + input.length - lastString.length ? last : searchToken; + } else { + lastToken = searchToken; + } + + return { + tokens, + lastToken, + searchToken, + }; + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchTokenizer = FilteredSearchTokenizer; +})(); diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index 46e272c3311..249fe23d4cb 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* 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 824413bf20f..a1b7b442882 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -1,22 +1,36 @@ -/* eslint-disable */ +/* 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() { - if (window.GitLab == null) { - window.GitLab = {}; + if (window.gl == null) { + window.gl = {}; + } + + function sanitize(str) { + return str.replace(/<(?:.|\n)*?>/gm, ''); } - GitLab.GfmAutoComplete = { - dataLoading: false, - dataLoaded: false, + window.gl.GfmAutoComplete = { + dataSources: {}, + defaultLoadingData: ['loading'], cachedData: {}, - dataSource: '', + isLoadingData: {}, + atTypeMap: { + ':': 'emojis', + '@': 'members', + '#': 'issues', + '!': 'mergeRequests', + '~': 'labels', + '%': 'milestones', + '/': 'commands' + }, // Emoji Emoji: { template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>' }, // Team Members Members: { - template: '<li>${username} <small>${title}</small></li>' + template: '<li>${avatarTag} ${username} <small>${title}</small></li>' }, Labels: { template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>' @@ -30,93 +44,102 @@ template: '<li>${title}</li>' }, Loading: { - template: '<li><i class="fa fa-refresh fa-spin"></i> Loading...</li>' + template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>' }, DefaultOptions: { sorter: function(query, items, searchKey) { - if ((items[0].name != null) && items[0].name === 'loading') { + this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0; + if (gl.GfmAutoComplete.isLoading(items)) { + this.setting.highlightFirst = false; return items; } return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); }, filter: function(query, data, searchKey) { - if (data[0] === 'loading') { + if (gl.GfmAutoComplete.isLoading(data)) { + gl.GfmAutoComplete.fetchData(this.$inputor, this.at); return data; + } else { + return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); } - return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); }, beforeInsert: function(value) { - if (!GitLab.GfmAutoComplete.dataLoaded) { - return this.at; + if (value && !this.setting.skipSpecialCharacterTest) { + var withoutAt = value.substring(1); + if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"'; + } + return value; + }, + matcher: function (flag, subtext) { + // The below is taken from At.js source + // Tweaked to commands to start without a space only if char before is a non-word character + // https://github.com/ichord/At.js + var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar; + atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); + atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); + subtext = subtext.split(/\s+/g).pop(); + flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + + _a = decodeURI("%C3%80"); + _y = decodeURI("%C3%BF"); + + regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi'); + + match = regexp.exec(subtext); + + if (match) { + return match[2] || match[1]; } else { - return value; + return null; } } }, - setup: _.debounce(function(input) { + setup: function(input) { // Add GFM auto-completion to all input fields, that accept GFM input. this.input = input || $('.js-gfm-input'); - // destroy previous instances - this.destroyAtWho(); - // set up instances - this.setupAtWho(); - - if (this.dataSource && !this.dataLoading && !this.cachedData) { - this.dataLoading = true; - return this.fetchData(this.dataSource) - .done((data) => { - this.dataLoading = false; - this.loadData(data); - }); - }; - - if (this.cachedData != null) { - return this.loadData(this.cachedData); - } - }, 1000), - setupAtWho: function() { + this.setupLifecycle(); + }, + setupLifecycle() { + this.input.each((i, input) => { + const $input = $(input); + $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); + }); + }, + setupAtWho: function($input) { // Emoji - this.input.atwho({ + $input.atwho({ at: ':', - displayTpl: (function(_this) { - return function(value) { - if (value.path != null) { - return _this.Emoji.template; - } else { - return _this.Loading.template; - } - }; - })(this), + displayTpl: function(value) { + return value.path != null ? this.Emoji.template : this.Loading.template; + }.bind(this), insertTpl: ':${name}:', - data: ['loading'], + skipSpecialCharacterTest: true, + data: this.defaultLoadingData, callbacks: { sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert + beforeInsert: this.DefaultOptions.beforeInsert, + filter: this.DefaultOptions.filter } }); // Team Members - this.input.atwho({ + $input.atwho({ at: '@', - displayTpl: (function(_this) { - return function(value) { - if (value.username != null) { - return _this.Members.template; - } else { - return _this.Loading.template; - } - }; - })(this), + displayTpl: function(value) { + return value.username != null ? this.Members.template : this.Loading.template; + }.bind(this), insertTpl: '${atwho-at}${username}', searchKey: 'search', - data: ['loading'], + alwaysHighlightFirst: true, + skipSpecialCharacterTest: true, + data: this.defaultLoadingData, callbacks: { sorter: this.DefaultOptions.sorter, filter: this.DefaultOptions.filter, beforeInsert: this.DefaultOptions.beforeInsert, + matcher: this.DefaultOptions.matcher, beforeSave: function(members) { return $.map(members, function(m) { - var title; + let title = ''; if (m.username == null) { return m; } @@ -124,34 +147,35 @@ if (m.count) { title += " (" + m.count + ")"; } + + const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase(); + const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`; + const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`; + return { username: m.username, - title: gl.utils.sanitize(title), - search: gl.utils.sanitize(m.username + " " + m.name) + avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar, + title: sanitize(title), + search: sanitize(m.username + " " + m.name) }; }); } } }); - this.input.atwho({ + $input.atwho({ at: '#', alias: 'issues', searchKey: 'search', - displayTpl: (function(_this) { - return function(value) { - if (value.title != null) { - return _this.Issues.template; - } else { - return _this.Loading.template; - } - }; - })(this), - data: ['loading'], + displayTpl: function(value) { + return value.title != null ? this.Issues.template : this.Loading.template; + }.bind(this), + data: this.defaultLoadingData, insertTpl: '${atwho-at}${id}', callbacks: { sorter: this.DefaultOptions.sorter, filter: this.DefaultOptions.filter, beforeInsert: this.DefaultOptions.beforeInsert, + matcher: this.DefaultOptions.matcher, beforeSave: function(issues) { return $.map(issues, function(i) { if (i.title == null) { @@ -159,29 +183,27 @@ } return { id: i.iid, - title: gl.utils.sanitize(i.title), + title: sanitize(i.title), search: i.iid + " " + i.title }; }); } } }); - this.input.atwho({ + $input.atwho({ at: '%', alias: 'milestones', searchKey: 'search', - displayTpl: (function(_this) { - return function(value) { - if (value.title != null) { - return _this.Milestones.template; - } else { - return _this.Loading.template; - } - }; - })(this), - insertTpl: '${atwho-at}"${title}"', - data: ['loading'], + insertTpl: '${atwho-at}${title}', + displayTpl: function(value) { + return value.title != null ? this.Milestones.template : this.Loading.template; + }.bind(this), + data: this.defaultLoadingData, callbacks: { + matcher: this.DefaultOptions.matcher, + sorter: this.DefaultOptions.sorter, + beforeInsert: this.DefaultOptions.beforeInsert, + filter: this.DefaultOptions.filter, beforeSave: function(milestones) { return $.map(milestones, function(m) { if (m.title == null) { @@ -189,32 +211,27 @@ } return { id: m.iid, - title: gl.utils.sanitize(m.title), + title: sanitize(m.title), search: "" + m.title }; }); } } }); - this.input.atwho({ + $input.atwho({ at: '!', alias: 'mergerequests', searchKey: 'search', - displayTpl: (function(_this) { - return function(value) { - if (value.title != null) { - return _this.Issues.template; - } else { - return _this.Loading.template; - } - }; - })(this), - data: ['loading'], + displayTpl: function(value) { + return value.title != null ? this.Issues.template : this.Loading.template; + }.bind(this), + data: this.defaultLoadingData, insertTpl: '${atwho-at}${id}', callbacks: { sorter: this.DefaultOptions.sorter, filter: this.DefaultOptions.filter, beforeInsert: this.DefaultOptions.beforeInsert, + matcher: this.DefaultOptions.matcher, beforeSave: function(merges) { return $.map(merges, function(m) { if (m.title == null) { @@ -222,32 +239,40 @@ } return { id: m.iid, - title: gl.utils.sanitize(m.title), + title: sanitize(m.title), search: m.iid + " " + m.title }; }); } } }); - this.input.atwho({ + $input.atwho({ at: '~', alias: 'labels', searchKey: 'search', - displayTpl: this.Labels.template, + data: this.defaultLoadingData, + displayTpl: function(value) { + return this.isLoading(value) ? this.Loading.template : this.Labels.template; + }.bind(this), insertTpl: '${atwho-at}${title}', callbacks: { + matcher: this.DefaultOptions.matcher, + beforeInsert: this.DefaultOptions.beforeInsert, + filter: this.DefaultOptions.filter, + sorter: this.DefaultOptions.sorter, beforeSave: function(merges) { + if (gl.GfmAutoComplete.isLoading(merges)) return merges; var sanitizeLabelTitle; sanitizeLabelTitle = function(title) { if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) { - return "\"" + (gl.utils.sanitize(title)) + "\""; + return "\"" + (sanitize(title)) + "\""; } else { - return gl.utils.sanitize(title); + return sanitize(title); } }; return $.map(merges, function(m) { return { - title: sanitizeLabelTitle(m.title), + title: sanitize(m.title), color: m.color, search: "" + m.title }; @@ -256,11 +281,14 @@ } }); // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms - this.input.filter('[data-supports-slash-commands="true"]').atwho({ + $input.filter('[data-supports-slash-commands="true"]').atwho({ at: '/', alias: 'commands', searchKey: 'search', + skipSpecialCharacterTest: true, + data: this.defaultLoadingData, displayTpl: function(value) { + if (this.isLoading(value)) return this.Loading.template; var tpl = '<li>/${name}'; if (value.aliases.length > 0) { tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>'; @@ -273,7 +301,7 @@ } tpl += '</li>'; return _.template(tpl)(value); - }, + }.bind(this), insertTpl: function(value) { var tpl = "/${name} "; var reference_prefix = null; @@ -291,6 +319,7 @@ filter: this.DefaultOptions.filter, beforeInsert: this.DefaultOptions.beforeInsert, beforeSave: function(commands) { + if (gl.GfmAutoComplete.isLoading(commands)) return commands; return $.map(commands, function(c) { var search = c.name; if (c.aliases.length > 0) { @@ -306,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]; @@ -318,33 +347,29 @@ }); return; }, - destroyAtWho: function() { - return this.input.atwho('destroy'); - }, - fetchData: function(dataSource) { - return $.getJSON(dataSource); + fetchData: function($input, at) { + if (this.isLoadingData[at]) return; + this.isLoadingData[at] = true; + if (this.cachedData[at]) { + this.loadData($input, at, this.cachedData[at]); + } else { + $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => { + this.loadData($input, at, data); + }).fail(() => { this.isLoadingData[at] = false; }); + } }, - loadData: function(data) { - this.cachedData = data; - this.dataLoaded = true; - // load members - this.input.atwho('load', '@', data.members); - // load issues - this.input.atwho('load', 'issues', data.issues); - // load milestones - this.input.atwho('load', 'milestones', data.milestones); - // load merge requests - this.input.atwho('load', 'mergerequests', data.mergerequests); - // load emojis - this.input.atwho('load', ':', data.emojis); - // load labels - this.input.atwho('load', '~', data.labels); - // load commands - this.input.atwho('load', '/', data.commands); + loadData: function($input, at, data) { + this.isLoadingData[at] = false; + this.cachedData[at] = data; + $input.atwho('load', at, data); // This trigger at.js again // otherwise we would be stuck with loading until the user types - return $(':focus').trigger('keyup'); + return $input.trigger('keyup'); + }, + isLoading(data) { + if (!data || !data.length) return false; + if (Array.isArray(data)) data = data[0]; + return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0]; } }; - }).call(this); diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 98e43c4d088..7746535d9ed 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,8 +1,11 @@ -/* eslint-disable */ +/* 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; @@ -20,7 +23,6 @@ this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; $inputContainer = this.input.parent(); $clearButton = $inputContainer.find('.js-dropdown-input-clear'); - this.indeterminateIds = []; $clearButton.on('click', (function(_this) { // Clear click return function(e) { @@ -35,7 +37,7 @@ .on('keydown', function (e) { var keyCode = e.which; if (keyCode === 13 && !options.elIsInput) { - e.preventDefault() + e.preventDefault(); } }) .on('input', function() { @@ -131,7 +133,6 @@ }; return GitLabDropdownFilter; - })(); GitLabDropdownRemote = (function() { @@ -184,11 +185,10 @@ }; return GitLabDropdownRemote; - })(); GitLabDropdown = (function() { - var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, currentIndex; + var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex; LOADING_CLASS = "is-loading"; @@ -204,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'; @@ -221,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; @@ -249,7 +249,7 @@ _this.fullData = data; _this.parseData(_this.fullData); _this.focusTextInput(); - if (_this.options.filterable && _this.filter && _this.filter.input) { + if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val().trim() !== '') { return _this.filter.input.trigger('input'); } }; @@ -341,16 +341,18 @@ selector = ".dropdown-page-one .dropdown-content a"; } this.dropdown.on("click", selector, function(e) { - var $el, selected; + var $el, selected, selectedObj, isMarking; $el = $(this); selected = self.rowClicked($el); + selectedObj = selected ? selected[0] : null; + isMarking = selected ? selected[1] : null; if (self.options.clicked) { - self.options.clicked(selected, $el, e); + self.options.clicked(selectedObj, $el, e, isMarking); } // Update label right after all modifications in dropdown has been done if (self.options.toggleLabel) { - self.updateLabel(selected, $el, self); + self.updateLabel(selectedObj, $el, self); } $el.trigger('blur'); @@ -441,12 +443,6 @@ this.resetRows(); this.addArrowKeyEvent(); - if (this.options.setIndeterminateIds) { - this.options.setIndeterminateIds.call(this); - } - if (this.options.setActiveIds) { - this.options.setActiveIds.call(this); - } // Makes indeterminate items effective if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { this.parseData(this.fullData); @@ -478,12 +474,7 @@ this.removeArrayKeyEvent(); $input = this.dropdown.find(".dropdown-input-field"); if (this.options.filterable) { - $input.blur().val(""); - } - // Triggering 'keyup' will re-render the dropdown which is not always required - // specially if we want to keep the state of the dropdown needed for bulk-assignment - if (!this.options.persistWhenHide) { - $input.trigger("input"); + $input.blur(); } if (this.dropdown.find(".dropdown-toggle-page").length) { $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); @@ -501,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) { @@ -557,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) { @@ -617,7 +608,8 @@ }; GitLabDropdown.prototype.rowClicked = function(el) { - var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value; + var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking; + fieldName = this.options.fieldName; isInput = $(this.el).is('input'); if (this.renderedData) { @@ -638,7 +630,7 @@ el.addClass(ACTIVE_CLASS); } - return selectedObject; + return [selectedObject]; } field = []; @@ -647,10 +639,16 @@ : 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, '\\\'') + "']"); } + + if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) { + return; + } + if (el.hasClass(ACTIVE_CLASS)) { + isMarking = false; el.removeClass(ACTIVE_CLASS); if (field && field.length) { if (isInput) { @@ -660,6 +658,7 @@ } } } else if (el.hasClass(INDETERMINATE_CLASS)) { + isMarking = true; el.addClass(ACTIVE_CLASS); el.removeClass(INDETERMINATE_CLASS); if (field && field.length && value == null) { @@ -669,6 +668,7 @@ this.addInput(fieldName, value, selectedObject); } } else { + isMarking = true; if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) { this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS); if (!isInput) { @@ -689,12 +689,12 @@ } } - return selectedObject; + return [selectedObject, isMarking]; }; 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; @@ -800,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')); @@ -827,7 +827,6 @@ }; return GitLabDropdown; - })(); $.fn.glDropdown = function(opts) { @@ -837,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 6ce392d2a5b..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 */ +/* 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 ce54c34492d..08b2494f3df 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -1,4 +1,8 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */ +/* global GitLab */ +/* global DropzoneInput */ +/* global autosize */ + (function() { this.GLForm = (function() { function GLForm(form) { @@ -26,13 +30,13 @@ this.form.addClass('gfm-form'); // remove notify commit author checkbox for non-commit notes gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button')); - GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); + gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); new DropzoneInput(this.form); autosize(this.textarea); // form and textarea event listeners this.addEventListeners(); - gl.text.init(this.form); } + gl.text.init(this.form); // hide discard button this.form.find('.js-note-discard').hide(); return this.form.show(); @@ -54,7 +58,5 @@ }; return GLForm; - })(); - }).call(this); diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index e103748d499..32c26349da0 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren */ // This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js index b796a9abb49..2e6da5750de 100644 --- a/app/assets/javascripts/graphs/stat_graph.js +++ b/app/assets/javascripts/graphs/stat_graph.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* 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 818bff0c413..73715286c4a 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -1,4 +1,9 @@ -/* eslint-disable */ +/* 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 */ +/* global ContributorsStatGraphUtil */ +/* global d3 */ /*= require d3 */ @@ -107,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 dea26a3f1e1..cacfc177fc8 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -1,9 +1,11 @@ -/* eslint-disable */ +/* 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; @@ -89,7 +91,6 @@ }; return ContributorsGraph; - })(); this.ContributorsMasterGraph = (function(superClass) { @@ -194,7 +195,6 @@ }; return ContributorsMasterGraph; - })(ContributorsGraph); this.ContributorsAuthorGraph = (function(superClass) { @@ -272,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 362a77e868f..29c3163328f 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js @@ -1,17 +1,18 @@ -/* eslint-disable */ +/* 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) { - var by_author, by_email, data, entry, i, len, total; + var by_author, by_email, data, entry, i, len, total, normalized_email; 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); } - data = by_author[entry.author_name] || by_email[entry.author_email]; + normalized_email = entry.author_email.toLowerCase(); + data = by_author[entry.author_name] || by_email[normalized_email]; if (data == null) { data = this.add_author(entry, by_author, by_email); } @@ -32,12 +33,14 @@ return collection[date].date = date; }, add_author: function(author, by_author, by_email) { - var data; + var data, normalized_email; data = {}; data.author_name = author.author_name; data.author_email = author.author_email; + normalized_email = author.author_email.toLowerCase(); by_author[author.author_name] = data; - return by_email[author.author_email] = data; + by_email[normalized_email] = data; + return data; }, store_data: function(entry, total, by_author) { this.store_commits(total, by_author); @@ -132,5 +135,4 @@ } } }; - }).call(this); diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js index 774477dc7a9..10dfd05fe3c 100644 --- a/app/assets/javascripts/group_avatar.js +++ b/app/assets/javascripts/group_avatar.js @@ -1,13 +1,13 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */ (function() { this.GroupAvatar = (function() { function GroupAvatar() { - $('.js-choose-group-avatar-button').bind("click", function() { + $('.js-choose-group-avatar-button').on("click", function() { var form; form = $(this).closest("form"); return form.find(".js-group-avatar-input").click(); }); - $('.js-group-avatar-input').bind("change", function() { + $('.js-group-avatar-input').on("change", function() { var filename, form; form = $(this).closest("form"); filename = $(this).val().replace(/^.*[\\\/]/, ''); @@ -16,7 +16,5 @@ } return GroupAvatar; - })(); - }).call(this); diff --git a/app/assets/javascripts/group_label_subscription.js.es6 b/app/assets/javascripts/group_label_subscription.js.es6 new file mode 100644 index 00000000000..15e695e81cf --- /dev/null +++ b/app/assets/javascripts/group_label_subscription.js.es6 @@ -0,0 +1,53 @@ +/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */ + +(function(global) { + class GroupLabelSubscription { + constructor(container) { + const $container = $(container); + this.$dropdown = $container.find('.dropdown'); + this.$subscribeButtons = $container.find('.js-subscribe-button'); + this.$unsubscribeButtons = $container.find('.js-unsubscribe-button'); + + this.$subscribeButtons.on('click', this.subscribe.bind(this)); + this.$unsubscribeButtons.on('click', this.unsubscribe.bind(this)); + } + + unsubscribe(event) { + event.preventDefault(); + + const url = this.$unsubscribeButtons.attr('data-url'); + + $.ajax({ + type: 'POST', + url: url + }).done(() => { + this.toggleSubscriptionButtons(); + this.$unsubscribeButtons.removeAttr('data-url'); + }); + } + + subscribe(event) { + event.preventDefault(); + + const $btn = $(event.currentTarget); + const url = $btn.attr('data-url'); + + this.$unsubscribeButtons.attr('data-url', url); + + $.ajax({ + type: 'POST', + url: url + }).done(() => { + this.toggleSubscriptionButtons(); + }); + } + + toggleSubscriptionButtons() { + this.$dropdown.toggleClass('hidden'); + this.$subscribeButtons.toggleClass('hidden'); + this.$unsubscribeButtons.toggleClass('hidden'); + } + } + + global.GroupLabelSubscription = GroupLabelSubscription; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index e3c39c895ba..a50bc4a9057 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* 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() { var slice = [].slice; @@ -14,7 +16,7 @@ multiple: $(select).hasClass('multiselect'), minimumInputLength: 0, query: function(query) { - options = { all_available: all_available, skip_groups: skip_groups }; + var options = { all_available: all_available, skip_groups: skip_groups }; return Api.groups(query.term, options, function(groups) { var data; data = { @@ -65,7 +67,5 @@ }; return GroupsSelect; - })(); - }).call(this); diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index 81fcaf06430..fa85f9a6c86 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -1,10 +1,8 @@ -/* eslint-disable */ +/* 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 c53f7c88aa2..9390136d3d8 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -1,6 +1,7 @@ -/* eslint-disable */ +/* 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() { - this.ImporterStatus = (function() { + window.ImporterStatus = (function() { function ImporterStatus(jobs_url, import_url) { this.jobs_url = jobs_url; this.import_url = import_url; @@ -67,7 +68,6 @@ }; return ImporterStatus; - })(); $(function() { @@ -75,7 +75,7 @@ var jobsImportPath = $('.js-importer-status').data('jobs-import-path'); var importPath = $('.js-importer-status').data('import-path'); - new ImporterStatus(jobsImportPath, importPath); + new window.ImporterStatus(jobsImportPath, importPath); } }); }).call(this); diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 46503c290ae..f63d700fd65 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -1,10 +1,13 @@ -/* eslint-disable */ -(function() { +/* 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 */ + +((global) => { var issuable_created; issuable_created = false; - this.Issuable = { + global.Issuable = { init: function() { Issuable.initTemplates(); Issuable.initSearch(); @@ -31,7 +34,6 @@ e.preventDefault(); debouncedExecSearch(e); }); - }, initSearchState: function($searchInput) { const currentSearchVal = $searchInput.val(); @@ -108,7 +110,11 @@ filterResults: (function(_this) { return function(form) { var formAction, formData, issuesUrl; - formData = form.serialize(); + formData = form.serializeArray(); + formData = formData.filter(function(data) { + return data.value !== ''; + }); + formData = $.param(formData); formAction = form.attr('action'); issuesUrl = formAction; issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); @@ -141,8 +147,11 @@ const $issuesOtherFilters = $('.issues-other-filters'); const $issuesBulkUpdate = $('.issues_bulk_update'); + this.issuableBulkActions.willUpdateLabels = false; + 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); @@ -152,7 +161,6 @@ $updateIssuesIds.val([]); $issuesBulkUpdate.hide(); $issuesOtherFilters.show(); - this.issuableBulkActions.willUpdateLabels = false; } return true; }, @@ -178,5 +186,4 @@ }); } }; - -}).call(this); +})(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 fae49ee6144..9c53cdee58e 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* 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() { this.IssuableContext = (function() { function IssuableContext(currentUser) { @@ -64,7 +66,5 @@ }; return IssuableContext; - })(); - }).call(this); diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 849b45756ee..293b856dc4d 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -1,6 +1,11 @@ -/* eslint-disable */ +/* 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?'; @@ -14,7 +19,7 @@ this.renderWipExplanation = bind(this.renderWipExplanation, this); this.resetAutosave = bind(this.resetAutosave, this); this.handleSubmit = bind(this.handleSubmit, this); - GitLab.GfmAutoComplete.setup(); + gl.GfmAutoComplete.setup(); new UsersSelect(); new ZenMode(); this.titleField = this.form.find("input[name*='[title]']"); @@ -46,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; } @@ -145,7 +150,5 @@ }; return IssuableForm; - })(); - }).call(this); diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 67ace697936..081b0d8b0d7 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,11 +1,12 @@ -/* eslint-disable */ +/* 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 */ /*= require jquery.waitforimages */ /*= 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() { @@ -138,22 +139,17 @@ return; } return $.getJSON($container.data('path')).error(function() { - $container.find('.checking').hide(); $container.find('.unavailable').show(); return new Flash('Failed to check if a new branch can be created.', 'alert'); }).success(function(data) { if (data.can_create_branch) { - $container.find('.checking').hide(); $container.find('.available').show(); } else { - $container.find('.checking').hide(); return $container.find('.unavailable').show(); } }); }; return Issue; - })(); - }).call(this); diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js index d7262e5eb74..1d6eff11403 100644 --- a/app/assets/javascripts/issue_status_select.js +++ b/app/assets/javascripts/issue_status_select.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* 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 9697fb33566..c260ad03d47 100644 --- a/app/assets/javascripts/issues_bulk_assignment.js.es6 +++ b/app/assets/javascripts/issues_bulk_assignment.js.es6 @@ -1,10 +1,13 @@ -/* eslint-disable */ -((global) => { +/* 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 } = {}) { - this.container = container || $('.content'), + constructor({ container, form, issues, prefixId } = {}) { + this.prefixId = prefixId || 'issue_'; this.form = form || this.getElement('.bulk-update'); + this.$labelDropdown = this.form.find('.js-label-select'); this.issues = issues || this.getElement('.issues-list .issue'); this.form.data('bulkActions', this); this.willUpdateLabels = false; @@ -13,10 +16,6 @@ Issuable.initChecks(); } - getElement(selector) { - return this.container.find(selector); - } - bindEvents() { return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); } @@ -70,10 +69,7 @@ getUnmarkedIndeterminedLabels() { const result = []; - const labelsToKeep = []; - - this.getElement('.labels-filter .is-indeterminate') - .each((i, el) => labelsToKeep.push($(el).data('labelId'))); + const labelsToKeep = this.$labelDropdown.data('indeterminate'); this.getLabelsFromSelection().forEach((id) => { if (labelsToKeep.indexOf(id) === -1) { @@ -103,48 +99,67 @@ } }; if (this.willUpdateLabels) { - this.getLabelsToApply().map(function(id) { - return formData.update.add_label_ids.push(id); - }); - this.getLabelsToRemove().map(function(id) { - return formData.update.remove_label_ids.push(id); - }); + formData.update.add_label_ids = this.$labelDropdown.data('marked'); + formData.update.remove_label_ids = this.$labelDropdown.data('unmarked'); } return formData; } - getLabelsToApply() { + setOriginalDropdownData() { + const $labelSelect = $('.bulk-update .js-label-select'); + $labelSelect.data('common', this.getOriginalCommonIds()); + $labelSelect.data('marked', this.getOriginalMarkedIds()); + $labelSelect.data('indeterminate', this.getOriginalIndeterminateIds()); + } + + // From issuable's initial bulk selection + getOriginalCommonIds() { const labelIds = []; - const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]'); - $labels.each(function(k, label) { - if (label) { - return labelIds.push(parseInt($(label).val())); - } + + this.getElement('.selected_issue:checked').each((i, el) => { + labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); }); - return labelIds; + return _.intersection.apply(this, labelIds); } + // From issuable's initial bulk selection + getOriginalMarkedIds() { + const labelIds = []; + this.getElement('.selected_issue:checked').each((i, el) => { + labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); + }); + return _.intersection.apply(this, labelIds); + } - /** - * Returns Label IDs that will be removed from issue selection - * @return {Array} Array of labels IDs - */ - - getLabelsToRemove() { - const result = []; - const indeterminatedLabels = this.getUnmarkedIndeterminedLabels(); - const labelsToApply = this.getLabelsToApply(); - indeterminatedLabels.map(function(id) { - // We need to exclude label IDs that will be applied - // By not doing this will cause issues from selection to not add labels at all - if (labelsToApply.indexOf(id) === -1) { - return result.push(id); - } + // From issuable's initial bulk selection + getOriginalIndeterminateIds() { + const uniqueIds = []; + const labelIds = []; + let issuableLabels = []; + + // Collect unique label IDs for all checked issues + this.getElement('.selected_issue:checked').each((i, el) => { + issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'); + issuableLabels.forEach((labelId) => { + // Store unique IDs + if (uniqueIds.indexOf(labelId) === -1) { + uniqueIds.push(labelId); + } + }); + // Store array of IDs per issuable + labelIds.push(issuableLabels); }); - return result; + // Add uniqueIds to add it as argument for _.intersection + labelIds.unshift(uniqueIds); + // Return IDs that are present but not in all selected issueables + return _.difference(uniqueIds, _.intersection.apply(this, labelIds)); + } + + getElement(selector) { + this.scopeEl = this.scopeEl || $('.content'); + return this.scopeEl.find(selector); } } 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 175623e7448..8f48b1f57ce 100644 --- a/app/assets/javascripts/label_manager.js.es6 +++ b/app/assets/javascripts/label_manager.js.es6 @@ -1,6 +1,7 @@ -/* eslint-disable */ -((global) => { +/* 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'); @@ -102,6 +103,4 @@ } gl.LabelManager = LabelManager; - })(window.gl || (window.gl = {})); - diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js index 3033e8ca5c2..40ad6fc348e 100644 --- a/app/assets/javascripts/labels.js +++ b/app/assets/javascripts/labels.js @@ -1,6 +1,6 @@ -/* eslint-disable */ +/* 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 c334e3e0c02..fd1e229e30a 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -1,12 +1,16 @@ -/* eslint-disable */ +/* 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 */ + (function() { this.LabelsSelect = (function() { function LabelsSelect() { var _this; _this = this; $('.js-label-select').each(function(i, dropdown) { - var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove; + var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer; $dropdown = $(dropdown); + $dropdownContainer = $dropdown.closest('.labels-filter'); $toggleText = $dropdown.find('.dropdown-toggle-text'); namespacePath = $dropdown.data('namespace-path'); projectPath = $dropdown.data('project-path'); @@ -122,7 +126,7 @@ }); }); }; - return $dropdown.glDropdown({ + $dropdown.glDropdown({ showMenuAbove: showMenuAbove, data: function(term, callback) { return $.ajax({ @@ -169,33 +173,40 @@ }); }, renderRow: function(label, instance) { - var $a, $li, active, color, colorEl, indeterminate, removesAll, selectedClass, spacing; + var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue; $li = $('<li>'); $a = $('<a href="#">'); selectedClass = []; removesAll = label.id <= 0 || (label.id == null); if ($dropdown.hasClass('js-filter-bulk-update')) { - indeterminate = instance.indeterminateIds; - active = instance.activeIds; + indeterminate = $dropdown.data('indeterminate') || []; + marked = $dropdown.data('marked') || []; + if (indeterminate.indexOf(label.id) !== -1) { selectedClass.push('is-indeterminate'); } - if (active.indexOf(label.id) !== -1) { + + if (marked.indexOf(label.id) !== -1) { // Remove is-indeterminate class if the item will be marked as active i = selectedClass.indexOf('is-indeterminate'); if (i !== -1) { selectedClass.splice(i, 1); } selectedClass.push('is-active'); - // Add input manually - instance.addInput(this.fieldName, label.id); } - } - if (this.id(label) && $form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + this.id(label).toString().replace(/'/g, '\\\'') + "']").length) { - selectedClass.push('is-active'); - } - if ($dropdown.hasClass('js-multiselect') && removesAll) { - selectedClass.push('dropdown-clear-active'); + } else { + if (this.id(label)) { + dropdownName = $dropdown.data('fieldName'); + dropdownValue = this.id(label).toString().replace(/'/g, '\\\''); + + if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) { + selectedClass.push('is-active'); + } + } + + if ($dropdown.hasClass('js-multiselect') && removesAll) { + selectedClass.push('dropdown-clear-active'); + } } if (label.duplicate) { spacing = 100 / label.color.length; @@ -231,7 +242,6 @@ // Return generated html return $li.html($a).prop('outerHTML'); }, - persistWhenHide: $dropdown.data('persistWhenHide'), search: { fields: ['title'] }, @@ -310,32 +320,28 @@ } } } - if ($dropdown.hasClass('js-filter-bulk-update')) { - // If we are persisting state we need the classes - if (!this.options.persistWhenHide) { - return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass(); - } - } }, multiSelect: $dropdown.hasClass('js-multiselect'), vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: function(label, $el, e) { + clicked: function(label, $el, e, isMarking) { var isIssueIndex, isMRIndex, page; - _this.enableBulkLabelDropdown(); + + page = $('body').data('page'); + isIssueIndex = page === 'projects:issues:index'; + isMRIndex = page === 'projects:merge_requests:index'; 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')) { + _this.enableBulkLabelDropdown(); + _this.setDropdownData($dropdown, isMarking, this.id(label)); return; } - page = $('body').data('page'); - isIssueIndex = page === 'projects:issues:index'; - isMRIndex = page === 'projects:merge_requests:index'; if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { if (label.isAny) { gl.issueBoards.BoardsStore.state.filters['label_name'] = []; @@ -397,17 +403,10 @@ } } }, - setIndeterminateIds: function() { - if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { - return this.indeterminateIds = _this.getIndeterminateIds(); - } - }, - setActiveIds: function() { - if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { - return this.activeIds = _this.getActiveIds(); - } - } }); + + // Set dropdown data + _this.setOriginalDropdownData($dropdownContainer, $dropdown); }); this.bindEvents(); } @@ -420,34 +419,9 @@ if ($('.selected_issue:checked').length) { return; } - // Remove inputs - $('.issues_bulk_update .labels-filter input[type="hidden"]').remove(); - // Also restore button text return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label'); }; - LabelsSelect.prototype.getIndeterminateIds = function() { - var label_ids; - label_ids = []; - $('.selected_issue:checked').each(function(i, el) { - var issue_id; - issue_id = $(el).data('id'); - return label_ids.push($("#issue_" + issue_id).data('labels')); - }); - return _.flatten(label_ids); - }; - - LabelsSelect.prototype.getActiveIds = function() { - var label_ids; - label_ids = []; - $('.selected_issue:checked').each(function(i, el) { - var issue_id; - issue_id = $(el).data('id'); - return label_ids.push($("#issue_" + issue_id).data('labels')); - }); - return _.intersection.apply(_, label_ids); - }; - LabelsSelect.prototype.enableBulkLabelDropdown = function() { var issuableBulkActions; if ($('.selected_issue:checked').length) { @@ -456,8 +430,58 @@ } }; - return LabelsSelect; + LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) { + var i, markedIds, unmarkedIds, indeterminateIds; + var issuableBulkActions = $('.bulk-update').data('bulkActions'); - })(); + markedIds = $dropdown.data('marked') || []; + unmarkedIds = $dropdown.data('unmarked') || []; + indeterminateIds = $dropdown.data('indeterminate') || []; + + if (isMarking) { + markedIds.push(value); + + i = indeterminateIds.indexOf(value); + if (i > -1) { + indeterminateIds.splice(i, 1); + } + + i = unmarkedIds.indexOf(value); + if (i > -1) { + unmarkedIds.splice(i, 1); + } + } else { + // If marked item (not common) is unmarked + i = markedIds.indexOf(value); + if (i > -1) { + markedIds.splice(i, 1); + } + // If an indeterminate item is being unmarked + if (issuableBulkActions.getOriginalIndeterminateIds().indexOf(value) > -1) { + unmarkedIds.push(value); + } + + // If a marked item is being unmarked + // (a marked item could also be a label that is present in all selection) + if (issuableBulkActions.getOriginalCommonIds().indexOf(value) > -1) { + unmarkedIds.push(value); + } + } + + $dropdown.data('marked', markedIds); + $dropdown.data('unmarked', unmarkedIds); + $dropdown.data('indeterminate', indeterminateIds); + }; + + LabelsSelect.prototype.setOriginalDropdownData = function($container, $dropdown) { + var labels = []; + $container.find('[name="label_name[]"]').map(function() { + return labels.push(this.value); + }); + $dropdown.data('marked', labels); + }; + + return LabelsSelect; + })(); }).call(this); diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index 6b4edf02f4d..1c0ea317c1a 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */ + (function() { var hideEndFade; @@ -27,10 +28,10 @@ }); $scrollingTabs.each(function () { - var $this = $(this), - scrollingTabWidth = $this.width(), - $active = $this.find('.active'), - activeWidth = $active.width(); + var $this = $(this); + var scrollingTabWidth = $this.width(); + var $active = $this.find('.active'); + var activeWidth = $active.width(); if ($active.length) { var offset = $active.offset().left + activeWidth; @@ -43,5 +44,4 @@ } }); }); - }).call(this); diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js index b1718e89d3d..4cdf99cae72 100644 --- a/app/assets/javascripts/lib/ace.js +++ b/app/assets/javascripts/lib/ace.js @@ -1,3 +1,2 @@ -/* eslint-disable */ /*= require ace-rails-ap */ /*= require ace/ext-searchbox */ diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js index e1dfdae97de..d8ad5aaeffe 100644 --- a/app/assets/javascripts/lib/chart.js +++ b/app/assets/javascripts/lib/chart.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren */ /*= require Chart */ diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js index 155e30cc462..5221f85ba7a 100644 --- a/app/assets/javascripts/lib/cropper.js +++ b/app/assets/javascripts/lib/cropper.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren */ /*= require cropper */ diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js index 0c9c2787077..57e7986576c 100644 --- a/app/assets/javascripts/lib/d3.js +++ b/app/assets/javascripts/lib/d3.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren */ /*= require d3 */ diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js index cc445db274b..5a9a501efe3 100644 --- a/app/assets/javascripts/lib/raphael.js +++ b/app/assets/javascripts/lib/raphael.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren */ /*= require raphael */ /*= require g.raphael */ diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js index a68edab2aad..ce090a2e4fd 100644 --- a/app/assets/javascripts/lib/utils/animate.js +++ b/app/assets/javascripts/lib/utils/animate.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* 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/bootstrap_linked_tabs.js.es6 b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 new file mode 100644 index 00000000000..e810ee85bd3 --- /dev/null +++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 @@ -0,0 +1,113 @@ +/** + * Linked Tabs + * + * Handles persisting and restores the current tab selection and content. + * Reusable component for static content. + * + * ### Example Markup + * + * <ul class="nav-links tab-links"> + * <li class="active"> + * <a data-action="tab1" data-target="#tab1" data-toggle="tab" href="/path/tab1"> + * Tab 1 + * </a> + * </li> + * <li class="groups-tab"> + * <a data-action="tab2" data-target="#tab2" data-toggle="tab" href="/path/tab2"> + * Tab 2 + * </a> + * </li> + * + * + * <div class="tab-content"> + * <div class="tab-pane" id="tab1"> + * Tab 1 Content + * </div> + * <div class="tab-pane" id="tab2"> + * Tab 2 Content + * </div> + * </div> + * + * + * ### How to use + * + * new window.gl.LinkedTabs({ + * action: "#{controller.action_name}", + * defaultAction: 'tab1', + * parentEl: '.tab-links' + * }); + */ + +(() => { + window.gl = window.gl || {}; + + window.gl.LinkedTabs = class LinkedTabs { + /** + * Binds the events and activates de default tab. + * + * @param {Object} options + */ + constructor(options) { + this.options = options || {}; + + this.defaultAction = this.options.defaultAction; + this.action = this.options.action || this.defaultAction; + + if (this.action === 'show') { + this.action = this.defaultAction; + } + + this.currentLocation = window.location; + + const tabSelector = `${this.options.parentEl} a[data-toggle="tab"]`; + + // since this is a custom event we need jQuery :( + $(document) + .off('shown.bs.tab', tabSelector) + .on('shown.bs.tab', tabSelector, e => this.tabShown(e)); + + this.activateTab(this.action); + } + + /** + * Handles the `shown.bs.tab` event to set the currect url action. + * + * @param {type} evt + * @return {Function} + */ + tabShown(evt) { + const source = evt.target.getAttribute('href'); + + return this.setCurrentAction(source); + } + + /** + * Updates the URL with the path that matched the given action. + * + * @param {String} source + * @return {String} + */ + setCurrentAction(source) { + const copySource = source; + + copySource.replace(/\/+$/, ''); + + const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`; + + history.replaceState({ + turbolinks: true, + url: newState, + }, document.title, newState); + return newState; + } + + /** + * Given the current action activates the correct tab. + * http://getbootstrap.com/javascript/#tab-show + * Note: Will trigger `shown.bs.tab` + */ + activateTab() { + return $(`${this.options.parentEl} a[data-action='${this.action}']`).tab('show'); + } + }; +})(); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js deleted file mode 100644 index 6cb3d95f984..00000000000 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ /dev/null @@ -1,130 +0,0 @@ -/* eslint-disable */ -(function() { - (function(w) { - var base; - w.gl || (w.gl = {}); - (base = w.gl).utils || (base.utils = {}); - w.gl.utils.isInGroupsPage = function() { - return gl.utils.getPagePath() === 'groups'; - }; - w.gl.utils.isInProjectPage = function() { - return gl.utils.getPagePath() === 'projects'; - }; - w.gl.utils.getProjectSlug = function() { - if (this.isInProjectPage()) { - return $('body').data('project'); - } else { - return null; - } - }; - w.gl.utils.getGroupSlug = function() { - if (this.isInGroupsPage()) { - return $('body').data('group'); - } else { - return null; - } - }; - - w.gl.utils.ajaxGet = function(url) { - return $.ajax({ - type: "GET", - url: url, - dataType: "script" - }); - }; - - w.gl.utils.split = function(val) { - return val.split(/,\s*/); - }; - - w.gl.utils.extractLast = function(term) { - return this.split(term).pop(); - }; - - w.gl.utils.rstrip = function rstrip(val) { - if (val) { - return val.replace(/\s+$/, ''); - } else { - return val; - } - }; - - w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) { - event_name = event_name || 'input'; - var closest_submit, field, that; - that = this; - field = $(field_selector); - closest_submit = field.closest('form').find(button_selector); - if (this.rstrip(field.val()) === "") { - closest_submit.disable(); - } - return field.on(event_name, function() { - if (that.rstrip($(this).val()) === "") { - return closest_submit.disable(); - } else { - return closest_submit.enable(); - } - }); - }; - - w.gl.utils.disableButtonIfAnyEmptyField = function(form, form_selector, button_selector) { - var closest_submit, updateButtons; - closest_submit = form.find(button_selector); - updateButtons = function() { - var filled; - filled = true; - form.find('input').filter(form_selector).each(function() { - return filled = this.rstrip($(this).val()) !== "" || !$(this).attr('required'); - }); - if (filled) { - return closest_submit.enable(); - } else { - return closest_submit.disable(); - } - }; - updateButtons(); - return form.keyup(updateButtons); - }; - - w.gl.utils.sanitize = function(str) { - return str.replace(/<(?:.|\n)*?>/gm, ''); - }; - - w.gl.utils.unbindEvents = function() { - return $(document).off('scroll'); - }; - - w.gl.utils.shiftWindow = function() { - return w.scrollBy(0, -100); - }; - - - gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) { - return $tooltipEl.tooltip('destroy').attr('title', newTitle).tooltip('fixTitle'); - }; - gl.utils.preventDisabledButtons = function() { - return $('.btn').click(function(e) { - if ($(this).hasClass('disabled')) { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - }); - }; - gl.utils.getPagePath = function() { - return $('body').data('page').split(':')[0]; - }; - gl.utils.parseUrl = function (url) { - var parser = document.createElement('a'); - parser.href = url; - return parser; - }; - gl.utils.cleanupBeforeFetch = function() { - // Unbind scroll events - $(document).off('scroll'); - // Close any open tooltips - $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy'); - }; - })(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 new file mode 100644 index 00000000000..6d57d31f380 --- /dev/null +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -0,0 +1,163 @@ +/* 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; + w.gl || (w.gl = {}); + (base = w.gl).utils || (base.utils = {}); + w.gl.utils.isInGroupsPage = function() { + return gl.utils.getPagePath() === 'groups'; + }; + w.gl.utils.isInProjectPage = function() { + return gl.utils.getPagePath() === 'projects'; + }; + w.gl.utils.getProjectSlug = function() { + if (this.isInProjectPage()) { + return $('body').data('project'); + } else { + return null; + } + }; + w.gl.utils.getGroupSlug = function() { + if (this.isInGroupsPage()) { + return $('body').data('group'); + } else { + return null; + } + }; + + w.gl.utils.ajaxGet = function(url) { + return $.ajax({ + type: "GET", + url: url, + dataType: "script" + }); + }; + + w.gl.utils.extractLast = function(term) { + return this.split(term).pop(); + }; + + w.gl.utils.rstrip = function rstrip(val) { + if (val) { + return val.replace(/\s+$/, ''); + } else { + return val; + } + }; + + w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) { + event_name = event_name || 'input'; + var closest_submit, field, that; + that = this; + field = $(field_selector); + closest_submit = field.closest('form').find(button_selector); + if (this.rstrip(field.val()) === "") { + closest_submit.disable(); + } + return field.on(event_name, function() { + if (that.rstrip($(this).val()) === "") { + return closest_submit.disable(); + } else { + return closest_submit.enable(); + } + }); + }; + + // automatically adjust scroll position for hash urls taking the height of the navbar into account + // https://github.com/twitter/bootstrap/issues/1768 + w.gl.utils.handleLocationHash = function() { + var hash = w.gl.utils.getLocationHash(); + if (!hash) return; + + var navbar = document.querySelector('.navbar-gitlab'); + var subnav = document.querySelector('.layout-nav'); + var fixedTabs = document.querySelector('.js-tabs-affix'); + + var adjustment = 0; + if (navbar) adjustment -= navbar.offsetHeight; + if (subnav) adjustment -= subnav.offsetHeight; + + // scroll to user-generated markdown anchor if we cannot find a match + if (document.getElementById(hash) === null) { + var target = document.getElementById('user-content-' + hash); + if (target && target.scrollIntoView) { + target.scrollIntoView(true); + window.scrollBy(0, adjustment); + } + } else { + // only adjust for fixedTabs when not targeting user-generated content + if (fixedTabs) { + adjustment -= fixedTabs.offsetHeight; + } + window.scrollBy(0, adjustment); + } + }; + + // Check if element scrolled into viewport from above or below + // Courtesy http://stackoverflow.com/a/7557433/414749 + w.gl.utils.isInViewport = function(el) { + var rect = el.getBoundingClientRect(); + + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= window.innerHeight && + rect.right <= window.innerWidth + ); + }; + + gl.utils.getPagePath = function(index) { + index = index || 0; + return $('body').data('page').split(':')[index]; + }; + + gl.utils.parseUrl = function (url) { + var parser = document.createElement('a'); + parser.href = url; + return parser; + }; + + gl.utils.parseUrlPathname = function (url) { + var parsedUrl = gl.utils.parseUrl(url); + // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11 + // We have to make sure we always have an absolute path. + return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname; + }; + + gl.utils.getUrlParamsArray = function () { + // We can trust that each param has one & since values containing & will be encoded + // Remove the first character of search as it is always ? + return window.location.search.slice(1).split('&'); + }; + + gl.utils.isMetaKey = function(e) { + return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; + }; + + gl.utils.scrollToElement = function($el) { + var top = $el.offset().top; + gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height(); + gl.navLinksHeight = gl.navLinksHeight || $('.nav-links').height(); + gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height(); + + return $('body, html').animate({ + scrollTop: top - (gl.navBarHeight + gl.navLinksHeight + gl.mrTabsHeight) + }, 200); + }; + + /** + this will take in the `name` of the param you want to parse in the url + if the name does not exist this function will return `null` + otherwise it will return the value of the param key provided + */ + w.gl.utils.getParameterByName = (name) => { + const url = window.location.href; + name = name.replace(/[[\]]/g, '\\$&'); + const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); + const results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, ' ')); + }; + })(window); +}).call(this); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 3965109dd65..3ed8bfd5651 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -1,4 +1,10 @@ -/* eslint-disable */ +/* 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 */ + +/*= require timeago */ +/*= require date.format */ + (function() { (function(w) { var base; @@ -23,7 +29,7 @@ setTimeago = true; } - $timeagoEls.each(function() { + $timeagoEls.filter(':not([data-timeago-rendered])').each(function() { var $el = $(this); $el.attr('title', gl.utils.formatDate($el.attr('datetime'))); @@ -33,6 +39,8 @@ template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' }); } + + $el.attr('data-timeago-rendered', true); gl.utils.renderTimeago($el); }); }; @@ -89,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 dafc006d2e5..6d5979603b9 100644 --- a/app/assets/javascripts/lib/utils/notify.js +++ b/app/assets/javascripts/lib/utils/notify.js @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* 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) { var notificationGranted, notifyMe, notifyPermissions; @@ -43,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 new file mode 100644 index 00000000000..ae397212e55 --- /dev/null +++ b/app/assets/javascripts/lib/utils/pretty_time.js.es6 @@ -0,0 +1,65 @@ +(() => { + /* + * TODO: Make these methods more configurable (e.g. parseSeconds timePeriodContstraints, + * stringifyTime condensed or non-condensed, abbreviateTimelengths) + * */ + + 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. + */ + parseSeconds(seconds) { + const DAYS_PER_WEEK = 5; + const HOURS_PER_DAY = 8; + const MINUTES_PER_HOUR = 60; + const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR; + const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR; + + const timePeriodConstraints = { + weeks: MINUTES_PER_WEEK, + days: MINUTES_PER_DAY, + hours: MINUTES_PER_HOUR, + minutes: 1, + }; + + let unorderedMinutes = prettyTime.secondsToMinutes(seconds); + + return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => { + const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); + + unorderedMinutes -= (periodCount * minutesPerPeriod); + + 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. + */ + + 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. + */ + + abbreviateTime(timeStr) { + return timeStr.split(' ') + .filter(unitStr => unitStr.charAt(0) !== '0')[0]; + }, + + secondsToMinutes(seconds) { + return Math.abs(seconds / 60); + }, + }; +})(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 98f9815ff05..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 */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len */ + (function() { (function(w) { var base; @@ -10,13 +11,28 @@ } gl.text.addDelimiter = function(text) { return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text; - } + }; gl.text.randomString = function() { return Math.random().toString(36).substring(7); }; gl.text.replaceRange = function(s, start, end, substitute) { return s.substring(0, start) + substitute + s.substring(end); }; + gl.text.getTextWidth = function(text, font) { + /** + * Uses canvas.measureText to compute and return the width of the given text of given font in pixels. + * + * @param {String} text The text to be rendered. + * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana"). + * + * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 + */ + // re-use canvas object for better performance + var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas')); + var context = canvas.getContext('2d'); + context.font = font; + return context.measureText(text).width; + }; gl.text.selectedText = function(text, textarea) { return text.substring(textarea.selectionStart, textarea.selectionEnd); }; @@ -44,9 +60,25 @@ } }; gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) { - var insertText, inserted, selectedSplit, startChar; + var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine; + removedLastNewLine = false; + removedFirstNewLine = false; + + // Remove the first newline + if (selected.indexOf('\n') === 0) { + removedFirstNewLine = true; + selected = selected.replace(/\n+/, ''); + } + + // Remove the last newline + if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) { + removedLastNewLine = true; + selected = selected.replace(/\n$/, ''); + } + selectedSplit = selected.split('\n'); startChar = !wrap && textArea.selectionStart > 0 ? '\n' : ''; + if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) { if (blockTag != null) { insertText = this.blockTagText(text, textArea, blockTag, selected); @@ -62,6 +94,15 @@ } else { insertText = "" + startChar + tag + selected + (wrap ? tag : ' '); } + + if (removedFirstNewLine) { + insertText = '\n' + insertText; + } + + if (removedLastNewLine) { + insertText += '\n'; + } + if (document.queryCommandSupported('insertText')) { inserted = document.execCommand('insertText', false, insertText); } @@ -74,9 +115,9 @@ document.execCommand("ms-endUndoUnit"); } catch (error) {} } - return this.moveCursor(textArea, tag, wrap); + return this.moveCursor(textArea, tag, wrap, removedLastNewLine); }; - gl.text.moveCursor = function(textArea, tag, wrapped) { + gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) { var pos; if (!textArea.setSelectionRange) { return; @@ -87,6 +128,11 @@ } else { pos = textArea.selectionStart; } + + if (removedLastNewLine) { + pos -= 1; + } + return textArea.setSelectionRange(pos, pos); } }; @@ -112,9 +158,11 @@ gl.text.removeListeners = function(form) { return $('.js-md', form).off(); }; + 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/timeago.js b/app/assets/javascripts/lib/utils/timeago.js deleted file mode 100644 index 42606dd2d46..00000000000 --- a/app/assets/javascripts/lib/utils/timeago.js +++ /dev/null @@ -1,237 +0,0 @@ -/** - * Copyright (c) 2016 hustcc - * License: MIT - * Version: v2.0.2 - * https://github.com/hustcc/timeago.js - * This is a forked from (https://gitlab.com/ClemMakesApps/timeago.js) -**/ -/* eslint-disable */ -/* jshint expr: true */ -!function (root, factory) { - if (typeof module === 'object' && module.exports) - module.exports = factory(root); - else - root.timeago = factory(root); -}(typeof window !== 'undefined' ? window : this, -function () { - var cnt = 0, // the timer counter, for timer key - indexMapEn = 'second_minute_hour_day_week_month_year'.split('_'), - - // build-in locales: en & zh_CN - locales = { - 'en': function(number, index) { - if (index === 0) return ['just now', 'right now']; - var unit = indexMapEn[parseInt(index / 2)]; - if (number > 1) unit += 's'; - return [number + ' ' + unit + ' ago', 'in ' + number + ' ' + unit]; - }, - }, - // second, minute, hour, day, week, month, year(365 days) - SEC_ARRAY = [60, 60, 24, 7, 365/7/12, 12], - SEC_ARRAY_LEN = 6, - ATTR_DATETIME = 'datetime'; - - // format Date / string / timestamp to Date instance. - function toDate(input) { - if (input instanceof Date) return input; - if (!isNaN(input)) return new Date(toInt(input)); - if (/^\d+$/.test(input)) return new Date(toInt(input, 10)); - input = (input || '').trim().replace(/\.\d+/, '') // remove milliseconds - .replace(/-/, '/').replace(/-/, '/') - .replace(/T/, ' ').replace(/Z/, ' UTC') - .replace(/([\+\-]\d\d)\:?(\d\d)/, ' $1$2'); // -04:00 -> -0400 - return new Date(input); - } - // change f into int, remove Decimal. just for code compression - function toInt(f) { - return parseInt(f); - } - // format the diff second to *** time ago, with setting locale - function formatDiff(diff, locale, defaultLocale) { - // if locale is not exist, use defaultLocale. - // if defaultLocale is not exist, use build-in `en`. - // be sure of no error when locale is not exist. - locale = locales[locale] ? locale : (locales[defaultLocale] ? defaultLocale : 'en'); - // if (! locales[locale]) locale = defaultLocale; - var i = 0; - agoin = diff < 0 ? 1 : 0; // timein or timeago - diff = Math.abs(diff); - - for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) { - diff /= SEC_ARRAY[i]; - } - diff = toInt(diff); - i *= 2; - - if (diff > (i === 0 ? 9 : 1)) i += 1; - return locales[locale](diff, i)[agoin].replace('%s', diff); - } - // calculate the diff second between date to be formated an now date. - function diffSec(date, nowDate) { - nowDate = nowDate ? toDate(nowDate) : new Date(); - return (nowDate - toDate(date)) / 1000; - } - /** - * nextInterval: calculate the next interval time. - * - diff: the diff sec between now and date to be formated. - * - * What's the meaning? - * diff = 61 then return 59 - * diff = 3601 (an hour + 1 second), then return 3599 - * make the interval with high performace. - **/ - function nextInterval(diff) { - var rst = 1, i = 0, d = Math.abs(diff); - for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) { - diff /= SEC_ARRAY[i]; - rst *= SEC_ARRAY[i]; - } - // return leftSec(d, rst); - d = d % rst; - d = d ? rst - d : rst; - return Math.ceil(d); - } - // get the datetime attribute, jQuery and DOM - function getDateAttr(node) { - if (node.getAttribute) return node.getAttribute(ATTR_DATETIME); - if(node.attr) return node.attr(ATTR_DATETIME); - } - /** - * timeago: the function to get `timeago` instance. - * - nowDate: the relative date, default is new Date(). - * - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you. - * - * How to use it? - * var timeagoLib = require('timeago.js'); - * var timeago = timeagoLib(); // all use default. - * var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago. - * var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`. - * var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前. - **/ - function Timeago(nowDate, defaultLocale) { - var timers = {}; // real-time render timers - // if do not set the defaultLocale, set it with `en` - if (! defaultLocale) defaultLocale = 'en'; // use default build-in locale - // what the timer will do - function doRender(node, date, locale, cnt) { - var diff = diffSec(date, nowDate); - node.innerHTML = formatDiff(diff, locale, defaultLocale); - // waiting %s seconds, do the next render - timers['k' + cnt] = setTimeout(function() { - doRender(node, date, locale, cnt); - }, nextInterval(diff) * 1000); - } - /** - * nextInterval: calculate the next interval time. - * - diff: the diff sec between now and date to be formated. - * - * What's the meaning? - * diff = 61 then return 59 - * diff = 3601 (an hour + 1 second), then return 3599 - * make the interval with high performace. - **/ - // this.nextInterval = function(diff) { // for dev test - // var rst = 1, i = 0, d = Math.abs(diff); - // for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) { - // diff /= SEC_ARRAY[i]; - // rst *= SEC_ARRAY[i]; - // } - // // return leftSec(d, rst); - // d = d % rst; - // d = d ? rst - d : rst; - // return Math.ceil(d); - // }; // for dev test - /** - * format: format the date to *** time ago, with setting or default locale - * - date: the date / string / timestamp to be formated - * - locale: the formated string's locale name, e.g. en / zh_CN - * - * How to use it? - * var timeago = require('timeago.js')(); - * timeago.format(new Date(), 'pl'); // Date instance - * timeago.format('2016-09-10', 'fr'); // formated date string - * timeago.format(1473473400269); // timestamp with ms - **/ - this.format = function(date, locale) { - return formatDiff(diffSec(date, nowDate), locale, defaultLocale); - }; - /** - * render: render the DOM real-time. - * - nodes: which nodes will be rendered. - * - locale: the locale name used to format date. - * - * How to use it? - * var timeago = new require('timeago.js')(); - * // 1. javascript selector - * timeago.render(document.querySelectorAll('.need_to_be_rendered')); - * // 2. use jQuery selector - * timeago.render($('.need_to_be_rendered'), 'pl'); - * - * Notice: please be sure the dom has attribute `datetime`. - **/ - this.render = function(nodes, locale) { - if (nodes.length === undefined) nodes = [nodes]; - for (var i = 0; i < nodes.length; i++) { - doRender(nodes[i], getDateAttr(nodes[i]), locale, ++ cnt); // render item - } - }; - /** - * cancel: cancel all the timers which are doing real-time render. - * - * How to use it? - * var timeago = new require('timeago.js')(); - * timeago.render(document.querySelectorAll('.need_to_be_rendered')); - * timeago.cancel(); // will stop all the timer, stop render in real time. - **/ - this.cancel = function() { - for (var key in timers) { - clearTimeout(timers[key]); - } - timers = {}; - }; - /** - * setLocale: set the default locale name. - * - * How to use it? - * var timeago = require('timeago.js'); - * timeago = new timeago(); - * timeago.setLocale('fr'); - **/ - this.setLocale = function(locale) { - defaultLocale = locale; - }; - return this; - } - /** - * timeago: the function to get `timeago` instance. - * - nowDate: the relative date, default is new Date(). - * - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you. - * - * How to use it? - * var timeagoLib = require('timeago.js'); - * var timeago = timeagoLib(); // all use default. - * var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago. - * var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`. - * var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前. - **/ - function timeagoFactory(nowDate, defaultLocale) { - return new Timeago(nowDate, defaultLocale); - } - /** - * register: register a new language locale - * - locale: locale name, e.g. en / zh_CN, notice the standard. - * - localeFunc: the locale process function - * - * How to use it? - * var timeagoLib = require('timeago.js'); - * - * timeagoLib.register('the locale name', the_locale_func); - * // or - * timeagoLib.register('pl', require('timeago.js/locales/pl')); - **/ - timeagoFactory.register = function(locale, localeFunc) { - locales[locale] = localeFunc; - }; - - return timeagoFactory; -});
\ No newline at end of file diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js index 4fd1e3fc1d3..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 */ +/* 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 44a66a915e3..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 */ +/* 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 ea5a60bb78e..4620715a521 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* 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 // // Handles single- and multi-line selection and highlight for blob views. @@ -30,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 @@ -118,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]; @@ -143,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; @@ -177,7 +178,5 @@ }; return LineHighlighter; - })(); - }).call(this); diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js index d4f86534f0c..ea9bfb4860a 100644 --- a/app/assets/javascripts/logo.js +++ b/app/assets/javascripts/logo.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */ +/* global Turbolinks */ + (function() { Turbolinks.enableProgressBar(); @@ -9,5 +11,4 @@ $(document).on('page:change', function() { $('.tanuki-logo').removeClass('animate'); }); - }).call(this); diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js.es6 index 0bd90c57396..bf6c0ec2798 100644 --- a/app/assets/javascripts/member_expiration_date.js +++ b/app/assets/javascripts/member_expiration_date.js.es6 @@ -1,30 +1,29 @@ -/* eslint-disable */ -(function() { +(() => { // Add datepickers to all `js-access-expiration-date` elements. If those elements are // children of an element with the `clearable-input` class, and have a sibling // `js-clear-input` element, then show that element when there is a value in the // datepicker, and make clicking on that element clear the field. // - gl.MemberExpirationDate = function() { + window.gl = window.gl || {}; + gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => { function toggleClearInput() { $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== ''); } - - var inputs = $('.js-access-expiration-date'); + const inputs = $(selector); inputs.datepicker({ dateFormat: 'yy-mm-dd', minDate: 1, - onSelect: function () { + onSelect: function onSelect() { $(this).trigger('change'); toggleClearInput.call(this); - } + }, }); - inputs.next('.js-clear-input').on('click', function(event) { + inputs.next('.js-clear-input').on('click', function clicked(event) { event.preventDefault(); - var input = $(this).closest('.clearable-input').find('.js-access-expiration-date'); + const input = $(this).closest('.clearable-input').find(selector); input.datepicker('setDate', null) .trigger('change'); toggleClearInput.call(input); diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js.es6 index 895bc10784f..e3f367a11eb 100644 --- a/app/assets/javascripts/members.js.es6 +++ b/app/assets/javascripts/members.js.es6 @@ -1,38 +1,81 @@ -/* eslint-disable */ -((w) => { - w.gl = w.gl || {}; +/* eslint-disable class-methods-use-this */ +(() => { + window.gl = window.gl || {}; class Members { constructor() { this.addListeners(); + this.initGLDropdown(); } addListeners() { $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow); - $('.js-member-update-control').off('change').on('change', this.formSubmit); - $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess); + $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this)); + $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this)); gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); } + initGLDropdown() { + $('.js-member-permissions-dropdown').each((i, btn) => { + const $btn = $(btn); + + $btn.glDropdown({ + selectable: true, + isSelectable(selected, $el) { + return !$el.hasClass('is-active'); + }, + fieldName: $btn.data('field-name'), + id(selected, $el) { + return $el.data('id'); + }, + toggleLabel(selected, $el) { + return $el.text(); + }, + clicked: (selected, $link) => { + this.formSubmit(null, $link); + }, + }); + }); + } + removeRow(e) { const $target = $(e.target); if ($target.hasClass('btn-remove')) { $target.closest('.member') - .fadeOut(function () { + .fadeOut(function fadeOutMemberRow() { $(this).remove(); }); } } - formSubmit() { - $(this).closest('form').trigger("submit.rails").end().disable(); + formSubmit(e, $el = null) { + const $this = e ? $(e.currentTarget) : $el; + const { $toggle, $dateInput } = this.getMemberListItems($this); + + $this.closest('form').trigger('submit.rails'); + + $toggle.disable(); + $dateInput.disable(); } - formSuccess() { - $(this).find('.js-member-update-control').enable(); + formSuccess(e) { + const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member')); + + $toggle.enable(); + $dateInput.enable(); + } + + getMemberListItems($el) { + const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`); + + return { + $memberListItem, + $toggle: $memberListItem.find('.dropdown-menu-toggle'), + $dateInput: $memberListItem.find('.js-access-expiration-date'), + }; } } gl.Members = Members; -})(window); +})(); 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 6da3942ea52..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,6 +1,9 @@ -/* eslint-disable */ -((global) => { +/* 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({ @@ -15,7 +18,7 @@ loading: false, fileLoaded: false, originalContent: '', - } + }; }, computed: { classObject() { @@ -36,7 +39,7 @@ this.loadEditor(); } }, - ready() { + mounted() { if (this.file.loadEditor) { this.loadEditor(); } @@ -47,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; @@ -90,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 23c4618af70..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,6 +1,7 @@ -/* eslint-disable */ -((global) => { +/* eslint-disable no-param-reassign, comma-dangle */ +/* global Vue */ +((global) => { global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts.inlineConflictLines = Vue.extend({ @@ -9,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_line.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 deleted file mode 100644 index 797850262cc..00000000000 --- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-disable */ -((global) => { - - global.mergeConflicts = global.mergeConflicts || {}; - - global.mergeConflicts.parallelConflictLine = Vue.extend({ - props: { - file: Object, - line: Object - }, - mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions], - template: '#parallel-conflict-line' - }); - -})(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 1b3e9901f1e..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,16 +1,28 @@ -/* eslint-disable */ -((global) => { +/* eslint-disable no-param-reassign, comma-dangle */ +/* global Vue */ +((global) => { global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts.parallelConflictLines = Vue.extend({ props: { file: Object }, - mixins: [global.mergeConflicts.utils], - components: { - 'parallel-conflict-line': gl.mergeConflicts.parallelConflictLine - } + mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions], + template: ` + <table> + <tr class="line_holder parallel" v-for="section in file.parallelLines"> + <template v-for="line in section"> + <td class="diff-line-num header" :class="lineCssClass(line)" v-if="line.isHeader"></td> + <td class="line_content header" :class="lineCssClass(line)" v-if="line.isHeader"> + <strong>{{line.richText}}</strong> + <button class="btn" @click="handleSelected(file, line.id, line.section)">{{line.buttonTitle}}</button> + </td> + <td class="diff-line-num old_line" :class="lineCssClass(line)" v-if="!line.isHeader">{{line.lineNumber}}</td> + <td class="line_content parallel" :class="lineCssClass(line)" v-if="!line.isHeader" v-html="line.richText"></td> + </template> + </tr> + </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 8a7519b0786..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,5 @@ -/* eslint-disable */ +/* eslint-disable no-param-reassign, comma-dangle */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; @@ -24,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 f94e51e783c..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,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, object-shorthand, no-param-reassign, camelcase, no-nested-ternary, no-continue, max-len */ +/* global Cookies */ +/* global Vue */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; @@ -37,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), }; }, @@ -86,7 +88,7 @@ this.decorateLineForInlineView(line, id, conflict); file.inlineLines.push(line); - }) + }); if (conflict) { file.inlineLines.push(this.getOriginHeaderLine(id)); @@ -118,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)); } }); @@ -126,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] @@ -159,11 +161,11 @@ if (file.type === CONFLICT_TYPES.TEXT) { file.sections.forEach((section) => { if (section.conflict) { - count++; + count += 1; } }); } else { - count++; + count += 1; } }); @@ -249,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: '' }); } } @@ -313,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; } } @@ -363,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; @@ -432,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 222a5dcfc2e..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,7 @@ -/* eslint-disable */ +/* eslint-disable new-cap, comma-dangle, no-new */ +/* global Vue */ +/* global Flash */ + //= require vue //= require ./merge_conflict_store //= require ./merge_conflict_service @@ -6,7 +9,6 @@ //= require ./mixins/line_conflict_actions //= require ./components/diff_file_editor //= require ./components/inline_conflict_lines -//= require ./components/parallel_conflict_line //= require ./components/parallel_conflict_lines $(() => { @@ -27,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 @@ -49,7 +51,7 @@ $(() => { mergeConflictsStore.setLoadingState(false); this.$nextTick(() => { - $(conflictsEl.querySelectorAll('.js-syntax-highlight')).syntaxHighlight(); + $('.js-syntax-highlight').syntaxHighlight(); }); }); }, @@ -86,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 c8de586aa21..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,5 @@ -/* eslint-disable */ +/* eslint-disable no-param-reassign, comma-dangle */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; @@ -9,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 88c3a20ce13..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,5 @@ -/* eslint-disable */ +/* eslint-disable no-param-reassign, quote-props, comma-dangle */ + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; @@ -15,5 +16,4 @@ } } }; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index d3bd1e846c1..09ee8dbe9d7 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,11 +1,12 @@ -/* eslint-disable */ +/* 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 */ /*= require task_list */ /*= 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) { @@ -26,6 +27,7 @@ // Prevent duplicate event bindings this.disableTaskList(); this.initMRBtnListeners(); + this.initCommitMessageListeners(); if ($("a.btn-close").length) { this.initTaskList(); } @@ -40,7 +42,7 @@ if (window.mrTabs) { window.mrTabs.unbindEvents(); } - window.mrTabs = new MergeRequestTabs(this.opts); + window.mrTabs = new gl.MergeRequestTabs(this.opts); }; MergeRequest.prototype.showAllCommits = function() { @@ -107,8 +109,26 @@ // note so that we can re-use its form here }; - return MergeRequest; + MergeRequest.prototype.initCommitMessageListeners = function() { + var textarea = $('textarea.js-commit-message'); - })(); + $('a.js-with-description-link').on('click', function(e) { + e.preventDefault(); + + textarea.val(textarea.data('messageWithDescription')); + $('p.js-with-description-hint').hide(); + $('p.js-without-description-hint').show(); + }); + + $('a.js-without-description-link').on('click', function(e) { + e.preventDefault(); + textarea.val(textarea.data('messageWithoutDescription')); + $('p.js-with-description-hint').show(); + $('p.js-without-description-hint').hide(); + }); + }; + + return MergeRequest; + })(); }).call(this); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js deleted file mode 100644 index 860ee5df57e..00000000000 --- a/app/assets/javascripts/merge_request_tabs.js +++ /dev/null @@ -1,441 +0,0 @@ -/* eslint-disable */ -// MergeRequestTabs -// -// Handles persisting and restoring the current tab selection and lazily-loading -// content on the MergeRequests#show page. -// -/*= require js.cookie */ - -// -// ### Example Markup -// -// <ul class="nav-links merge-request-tabs"> -// <li class="notes-tab active"> -// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1"> -// Discussion -// </a> -// </li> -// <li class="commits-tab"> -// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits"> -// Commits -// </a> -// </li> -// <li class="diffs-tab"> -// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs"> -// Diffs -// </a> -// </li> -// </ul> -// -// <div class="tab-content"> -// <div class="notes tab-pane active" id="notes"> -// Notes Content -// </div> -// <div class="commits tab-pane" id="commits"> -// Commits Content -// </div> -// <div class="diffs tab-pane" id="diffs"> -// Diffs Content -// </div> -// </div> -// -// <div class="mr-loading-status"> -// <div class="loading"> -// Loading Animation -// </div> -// </div> -// -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.MergeRequestTabs = (function() { - MergeRequestTabs.prototype.diffsLoaded = false; - - MergeRequestTabs.prototype.buildsLoaded = false; - - MergeRequestTabs.prototype.pipelinesLoaded = false; - - MergeRequestTabs.prototype.commitsLoaded = false; - - MergeRequestTabs.prototype.fixedLayoutPref = null; - - function MergeRequestTabs(opts) { - this.opts = opts != null ? opts : {}; - this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true; - - this.buildsLoaded = this.opts.buildsLoaded || false; - - this.setCurrentAction = bind(this.setCurrentAction, this); - this.tabShown = bind(this.tabShown, this); - this.showTab = bind(this.showTab, this); - // Store the `location` object, allowing for easier stubbing in tests - this._location = location; - this.bindEvents(); - this.activateTab(this.opts.action); - this.initAffix(); - } - - MergeRequestTabs.prototype.bindEvents = function() { - $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); - $(document).on('click', '.js-show-tab', this.showTab); - }; - - MergeRequestTabs.prototype.unbindEvents = function() { - $(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); - $(document).off('click', '.js-show-tab', this.showTab); - }; - - MergeRequestTabs.prototype.showTab = function(event) { - event.preventDefault(); - return this.activateTab($(event.target).data('action')); - }; - - MergeRequestTabs.prototype.tabShown = function(event) { - var $target, action, navBarHeight; - $target = $(event.target); - action = $target.data('action'); - if (action === 'commits') { - this.loadCommits($target.attr('href')); - this.expandView(); - this.resetViewContainer(); - } else if (this.isDiffAction(action)) { - this.loadDiff($target.attr('href')); - if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { - this.shrinkView(); - } - if (this.diffViewType() === 'parallel') { - this.expandViewContainer(); - } - navBarHeight = $('.navbar-gitlab').outerHeight(); - $.scrollTo(".merge-request-details .merge-request-tabs", { - offset: -navBarHeight - }); - } else if (action === 'builds') { - this.loadBuilds($target.attr('href')); - this.expandView(); - this.resetViewContainer(); - } else if (action === 'pipelines') { - this.loadPipelines($target.attr('href')); - this.expandView(); - this.resetViewContainer(); - } else { - this.expandView(); - this.resetViewContainer(); - } - if (this.opts.setUrl) { - this.setCurrentAction(action); - } - }; - - MergeRequestTabs.prototype.scrollToElement = function(container) { - var $el, navBarHeight; - if (window.location.hash) { - navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); - $el = $(container + " " + window.location.hash + ":not(.match)"); - if ($el.length) { - return $.scrollTo(container + " " + window.location.hash + ":not(.match)", { - offset: -navBarHeight - }); - } - } - }; - - // Activate a tab based on the current action - MergeRequestTabs.prototype.activateTab = function(action) { - if (action === 'show') { - action = 'notes'; - } - $(".merge-request-tabs a[data-action='" + action + "']").tab('show').trigger('shown.bs.tab'); - }; - - // Replaces the current Merge Request-specific action in the URL with a new one - // - // If the action is "notes", the URL is reset to the standard - // `MergeRequests#show` route. - // - // Examples: - // - // location.pathname # => "/namespace/project/merge_requests/1" - // setCurrentAction('diffs') - // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // - // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // setCurrentAction('notes') - // location.pathname # => "/namespace/project/merge_requests/1" - // - // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // setCurrentAction('commits') - // location.pathname # => "/namespace/project/merge_requests/1/commits" - // - // Returns the new URL String - MergeRequestTabs.prototype.setCurrentAction = function(action) { - var new_state; - // Normalize action, just to be safe - if (action === 'show') { - action = 'notes'; - } - this.currentAction = action; - // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs' - new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); - - // Append the new action if we're on a tab other than 'notes' - if (action !== 'notes') { - new_state += "/" + action; - } - // Ensure parameters and hash come along for the ride - new_state += this._location.search + this._location.hash; - history.replaceState({ - turbolinks: true, - url: new_state - // Replace the current history state with the new one without breaking - // Turbolinks' history. - // - // See https://github.com/rails/turbolinks/issues/363 - }, document.title, new_state); - return new_state; - }; - - MergeRequestTabs.prototype.loadCommits = function(source) { - if (this.commitsLoaded) { - return; - } - return this._get({ - url: source + ".json", - success: (function(_this) { - return function(data) { - document.querySelector("div#commits").innerHTML = data.html; - gl.utils.localTimeAgo($('.js-timeago', 'div#commits')); - _this.commitsLoaded = true; - return _this.scrollToElement("#commits"); - }; - })(this) - }); - }; - - MergeRequestTabs.prototype.loadDiff = function(source) { - if (this.diffsLoaded) { - return; - } - - // We extract pathname for the current Changes tab anchor href - // some pages like MergeRequestsController#new has query parameters on that anchor - var url = gl.utils.parseUrl(source); - - return this._get({ - url: (url.pathname + ".json") + this._location.search, - success: (function(_this) { - return function(data) { - $('#diffs').html(data.html); - - if (typeof DiffNotesApp !== 'undefined') { - DiffNotesApp.compileComponents(); - } - - gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); - $('#diffs .js-syntax-highlight').syntaxHighlight(); - $('#diffs .diff-file').singleFileDiff(); - if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) { - _this.expandViewContainer(); - } - _this.diffsLoaded = true; - var anchoredDiff = gl.utils.getLocationHash(); - if (anchoredDiff) _this.openAnchoredDiff(anchoredDiff, function() { - _this.scrollToElement("#diffs"); - _this.highlighSelectedLine(); - }); - _this.filesCommentButton = $('.files .diff-file').filesCommentButton(); - return $(document).off('click', '.diff-line-num a').on('click', '.diff-line-num a', function(e) { - e.preventDefault(); - window.location.hash = $(e.currentTarget).attr('href'); - _this.highlighSelectedLine(); - return _this.scrollToElement("#diffs"); - }); - }; - })(this) - }); - }; - - MergeRequestTabs.prototype.openAnchoredDiff = function(anchoredDiff, cb) { - var diffTitle = $('#file-path-' + anchoredDiff); - var diffFile = diffTitle.closest('.diff-file'); - var nothingHereBlock = $('.nothing-here-block:visible', diffFile); - if (nothingHereBlock.length) { - diffFile.singleFileDiff(true, cb); - } else { - cb(); - } - }; - - MergeRequestTabs.prototype.highlighSelectedLine = function() { - var $diffLine, diffLineTop, hashClassString, locationHash, navBarHeight; - $('.hll').removeClass('hll'); - locationHash = window.location.hash; - if (locationHash !== '') { - dataLineString = '[data-line-code="' + locationHash.replace('#', '') + '"]'; - $diffLine = $(locationHash + ":not(.match)", $('#diffs')); - if (!$diffLine.is('tr')) { - $diffLine = $('#diffs').find("td" + locationHash + ", td" + dataLineString); - } else { - $diffLine = $diffLine.find('td'); - } - if ($diffLine.length) { - $diffLine.addClass('hll'); - diffLineTop = $diffLine.offset().top; - return navBarHeight = $('.navbar-gitlab').outerHeight(); - } - } - }; - - MergeRequestTabs.prototype.loadBuilds = function(source) { - if (this.buildsLoaded) { - return; - } - return this._get({ - url: source + ".json", - success: (function(_this) { - return function(data) { - document.querySelector("div#builds").innerHTML = data.html; - gl.utils.localTimeAgo($('.js-timeago', 'div#builds')); - _this.buildsLoaded = true; - if (!this.pipelines) this.pipelines = new gl.Pipelines(); - return _this.scrollToElement("#builds"); - }; - })(this) - }); - }; - - MergeRequestTabs.prototype.loadPipelines = function(source) { - if (this.pipelinesLoaded) { - return; - } - return this._get({ - url: source + ".json", - success: function(data) { - $('#pipelines').html(data.html); - gl.utils.localTimeAgo($('.js-timeago', '#pipelines')); - this.pipelinesLoaded = true; - return this.scrollToElement("#pipelines"); - }.bind(this) - }); - }; - - // Show or hide the loading spinner - // - // status - Boolean, true to show, false to hide - MergeRequestTabs.prototype.toggleLoading = function(status) { - return $('.mr-loading-status .loading').toggle(status); - }; - - MergeRequestTabs.prototype._get = function(options) { - var defaults; - defaults = { - beforeSend: (function(_this) { - return function() { - return _this.toggleLoading(true); - }; - })(this), - complete: (function(_this) { - return function() { - return _this.toggleLoading(false); - }; - })(this), - dataType: 'json', - type: 'GET' - }; - options = $.extend({}, defaults, options); - return $.ajax(options); - }; - - MergeRequestTabs.prototype.diffViewType = function() { - return $('.inline-parallel-buttons a.active').data('view-type'); - }; - - MergeRequestTabs.prototype.isDiffAction = function(action) { - return action === 'diffs' || action === 'new/diffs' - }; - - MergeRequestTabs.prototype.expandViewContainer = function() { - var $wrapper = $('.content-wrapper .container-fluid'); - if (this.fixedLayoutPref === null) { - this.fixedLayoutPref = $wrapper.hasClass('container-limited'); - } - $wrapper.removeClass('container-limited'); - }; - - MergeRequestTabs.prototype.resetViewContainer = function() { - if (this.fixedLayoutPref !== null) { - $('.content-wrapper .container-fluid') - .toggleClass('container-limited', this.fixedLayoutPref); - } - }; - - MergeRequestTabs.prototype.shrinkView = function() { - var $gutterIcon; - $gutterIcon = $('.js-sidebar-toggle i:visible'); - return setTimeout(function() { - if ($gutterIcon.is('.fa-angle-double-right')) { - return $gutterIcon.closest('a').trigger('click', [true]); - } - // Wait until listeners are set - // Only when sidebar is expanded - }, 0); - }; - - MergeRequestTabs.prototype.expandView = function() { - var $gutterIcon; - if (Cookies.get('collapsed_gutter') === 'true') { - return; - } - $gutterIcon = $('.js-sidebar-toggle i:visible'); - return setTimeout(function() { - if ($gutterIcon.is('.fa-angle-double-left')) { - return $gutterIcon.closest('a').trigger('click', [true]); - } - }, 0); - // Expand the issuable sidebar unless the user explicitly collapsed it - // Wait until listeners are set - // Only when sidebar is collapsed - }; - - MergeRequestTabs.prototype.initAffix = function () { - var $tabs = $('.js-tabs-affix'); - - // Screen space on small screens is usually very sparse - // So we dont affix the tabs on these - if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; - - var $diffTabs = $('#diff-notes-app'), - $fixedNav = $('.navbar-fixed-top'), - $layoutNav = $('.layout-nav'); - - $tabs.off('affix.bs.affix affix-top.bs.affix') - .affix({ - offset: { - top: function () { - var tabsTop = $diffTabs.offset().top - $tabs.height(); - tabsTop = tabsTop - ($fixedNav.height() + $layoutNav.height()); - - return tabsTop; - } - } - }).on('affix.bs.affix', function () { - $diffTabs.css({ - marginTop: $tabs.height() - }); - }).on('affix-top.bs.affix', function () { - $diffTabs.css({ - marginTop: '' - }); - }); - - // Fix bug when reloading the page already scrolling - if ($tabs.hasClass('affix')) { - $tabs.trigger('affix.bs.affix'); - } - }; - - return MergeRequestTabs; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 new file mode 100644 index 00000000000..4c8c28af755 --- /dev/null +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -0,0 +1,365 @@ +/* eslint-disable no-new, class-methods-use-this */ +/* global Breakpoints */ +/* global Cookies */ +/* global DiffNotesApp */ +/* global Flash */ + +/*= require js.cookie */ +/*= require breakpoints */ + +/* eslint-disable max-len */ +// MergeRequestTabs +// +// Handles persisting and restoring the current tab selection and lazily-loading +// content on the MergeRequests#show page. +// +// ### Example Markup +// +// <ul class="nav-links merge-request-tabs"> +// <li class="notes-tab active"> +// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1"> +// Discussion +// </a> +// </li> +// <li class="commits-tab"> +// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits"> +// Commits +// </a> +// </li> +// <li class="diffs-tab"> +// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs"> +// Diffs +// </a> +// </li> +// </ul> +// +// <div class="tab-content"> +// <div class="notes tab-pane active" id="notes"> +// Notes Content +// </div> +// <div class="commits tab-pane" id="commits"> +// Commits Content +// </div> +// <div class="diffs tab-pane" id="diffs"> +// Diffs Content +// </div> +// </div> +// +// <div class="mr-loading-status"> +// <div class="loading"> +// Loading Animation +// </div> +// </div> +// +/* eslint-enable max-len */ + +(() => { + // Store the `location` object, allowing for easier stubbing in tests + let location = window.location; + + class MergeRequestTabs { + + constructor({ action, setUrl, stubLocation } = {}) { + this.diffsLoaded = false; + this.pipelinesLoaded = false; + this.commitsLoaded = false; + this.fixedLayoutPref = null; + + this.setUrl = setUrl !== undefined ? setUrl : true; + this.setCurrentAction = this.setCurrentAction.bind(this); + this.tabShown = this.tabShown.bind(this); + this.showTab = this.showTab.bind(this); + + if (stubLocation) { + location = stubLocation; + } + + this.bindEvents(); + this.activateTab(action); + this.initAffix(); + } + + bindEvents() { + $(document) + .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) + .on('click', '.js-show-tab', this.showTab); + } + + unbindEvents() { + $(document) + .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) + .off('click', '.js-show-tab', this.showTab); + } + + showTab(e) { + e.preventDefault(); + this.activateTab($(e.target).data('action')); + } + + tabShown(e) { + const $target = $(e.target); + const action = $target.data('action'); + + if (action === 'commits') { + this.loadCommits($target.attr('href')); + this.expandView(); + this.resetViewContainer(); + } else if (this.isDiffAction(action)) { + this.loadDiff($target.attr('href')); + if (Breakpoints.get().getBreakpointSize() !== 'lg') { + this.shrinkView(); + } + if (this.diffViewType() === 'parallel') { + this.expandViewContainer(); + } + const navBarHeight = $('.navbar-gitlab').outerHeight(); + $.scrollTo('.merge-request-details .merge-request-tabs', { + offset: -navBarHeight, + }); + } else if (action === 'pipelines') { + this.loadPipelines($target.attr('href')); + this.expandView(); + this.resetViewContainer(); + } else { + this.expandView(); + this.resetViewContainer(); + } + if (this.setUrl) { + this.setCurrentAction(action); + } + } + + scrollToElement(container) { + if (location.hash) { + const offset = 0 - ( + $('.navbar-gitlab').outerHeight() + + $('.layout-nav').outerHeight() + + $('.js-tabs-affix').outerHeight() + ); + const $el = $(`${container} ${location.hash}:not(.match)`); + if ($el.length) { + $.scrollTo($el[0], { offset }); + } + } + } + + // Activate a tab based on the current action + activateTab(action) { + const activate = action === 'show' ? 'notes' : action; + // important note: the .tab('show') method triggers 'shown.bs.tab' event itself + $(`.merge-request-tabs a[data-action='${activate}']`).tab('show'); + } + + // Replaces the current Merge Request-specific action in the URL with a new one + // + // If the action is "notes", the URL is reset to the standard + // `MergeRequests#show` route. + // + // Examples: + // + // location.pathname # => "/namespace/project/merge_requests/1" + // setCurrentAction('diffs') + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // setCurrentAction('notes') + // location.pathname # => "/namespace/project/merge_requests/1" + // + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // setCurrentAction('commits') + // location.pathname # => "/namespace/project/merge_requests/1/commits" + // + // Returns the new URL String + setCurrentAction(action) { + this.currentAction = action === 'show' ? 'notes' : action; + + // Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs' + let newState = location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); + + // Append the new action if we're on a tab other than 'notes' + if (this.currentAction !== 'notes') { + newState += `/${this.currentAction}`; + } + + // Ensure parameters and hash come along for the ride + newState += location.search + location.hash; + + // Replace the current history state with the new one without breaking + // Turbolinks' history. + // + // See https://github.com/rails/turbolinks/issues/363 + window.history.replaceState({ + turbolinks: true, + url: newState, + }, document.title, newState); + + return newState; + } + + loadCommits(source) { + if (this.commitsLoaded) { + return; + } + this.ajaxGet({ + url: `${source}.json`, + success: (data) => { + document.querySelector('div#commits').innerHTML = data.html; + gl.utils.localTimeAgo($('.js-timeago', 'div#commits')); + this.commitsLoaded = true; + this.scrollToElement('#commits'); + }, + }); + } + + loadDiff(source) { + if (this.diffsLoaded) { + return; + } + + // We extract pathname for the current Changes tab anchor href + // some pages like MergeRequestsController#new has query parameters on that anchor + const urlPathname = gl.utils.parseUrlPathname(source); + + this.ajaxGet({ + url: `${urlPathname}.json${location.search}`, + success: (data) => { + $('#diffs').html(data.html); + + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + gl.diffNotesCompileComponents(); + } + + gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); + $('#diffs .js-syntax-highlight').syntaxHighlight(); + + if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) { + this.expandViewContainer(); + } + this.diffsLoaded = true; + + new gl.Diff(); + this.scrollToElement('#diffs'); + }, + }); + } + + loadPipelines(source) { + if (this.pipelinesLoaded) { + return; + } + this.ajaxGet({ + url: `${source}.json`, + success: (data) => { + $('#pipelines').html(data.html); + gl.utils.localTimeAgo($('.js-timeago', '#pipelines')); + this.pipelinesLoaded = true; + this.scrollToElement('#pipelines'); + + new gl.MiniPipelineGraph({ + container: '.js-pipeline-table', + }); + }, + }); + } + + // Show or hide the loading spinner + // + // status - Boolean, true to show, false to hide + toggleLoading(status) { + $('.mr-loading-status .loading').toggle(status); + } + + ajaxGet(options) { + const defaults = { + beforeSend: () => this.toggleLoading(true), + error: () => new Flash('An error occurred while fetching this tab.', 'alert'), + complete: () => this.toggleLoading(false), + dataType: 'json', + type: 'GET', + }; + $.ajax($.extend({}, defaults, options)); + } + + diffViewType() { + return $('.inline-parallel-buttons a.active').data('view-type'); + } + + isDiffAction(action) { + return action === 'diffs' || action === 'new/diffs'; + } + + expandViewContainer() { + const $wrapper = $('.content-wrapper .container-fluid'); + if (this.fixedLayoutPref === null) { + this.fixedLayoutPref = $wrapper.hasClass('container-limited'); + } + $wrapper.removeClass('container-limited'); + } + + resetViewContainer() { + if (this.fixedLayoutPref !== null) { + $('.content-wrapper .container-fluid') + .toggleClass('container-limited', this.fixedLayoutPref); + } + } + + shrinkView() { + const $gutterIcon = $('.js-sidebar-toggle i:visible'); + + // Wait until listeners are set + setTimeout(() => { + // Only when sidebar is expanded + if ($gutterIcon.is('.fa-angle-double-right')) { + $gutterIcon.closest('a').trigger('click', [true]); + } + }, 0); + } + + // Expand the issuable sidebar unless the user explicitly collapsed it + expandView() { + if (Cookies.get('collapsed_gutter') === 'true') { + return; + } + const $gutterIcon = $('.js-sidebar-toggle i:visible'); + + // Wait until listeners are set + setTimeout(() => { + // Only when sidebar is collapsed + if ($gutterIcon.is('.fa-angle-double-left')) { + $gutterIcon.closest('a').trigger('click', [true]); + } + }, 0); + } + + initAffix() { + const $tabs = $('.js-tabs-affix'); + + // Screen space on small screens is usually very sparse + // So we dont affix the tabs on these + if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; + + const $diffTabs = $('#diff-notes-app'); + const $fixedNav = $('.navbar-fixed-top'); + const $layoutNav = $('.layout-nav'); + + $tabs.off('affix.bs.affix affix-top.bs.affix') + .affix({ + offset: { + top: () => ( + $diffTabs.offset().top - $tabs.height() - $fixedNav.height() - $layoutNav.height() + ), + }, + }) + .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() })) + .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' })); + + // Fix bug when reloading the page already scrolling + if ($tabs.hasClass('affix')) { + $tabs.trigger('affix.bs.affix'); + } + } + } + + window.gl = window.gl || {}; + window.gl.MergeRequestTabs = MergeRequestTabs; +})(); diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 56c87af3226..7cc319e2f4e 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -1,6 +1,11 @@ -/* eslint-disable */ - ((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; }; +/* 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 += 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"> @@ -27,7 +32,7 @@ </div> </div>`; - global.MergeRequestWidget = (function() { + global.MergeRequestWidget = (function() { function MergeRequestWidget(opts) { // Initialize MergeRequestWidget behavior // @@ -40,19 +45,26 @@ $('#modal_merge_info').modal({ show: false }); - this.firstCICheck = true; - this.readyForCICheck = false; - this.readyForCIEnvironmentCheck = false; - this.cancel = false; - clearInterval(this.fetchBuildStatusInterval); - clearInterval(this.fetchBuildEnvironmentStatusInterval); this.clearEventListeners(); this.addEventListeners(); this.getCIStatus(false); - this.getCIEnvironmentsStatus(); this.retrieveSuccessIcon(); - this.pollCIStatus(); - this.pollCIEnvironmentsStatus(); + + this.ciStatusInterval = new global.SmartInterval({ + callback: this.getCIStatus.bind(this, true), + startingInterval: 10000, + maxInterval: 30000, + hiddenInterval: 120000, + incrementByFactorOf: 5000, + }); + this.ciEnvironmentStatusInterval = new global.SmartInterval({ + callback: this.getCIEnvironmentsStatus.bind(this), + startingInterval: 30000, + maxInterval: 120000, + hiddenInterval: 240000, + incrementByFactorOf: 15000, + immediateExecution: true, + }); notifyPermissions(); } @@ -60,21 +72,14 @@ return $(document).off('page:change.merge_request'); }; - MergeRequestWidget.prototype.cancelPolling = function() { - return this.cancel = true; - }; - MergeRequestWidget.prototype.addEventListeners = function() { var allowedPages; - allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes']; - return $(document).on('page:change.merge_request', (function(_this) { + allowedPages = ['show', 'commits', 'pipelines', 'changes']; + $(document).on('page:change.merge_request', (function(_this) { return function() { var page; page = $('body').data('page').split(':').last(); if (allowedPages.indexOf(page) < 0) { - clearInterval(_this.fetchBuildStatusInterval); - clearInterval(_this.fetchBuildEnvironmentStatusInterval); - _this.cancelPolling(); return _this.clearEventListeners(); } }; @@ -82,10 +87,10 @@ }; MergeRequestWidget.prototype.retrieveSuccessIcon = function() { - const $ciSuccessIcon = $('.js-success-icon'); - this.$ciSuccessIcon = $ciSuccessIcon.html(); - $ciSuccessIcon.remove(); - } + const $ciSuccessIcon = $('.js-success-icon'); + this.$ciSuccessIcon = $ciSuccessIcon.html(); + $ciSuccessIcon.remove(); + }; MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) { if (deleteSourceBranch == null) { @@ -101,7 +106,7 @@ urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : ''; return window.location.href = window.location.pathname + urlSuffix; } else if (data.merge_error) { - return this.$widgetBody.html("<h4>" + data.merge_error + "</h4>"); + return _this.$widgetBody.html("<h4>" + data.merge_error + "</h4>"); } else { callback = function() { return merge_request_widget.mergeInProgress(deleteSourceBranch); @@ -114,9 +119,16 @@ }); }; + MergeRequestWidget.prototype.cancelPolling = function () { + this.ciStatusInterval.cancel(); + this.ciEnvironmentStatusInterval.cancel(); + }; + MergeRequestWidget.prototype.getMergeStatus = function() { return $.get(this.opts.merge_check_url, function(data) { - return $('.mr-state-widget').replaceWith(data); + var $html = $(data); + $('.mr-widget-body').replaceWith($html.find('.mr-widget-body')); + $('.mr-widget-footer').replaceWith($html.find('.mr-widget-footer')); }); }; @@ -131,18 +143,6 @@ } }; - MergeRequestWidget.prototype.pollCIStatus = function() { - return this.fetchBuildStatusInterval = setInterval(((function(_this) { - return function() { - if (!_this.readyForCICheck) { - return; - } - _this.getCIStatus(true); - return _this.readyForCICheck = false; - }; - })(this)), 10000); - }; - MergeRequestWidget.prototype.getCIStatus = function(showNotification) { var _this; _this = this; @@ -150,23 +150,17 @@ return $.getJSON(this.opts.ci_status_url, (function(_this) { return function(data) { var message, status, title; - if (_this.cancel) { - return; - } - _this.readyForCICheck = true; if (data.status === '') { return; } if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); - if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) { + if (data.status !== _this.opts.ci_status && (data.status != null)) { _this.opts.ci_status = data.status; _this.showCIStatus(data.status); if (data.coverage) { _this.showCICoverage(data.coverage); } - // The first check should only update the UI, a notification - // should only be displayed on status changes - if (showNotification && !_this.firstCICheck) { + if (showNotification) { status = _this.ciLabelForStatus(data.status); if (status === "preparing") { title = _this.opts.ci_title.preparing; @@ -181,54 +175,42 @@ message = message.replace('{{title}}', data.title); notify(title, message, _this.opts.gitlab_icon, function() { this.close(); - return Turbolinks.visit(_this.opts.builds_path); }); } - return _this.firstCICheck = false; } }; })(this)); }; - MergeRequestWidget.prototype.pollCIEnvironmentsStatus = function() { - this.fetchBuildEnvironmentStatusInterval = setInterval(() => { - if (!this.readyForCIEnvironmentCheck) return; - this.getCIEnvironmentsStatus(); - this.readyForCIEnvironmentCheck = false; - }, 300000); - }; - MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() { $.getJSON(this.opts.ci_environments_status_url, (environments) => { - if (this.cancel) return; - this.readyForCIEnvironmentCheck = true; if (environments && environments.length) this.renderEnvironments(environments); }); }; 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(); - + if (!environment.stop_url) { $('.js-stop-env-link', $template).remove(); } - + if (environment.deployed_at && environment.deployed_at_formatted) { - environment.deployed_at = gl.utils.getTimeago(environment.deployed_at) + '.'; + environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.'; } else { $('.js-environment-timeago', $template).remove(); environment.name += '.'; } 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); } - }; + }; MergeRequestWidget.prototype.showCIStatus = function(state) { var allowed_states; @@ -245,7 +227,7 @@ case "not_found": return this.setMergeButtonClass('btn-danger'); case "running": - return this.setMergeButtonClass('btn-warning'); + return this.setMergeButtonClass('btn-info'); case "success": case "success_with_warnings": return this.setMergeButtonClass('btn-create'); @@ -263,11 +245,9 @@ }; MergeRequestWidget.prototype.setMergeButtonClass = function(css_class) { - return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-warning btn-create').addClass(css_class); + return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-info btn-create').addClass(css_class); }; return MergeRequestWidget; - })(); - - })(window.gl || (window.gl = {})); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 new file mode 100644 index 00000000000..5969d2ba56b --- /dev/null +++ b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 @@ -0,0 +1,53 @@ +/* global merge_request_widget */ + +(() => { + $(() => { + /* TODO: This needs a better home, or should be refactored. It was previously contained + * in a script tag in app/views/projects/merge_requests/widget/open/_accept.html.haml, + * but Vue chokes on script tags and prevents their execution. So it was moved here + * temporarily. + * */ + + $(document) + .off('ajax:send', '.accept-mr-form') + .on('ajax:send', '.accept-mr-form', () => { + $('.accept-mr-form :input').disable(); + }); + + $(document) + .off('click', '.accept_merge_request') + .on('click', '.accept_merge_request', () => { + $('.js-merge-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress'); + }); + + $(document) + .off('click', '.merge_when_build_succeeds') + .on('click', '.merge_when_build_succeeds', () => { + $('#merge_when_build_succeeds').val('1'); + }); + + $(document) + .off('click', '.js-merge-dropdown a') + .on('click', '.js-merge-dropdown a', (e) => { + e.preventDefault(); + $(e.target).closest('form').submit(); + }); + if ($('.rebase-in-progress').length) { + merge_request_widget.rebaseInProgress(); + } else if ($('.rebase-mr-form').length) { + $(document) + .off('ajax:send', '.rebase-mr-form') + .on('ajax:send', '.rebase-mr-form', () => { + $('.rebase-mr-form :input').disable(); + }); + + $(document) + .off('click', '.js-rebase-button') + .on('click', '.js-rebase-button', () => { + $('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress"); + }); + } else { + merge_request_widget.getMergeStatus(); + } + }); +})(); diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js index 7ad86d8c084..527cdc9b698 100644 --- a/app/assets/javascripts/merged_buttons.js +++ b/app/assets/javascripts/merged_buttons.js @@ -1,6 +1,7 @@ -/* eslint-disable */ +/* 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() { @@ -40,7 +41,5 @@ }; return MergedButtons; - })(); - }).call(this); diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 9299c96e8ea..7ce1259e015 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* 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() { this.Milestone = (function() { Milestone.updateIssue = function(li, issue_url, data) { @@ -191,7 +193,5 @@ }; return Milestone; - })(); - }).call(this); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index d1cd38ad110..7ab39ffbd05 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -1,4 +1,8 @@ -/* eslint-disable */ +/* 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 */ + (function() { this.MilestoneSelect = (function() { function MilestoneSelect(currentProject) { @@ -177,7 +181,5 @@ } return MilestoneSelect; - })(); - }).call(this); diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 new file mode 100644 index 00000000000..80549532ea9 --- /dev/null +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 @@ -0,0 +1,97 @@ +/* eslint-disable no-new */ +/* global Flash */ + +/** + * In each pipelines table we have a mini pipeline graph for each pipeline. + * + * When we click in a pipeline stage, we need to make an API call to get the + * builds list to render in a dropdown. + * + * The container should be the table element. + * + * The stage icon clicked needs to have the following HTML structure: + * <div class="dropdown"> + * <button class="dropdown js-builds-dropdown-button" data-toggle="dropdown"></button> + * <div class="js-builds-dropdown-container dropdown-menu"></div> + * </div> + */ +(() => { + class MiniPipelineGraph { + constructor(opts = {}) { + this.container = opts.container || ''; + this.dropdownListSelector = '.js-builds-dropdown-container'; + this.getBuildsList = this.getBuildsList.bind(this); + + this.bindEvents(); + } + + /** + * Adds the event listener when the dropdown is opened. + * All dropdown events are fired at the .dropdown-menu's parent element. + */ + bindEvents() { + $(this.container).on('shown.bs.dropdown', this.getBuildsList); + } + + /** + * For the clicked stage, renders the given data in the dropdown list. + * + * @param {HTMLElement} stageContainer + * @param {Object} data + */ + renderBuildsList(stageContainer, data) { + const dropdownContainer = stageContainer.parentElement.querySelector( + `${this.dropdownListSelector} .js-builds-dropdown-list`, + ); + + dropdownContainer.innerHTML = data; + } + + /** + * For the clicked stage, gets the list of builds. + * + * All dropdown events have a relatedTarget property, + * whose value is the toggling anchor element. + * + * @param {Object} e bootstrap dropdown event + * @return {Promise} + */ + getBuildsList(e) { + const button = e.relatedTarget; + const endpoint = button.dataset.stageEndpoint; + + return $.ajax({ + dataType: 'json', + type: 'GET', + url: endpoint, + beforeSend: () => { + this.renderBuildsList(button, ''); + this.toggleLoading(button); + }, + success: (data) => { + this.toggleLoading(button); + this.renderBuildsList(button, data.html); + }, + error: () => { + this.toggleLoading(button); + new Flash('An error occurred while fetching the builds.', 'alert'); + }, + }); + } + + /** + * Toggles the visibility of the loading icon. + * + * @param {HTMLElement} stageContainer + * @return {type} + */ + toggleLoading(stageContainer) { + stageContainer.parentElement.querySelector( + `${this.dropdownListSelector} .js-builds-dropdown-loading`, + ).classList.toggle('hidden'); + } + } + + window.gl = window.gl || {}; + window.gl.MiniPipelineGraph = MiniPipelineGraph; +})(); diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index d1168227b77..514556ade0b 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -1,8 +1,10 @@ -/* eslint-disable */ +/* 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); }; }; - this.NamespaceSelect = (function() { + window.NamespaceSelect = (function() { function NamespaceSelect(opts) { this.onSelectItem = bind(this.onSelectItem, this); var fieldName, showAny; @@ -61,10 +63,9 @@ }; return NamespaceSelect; - })(); - this.NamespaceSelects = (function() { + window.NamespaceSelects = (function() { function NamespaceSelects(opts) { var ref; if (opts == null) { @@ -74,14 +75,12 @@ this.$dropdowns.each(function(i, dropdown) { var $dropdown; $dropdown = $(dropdown); - return new NamespaceSelect({ + return new window.NamespaceSelect({ dropdown: $dropdown }); }); } return NamespaceSelects; - })(); - }).call(this); diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index 74dbeb94741..a7ccd03b60c 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -1,6 +1,8 @@ -/* eslint-disable */ +/* 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) { @@ -51,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; @@ -66,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); @@ -74,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])); @@ -94,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; }; @@ -111,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 @@ -284,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; @@ -344,7 +346,6 @@ }; return BranchGraph; - })(); Raphael.prototype.commitTooltip = function(x, y, commit) { @@ -354,7 +355,7 @@ icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20); nameText = this.text(x + 25, y + 10, commit.author.name); idText = this.text(x, y + 35, commit.id); - messageText = this.text(x, y + 50, commit.message); + messageText = this.text(x, y + 50, commit.message.replace(/\r?\n/g, " \n ")); textSet = this.set(icon, nameText, idText, messageText).attr({ "text-anchor": "start", font: "12px Monaco, monospace" @@ -366,6 +367,7 @@ idText.attr({ fill: "#AAA" }); + messageText.node.style["white-space"] = "pre"; this.textWrap(messageText, boxWidth - 50); rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({ fill: "#FFF", @@ -396,23 +398,27 @@ 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"); x = 0; } - x += word.length * letterWidth; - s.push(word + " "); + if (word === "\n") { + s.push("\n"); + x = 0; + } else { + s.push(word + " "); + x += word.length * letterWidth; + } } t.attr({ - text: s.join("") + text: s.join("").trim() }); b = t.getBBox(); - h = Math.abs(b.y2) - Math.abs(b.y) + 1; + h = Math.abs(b.y2) + 1; return t.attr({ - y: b.y + h + y: h }); }; - }).call(this); diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js index 8898e7ace43..37bf6436fd1 100644 --- a/app/assets/javascripts/network/network.js +++ b/app/assets/javascripts/network/network.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */ +/* global BranchGraph */ + (function() { this.Network = (function() { function Network(opts) { @@ -14,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 a192273a180..2e6eb83cec7 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* 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 */ + // This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js @@ -20,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 0e643b0ff14..7f763c13b50 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -1,7 +1,7 @@ -/* eslint-disable */ +/* 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 acb529023fa..41eea78a3e6 100644 --- a/app/assets/javascripts/new_commit_form.js +++ b/app/assets/javascripts/new_commit_form.js @@ -1,6 +1,6 @@ -/* eslint-disable */ +/* 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 4976eef2896..06a72efa21d 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,4 +1,9 @@ -/* eslint-disable */ +/* 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 */ @@ -9,10 +14,10 @@ /*= 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() { - var isMetaKey; + const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; Notes.interval = null; @@ -33,6 +38,7 @@ this.resetMainTargetForm = bind(this.resetMainTargetForm, this); this.refresh = bind(this.refresh, this); this.keydownNoteText = bind(this.keydownNoteText, this); + this.toggleCommitList = bind(this.toggleCommitList, this); this.notes_url = notes_url; this.note_ids = note_ids; this.last_fetched_at = last_fetched_at; @@ -46,6 +52,13 @@ this.setPollingInterval(); this.setupMainTargetNoteForm(); this.initTaskList(); + this.collapseLongCommitList(); + + // We are in the Merge Requests page so we need another edit form for Changes tab + if (gl.utils.getPagePath(1) === 'merge_requests') { + $('.note-edit-form').clone() + .addClass('mr-note-edit-form').insertAfter('.note-edit-form'); + } } Notes.prototype.addBinding = function() { @@ -57,7 +70,7 @@ // change note in UI after update $(document).on("ajax:success", "form.edit-note", this.updateNote); // Edit note link - $(document).on("click", ".js-note-edit", this.showEditForm); + $(document).on("click", ".js-note-edit", this.showEditForm.bind(this)); $(document).on("click", ".note-edit-cancel", this.cancelEdit); // Reopen and close actions for Issue/MR combined with note form submit $(document).on("click", ".js-comment-button", this.updateCloseButton); @@ -81,10 +94,13 @@ $(document).on("click", ".js-add-diff-note-button", this.addDiffNote); // hide diff note form $(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm); + // toggle commit list + $(document).on("click", '.system-note-commit-list-toggler', this.toggleCommitList); // fetch notes when tab becomes visible $(document).on("visibilitychange", this.visibilityChange); // when issue status changes, we need to refresh data $(document).on("issuable:change", this.refresh); + // when a key is clicked on the notes return $(document).on("keydown", ".js-note-text", this.keydownNoteText); }; @@ -108,15 +124,17 @@ $(document).off("click", ".js-note-discard"); $(document).off("keydown", ".js-note-text"); $(document).off('click', '.js-comment-resolve-button'); + $(document).off("click", '.system-note-commit-list-toggler'); $('.note .js-task-list-container').taskList('disable'); return $(document).off('tasklist:changed', '.note .js-task-list-container'); }; Notes.prototype.keydownNoteText = function(e) { var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText; - if (isMetaKey(e)) { + if (gl.utils.isMetaKey(e)) { return; } + $textarea = $(e.target); // Edit previous note when UP arrow is hit switch (e.which) { @@ -156,10 +174,6 @@ } }; - isMetaKey = function(e) { - return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; - }; - Notes.prototype.initRefresh = function() { clearInterval(Notes.interval); return Notes.interval = setInterval((function(_this) { @@ -231,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. @@ -263,6 +287,7 @@ $notesList.append(note.html).syntaxHighlight(); // Update datetime format on the recent note gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false); + this.collapseLongCommitList(); this.initTaskList(); this.refresh(); return this.updateNotesCount(1); @@ -301,7 +326,7 @@ } row = form.closest("tr"); note_html = $(note.html); - note_html.syntaxHighlight(); + note_html.renderGFM(); // is this the first note of discussion? discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); if ((note.original_discussion_id != null) && discussionContainer.length === 0) { @@ -318,18 +343,18 @@ discussionContainer.append(note_html); // Init discussion on 'Discussion' page if it is merge request page if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) { - $('ul.main-notes-list').append(note.discussion_html).syntaxHighlight(); + $('ul.main-notes-list').append(note.discussion_html).renderGFM(); } } else { // append new note to all matching discussions discussionContainer.append(note_html); } - if (typeof DiffNotesApp !== 'undefined') { - DiffNotesApp.compileComponents(); + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + gl.diffNotesCompileComponents(); } - gl.utils.localTimeAgo($('.js-timeago', note_html), false); + gl.utils.localTimeAgo($('.js-timeago'), false); return this.updateNotesCount(1); }; @@ -415,6 +440,7 @@ */ Notes.prototype.addNote = function(xhr, note, status) { + this.handleCreateChanges(note); return this.renderNote(note); }; @@ -433,9 +459,9 @@ var $form = $(xhr.target); if ($form.attr('data-resolve-all') != null) { - var projectPath = $form.data('project-path') - discussionId = $form.data('discussion-id'), - mergeRequestId = $form.data('noteable-iid'); + var projectPath = $form.data('project-path'); + var discussionId = $form.data('discussion-id'); + var mergeRequestId = $form.data('noteable-iid'); if (ResolveService != null) { ResolveService.toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId); @@ -458,17 +484,42 @@ var $html, $note_li; // Convert returned HTML to a jQuery object so we can modify it further $html = $(note.html); + this.revertNoteEditForm(); gl.utils.localTimeAgo($('.js-timeago', $html)); - $html.syntaxHighlight(); + $html.renderGFM(); $html.find('.js-task-list-container').taskList('enable'); // Find the note's `li` element by ID and replace it with the updated HTML $note_li = $('.note-row-' + note.id); $note_li.replaceWith($html); - if (typeof DiffNotesApp !== 'undefined') { - DiffNotesApp.compileComponents(); + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + gl.diffNotesCompileComponents(); + } + }; + + + Notes.prototype.checkContentToAllowEditing = function($el) { + var initialContent = $el.find('.original-note-content').text().trim(); + var currentContent = $el.find('.note-textarea').val(); + var isAllowed = true; + + if (currentContent === initialContent) { + this.removeNoteEditForm($el); + } + else { + var $buttons = $el.find('.note-form-actions'); + var isWidgetVisible = gl.utils.isInViewport($el.get(0)); + + if (!isWidgetVisible) { + gl.utils.scrollToElement($el); + } + + $el.find('.js-edit-warning').show(); + isAllowed = false; } + + return isAllowed; }; @@ -477,43 +528,27 @@ Replaces the note text with the note edit form Adds a data attribute to the form with the original content of the note for cancellations - */ - + */ Notes.prototype.showEditForm = function(e, scrollTo, myLastNote) { - var $noteText, done, form, note; e.preventDefault(); - note = $(this).closest(".note"); - note.addClass("is-editting"); - form = note.find(".note-edit-form"); - form.addClass('current-note-edit-form'); - // Show the attachment delete link - note.find(".js-note-attachment-delete").show(); - done = function($noteText) { - var noteTextVal; - // Neat little trick to put the cursor at the end - noteTextVal = $noteText.val(); - // Store the original note text in a data attribute to retrieve if a user cancels edit. - form.find('form.edit-note').data('original-note', noteTextVal); - return $noteText.val('').val(noteTextVal); - }; - new GLForm(form); - if ((scrollTo != null) && (myLastNote != null)) { - // scroll to the bottom - // so the open of the last element doesn't make a jump - $('html, body').scrollTop($(document).height()); - return $('html, body').animate({ - scrollTop: myLastNote.offset().top - 150 - }, 500, function() { - var $noteText; - $noteText = form.find(".js-note-text"); - $noteText.focus(); - return done($noteText); - }); - } else { - $noteText = form.find('.js-note-text'); - $noteText.focus(); - return done($noteText); + + var $target = $(e.target); + var $editForm = $(this.getEditFormSelector($target)); + var $note = $target.closest('.note'); + var $currentlyEditing = $('.note.is-editting:visible'); + + if ($currentlyEditing.length) { + var isEditAllowed = this.checkContentToAllowEditing($currentlyEditing); + + if (!isEditAllowed) { + return; + } } + + $note.find('.js-note-attachment-delete').show(); + $editForm.addClass('current-note-edit-form'); + $note.addClass('is-editting'); + this.putEditFormInPlace($target); }; @@ -524,19 +559,41 @@ */ Notes.prototype.cancelEdit = function(e) { - var note; e.preventDefault(); - note = $(e.target).closest('.note'); + var $target = $(e.target); + var note = $target.closest('.note'); + note.find('.js-edit-warning').hide(); + this.revertNoteEditForm($target); return this.removeNoteEditForm(note); }; + Notes.prototype.revertNoteEditForm = function($target) { + $target = $target || $('.note.is-editting:visible'); + var selector = this.getEditFormSelector($target); + var $editForm = $(selector); + + $editForm.insertBefore('.notes-form'); + $editForm.find('.js-comment-button').enable(); + $editForm.find('.js-edit-warning').hide(); + }; + + Notes.prototype.getEditFormSelector = function($el) { + var selector = '.note-edit-form:not(.mr-note-edit-form)'; + + if ($el.parents('#diffs').length) { + selector = '.note-edit-form.mr-note-edit-form'; + } + + return selector; + }; + Notes.prototype.removeNoteEditForm = function(note) { - var form; - form = note.find(".current-note-edit-form"); - note.removeClass("is-editting"); - form.removeClass("current-note-edit-form"); + var form = note.find('.current-note-edit-form'); + note.removeClass('is-editting'); + form.removeClass('current-note-edit-form'); + form.find('.js-edit-warning').hide(); // Replace markdown textarea text with original note text. - return form.find(".js-note-text").val(form.find('form.edit-note').data('original-note')); + return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note')); }; @@ -559,11 +616,9 @@ note = $(el); notes = note.closest(".notes"); - if (typeof DiffNotesApp !== "undefined" && DiffNotesApp !== null) { - ref = DiffNotesApp.$refs[noteId]; - - if (ref) { - ref.$destroy(true); + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + if (gl.diffNoteApps[noteId]) { + gl.diffNoteApps[noteId].$destroy(); } } @@ -643,11 +698,12 @@ form.find('.js-note-target-close').remove(); this.setupNoteForm(form); - if (typeof DiffNotesApp !== 'undefined') { + if (typeof gl.diffNotesCompileComponents !== 'undefined') { var $commentBtn = form.find('comment-and-resolve-btn'); $commentBtn .attr(':discussion-id', "'" + dataHolder.data('discussionId') + "'"); - DiffNotesApp.$compile($commentBtn.get(0)); + + gl.diffNotesCompileComponents(); } form.find(".js-note-text").focus(); @@ -668,7 +724,7 @@ */ Notes.prototype.addDiffNote = function(e) { - var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, replyButton, row, rowCssToAdd, targetContent; + var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent; e.preventDefault(); $link = $(e.currentTarget); row = $link.closest("tr"); @@ -830,24 +886,55 @@ Notes.prototype.initTaskList = function() { this.enableTaskList(); - return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList); + return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList.bind(this)); }; Notes.prototype.enableTaskList = function() { return $('.note .js-task-list-container').taskList('enable'); }; - Notes.prototype.updateTaskList = function() { - return $('form', this).submit(); + Notes.prototype.putEditFormInPlace = function($el) { + var $editForm = $(this.getEditFormSelector($el)); + var $note = $el.closest('.note'); + + $editForm.insertAfter($note.find('.note-text')); + + var $originalContentEl = $note.find('.original-note-content'); + var originalContent = $originalContentEl.text().trim(); + var postUrl = $originalContentEl.data('post-url'); + var targetId = $originalContentEl.data('target-id'); + var targetType = $originalContentEl.data('target-type'); + + new GLForm($editForm.find('form')); + + $editForm.find('form') + .attr('action', postUrl) + .attr('data-remote', 'true'); + $editForm.find('.js-form-target-id').val(targetId); + $editForm.find('.js-form-target-type').val(targetType); + $editForm.find('.js-note-text').focus().val(originalContent); + $editForm.find('.js-md-write-button').trigger('click'); + $editForm.find('.referenced-users').hide(); + }; + + Notes.prototype.updateTaskList = function(e) { + var $target = $(e.target); + var $list = $target.closest('.js-task-list-container'); + var $editForm = $(this.getEditFormSelector($target)); + var $note = $list.closest('.note'); + + this.putEditFormInPlace($list); + $editForm.find('#note_note').val($note.find('.original-task-list').val()); + $('form', $list).submit(); }; Notes.prototype.updateNotesCount = function(updateCount) { - return this.notesCountBadge.text(parseInt(this.notesCountBadge.text()) + updateCount); + return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount); }; - Notes.prototype.resolveDiscussion = function () { - var $this = $(this), - discussionId = $this.attr('data-discussion-id'); + Notes.prototype.resolveDiscussion = function() { + var $this = $(this); + var discussionId = $this.attr('data-discussion-id'); $this .closest('form') @@ -856,8 +943,36 @@ .attr('data-project-path', $this.attr('data-project-path')); }; - return Notes; + Notes.prototype.toggleCommitList = function(e) { + const $element = $(e.target); + const $closestSystemCommitList = $element.siblings('.system-note-commit-list'); - })(); + $closestSystemCommitList.toggleClass('hide-shade'); + }; + + /** + Scans system notes with `ul` elements in system note body + then collapse long commit list pushed by user to make it less + intrusive. + */ + Notes.prototype.collapseLongCommitList = function() { + const systemNotes = $('#notes-list').find('li.system-note').has('ul'); + $.each(systemNotes, function(index, systemNote) { + const $systemNote = $(systemNote); + const headerMessage = $systemNote.find('.note-text').find('p:first').text().replace(':', ''); + + $systemNote.find('.note-header .system-note-message').html(headerMessage); + + if ($systemNote.find('li').length > MAX_VISIBLE_COMMIT_LIST_COUNT) { + $systemNote.find('.note-text').addClass('system-note-commit-list'); + $systemNote.find('.system-note-commit-list-toggler').show(); + } else { + $systemNote.find('.note-text').addClass('system-note-commit-list hide-shade'); + } + }); + }; + + return Notes; + })(); }).call(this); diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js index ef3f2c6ae73..926dc35fee8 100644 --- a/app/assets/javascripts/notifications_dropdown.js +++ b/app/assets/javascripts/notifications_dropdown.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* 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() { this.NotificationsDropdown = (function() { function NotificationsDropdown() { @@ -17,7 +19,7 @@ }); $(document).off('ajax:success', '.notification-form').on('ajax:success', '.notification-form', function(e, data) { if (data.saved) { - return $(e.currentTarget).closest('.notification-dropdown').replaceWith(data.html); + return $(e.currentTarget).closest('.js-notification-dropdown').replaceWith(data.html); } else { return new Flash('Failed to save new settings', 'alert'); } @@ -25,7 +27,5 @@ } return NotificationsDropdown; - })(); - }).call(this); diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js index 6fbec8efe9b..c3d7cc0adfb 100644 --- a/app/assets/javascripts/notifications_form.js +++ b/app/assets/javascripts/notifications_form.js @@ -1,6 +1,6 @@ -/* eslint-disable */ +/* 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/pager.js b/app/assets/javascripts/pager.js deleted file mode 100644 index 2e4dc62273e..00000000000 --- a/app/assets/javascripts/pager.js +++ /dev/null @@ -1,64 +0,0 @@ -/* eslint-disable */ -(function() { - this.Pager = { - init: function(limit, preload, disable, callback) { - this.limit = limit != null ? limit : 0; - this.disable = disable != null ? disable : false; - this.callback = callback != null ? callback : $.noop; - this.loading = $('.loading').first(); - if (preload) { - this.offset = 0; - this.getOld(); - } else { - this.offset = this.limit; - } - return this.initLoadMore(); - }, - getOld: function() { - this.loading.show(); - return $.ajax({ - type: "GET", - url: $(".content_list").data('href') || location.href, - data: "limit=" + this.limit + "&offset=" + this.offset, - complete: (function(_this) { - return function() { - return _this.loading.hide(); - }; - })(this), - success: function(data) { - Pager.append(data.count, data.html); - return Pager.callback(); - }, - dataType: "json" - }); - }, - append: function(count, html) { - $(".content_list").append(html); - if (count > 0) { - return this.offset += count; - } else { - return this.disable = true; - } - }, - initLoadMore: function() { - $(document).unbind('scroll'); - return $(document).endlessScroll({ - bottomPixels: 400, - fireDelay: 1000, - fireOnce: true, - ceaseFire: function() { - return Pager.disable; - }, - callback: (function(_this) { - return function(i) { - if (!_this.loading.is(':visible')) { - _this.loading.show(); - return Pager.getOld(); - } - }; - })(this) - }); - } - }; - -}).call(this); diff --git a/app/assets/javascripts/pager.js.es6 b/app/assets/javascripts/pager.js.es6 new file mode 100644 index 00000000000..e35cf6d295e --- /dev/null +++ b/app/assets/javascripts/pager.js.es6 @@ -0,0 +1,73 @@ +(() => { + const ENDLESS_SCROLL_BOTTOM_PX = 400; + const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000; + + const Pager = { + init(limit = 0, preload = false, disable = false, callback = $.noop) { + this.limit = limit; + this.offset = this.limit; + this.disable = disable; + this.callback = callback; + this.loading = $('.loading').first(); + if (preload) { + this.offset = 0; + this.getOld(); + } + this.initLoadMore(); + }, + + getOld() { + this.loading.show(); + $.ajax({ + type: 'GET', + url: $('.content_list').data('href') || window.location.href, + data: `limit=${this.limit}&offset=${this.offset}`, + dataType: 'json', + error: () => this.loading.hide(), + success: (data) => { + this.append(data.count, data.html); + this.callback(); + + // keep loading until we've filled the viewport height + if (!this.disable && !this.isScrollable()) { + this.getOld(); + } else { + this.loading.hide(); + } + }, + }); + }, + + append(count, html) { + $('.content_list').append(html); + if (count > 0) { + this.offset += count; + } else { + this.disable = true; + } + }, + + isScrollable() { + const $w = $(window); + return $(document).height() > $w.height() + $w.scrollTop() + ENDLESS_SCROLL_BOTTOM_PX; + }, + + initLoadMore() { + $(document).unbind('scroll'); + $(document).endlessScroll({ + bottomPixels: ENDLESS_SCROLL_BOTTOM_PX, + fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS, + fireOnce: true, + ceaseFire: () => this.disable === true, + callback: () => { + if (!this.loading.is(':visible')) { + this.loading.show(); + this.getOld(); + } + }, + }); + }, + }; + + window.Pager = Pager; +})(); diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index e6fada5c84c..43263368494 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -1,29 +1,23 @@ -/* eslint-disable */ -((global) => { +/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, no-param-reassign, max-len */ - class Pipelines { - constructor() { - this.initGraphToggle(); - this.addMarginToBuildColumns(); - } +//= require lib/utils/bootstrap_linked_tabs - initGraphToggle() { - this.pipelineGraph = document.querySelector('.pipeline-graph'); - this.toggleButton = document.querySelector('.toggle-pipeline-btn'); - this.toggleButtonText = this.toggleButton.querySelector('.toggle-btn-text'); - this.toggleButton.addEventListener('click', this.toggleGraph.bind(this)); - } +((global) => { + class Pipelines { + constructor(options = {}) { + if (options.initTabs && options.tabsOptions) { + new global.LinkedTabs(options.tabsOptions); + } - toggleGraph() { - const graphCollapsed = this.pipelineGraph.classList.contains('graph-collapsed'); - this.toggleButton.classList.toggle('graph-collapsed'); - this.pipelineGraph.classList.toggle('graph-collapsed'); - this.toggleButtonText.textContent = graphCollapsed ? 'Hide' : 'Expand'; + this.addMarginToBuildColumns(); } addMarginToBuildColumns() { + this.pipelineGraph = document.querySelector('.js-pipeline-graph'); + const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)'); - for (buildNodeIndex in secondChildBuildNodes) { + + for (const buildNodeIndex in secondChildBuildNodes) { const buildNode = secondChildBuildNodes[buildNodeIndex]; const firstChildBuildNode = buildNode.previousElementSibling; if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue; @@ -35,10 +29,10 @@ const columnBuilds = previousColumn.querySelectorAll('.build'); if (columnBuilds.length === 1) previousColumn.classList.add('no-margin'); } + this.pipelineGraph.classList.remove('hidden'); } } global.Pipelines = Pipelines; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index f2a45a18bed..07eea98e737 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -1,13 +1,18 @@ -/* eslint-disable */ +/* eslint-disable func-names, no-var, object-shorthand, comma-dangle, prefer-arrow-callback */ + // MarkdownPreview // // Handles toggling the "Write" and "Preview" tab clicks, rendering the preview, // and showing a warning when more than `x` users are referenced. // -(function() { - var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector; - - this.MarkdownPreview = (function() { +(function () { + var lastTextareaPreviewed; + var lastTextareaHeight = null; + var markdownPreview; + var previewButtonSelector; + var writeButtonSelector; + + window.MarkdownPreview = (function () { function MarkdownPreview() {} // Minimum number of users referenced before triggering a warning @@ -15,75 +20,73 @@ MarkdownPreview.prototype.ajaxCache = {}; - MarkdownPreview.prototype.showPreview = function(form) { - var mdText, preview; - preview = form.find('.js-md-preview'); - mdText = form.find('textarea.markdown-area').val(); + MarkdownPreview.prototype.showPreview = function ($form) { + var mdText; + var preview = $form.find('.js-md-preview'); + if (preview.hasClass('md-preview-loading')) { + return; + } + mdText = $form.find('textarea.markdown-area').val(); + if (mdText.trim().length === 0) { preview.text('Nothing to preview.'); - return this.hideReferencedUsers(form); + this.hideReferencedUsers($form); } else { - preview.text('Loading...'); - return this.renderMarkdown(mdText, (function(_this) { - return function(response) { - preview.html(response.body); - preview.syntaxHighlight(); - return _this.renderReferencedUsers(response.references.users, form); - }; - })(this)); + preview.addClass('md-preview-loading').text('Loading...'); + this.fetchMarkdownPreview(mdText, (function (response) { + preview.removeClass('md-preview-loading').html(response.body); + preview.renderGFM(); + this.renderReferencedUsers(response.references.users, $form); + }).bind(this)); } }; - MarkdownPreview.prototype.renderMarkdown = function(text, success) { + MarkdownPreview.prototype.fetchMarkdownPreview = function (text, success) { if (!window.preview_markdown_path) { return; } if (text === this.ajaxCache.text) { - return success(this.ajaxCache.response); + success(this.ajaxCache.response); + return; } - return $.ajax({ + $.ajax({ type: 'POST', url: window.preview_markdown_path, data: { text: text }, dataType: 'json', - success: (function(_this) { - return function(response) { - _this.ajaxCache = { - text: text, - response: response - }; - return success(response); + success: (function (response) { + this.ajaxCache = { + text: text, + response: response }; - })(this) + success(response); + }).bind(this) }); }; - MarkdownPreview.prototype.hideReferencedUsers = function(form) { - var referencedUsers; - referencedUsers = form.find('.referenced-users'); - return referencedUsers.hide(); + MarkdownPreview.prototype.hideReferencedUsers = function ($form) { + $form.find('.referenced-users').hide(); }; - MarkdownPreview.prototype.renderReferencedUsers = function(users, form) { + MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) { var referencedUsers; - referencedUsers = form.find('.referenced-users'); + referencedUsers = $form.find('.referenced-users'); if (referencedUsers.length) { if (users.length >= this.referenceThreshold) { referencedUsers.show(); - return referencedUsers.find('.js-referenced-users-count').text(users.length); + referencedUsers.find('.js-referenced-users-count').text(users.length); } else { - return referencedUsers.hide(); + referencedUsers.hide(); } } }; return MarkdownPreview; + }()); - })(); - - markdownPreview = new MarkdownPreview(); + markdownPreview = new window.MarkdownPreview(); previewButtonSelector = '.js-md-preview-button'; @@ -91,71 +94,75 @@ lastTextareaPreviewed = null; - $.fn.setupMarkdownPreview = function() { - var $form, form_textarea; - $form = $(this); - form_textarea = $form.find('textarea.markdown-area'); - form_textarea.on('input', function() { - return markdownPreview.hideReferencedUsers($form); - }); - return form_textarea.on('blur', function() { - return markdownPreview.showPreview($form); + $.fn.setupMarkdownPreview = function () { + var $form = $(this); + $form.find('textarea.markdown-area').on('input', function () { + markdownPreview.hideReferencedUsers($form); }); }; - $(document).on('markdown-preview:show', function(e, $form) { + $(document).on('markdown-preview:show', function (e, $form) { if (!$form) { return; } + lastTextareaPreviewed = $form.find('textarea.markdown-area'); + lastTextareaHeight = lastTextareaPreviewed.height(); + // toggle tabs $form.find(writeButtonSelector).parent().removeClass('active'); $form.find(previewButtonSelector).parent().addClass('active'); + // toggle content $form.find('.md-write-holder').hide(); $form.find('.md-preview-holder').show(); - return markdownPreview.showPreview($form); + markdownPreview.showPreview($form); }); - $(document).on('markdown-preview:hide', function(e, $form) { + $(document).on('markdown-preview:hide', function (e, $form) { if (!$form) { return; } lastTextareaPreviewed = null; + + if (lastTextareaHeight) { + $form.find('textarea.markdown-area').height(lastTextareaHeight); + } + // toggle tabs $form.find(writeButtonSelector).parent().addClass('active'); $form.find(previewButtonSelector).parent().removeClass('active'); + // toggle content $form.find('.md-write-holder').show(); $form.find('textarea.markdown-area').focus(); - return $form.find('.md-preview-holder').hide(); + $form.find('.md-preview-holder').hide(); }); - $(document).on('markdown-preview:toggle', function(e, keyboardEvent) { + $(document).on('markdown-preview:toggle', function (e, keyboardEvent) { var $target; $target = $(keyboardEvent.target); if ($target.is('textarea.markdown-area')) { $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]); - return keyboardEvent.preventDefault(); + keyboardEvent.preventDefault(); } else if (lastTextareaPreviewed) { $target = lastTextareaPreviewed; $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]); - return keyboardEvent.preventDefault(); + keyboardEvent.preventDefault(); } }); - $(document).on('click', previewButtonSelector, function(e) { + $(document).on('click', previewButtonSelector, function (e) { var $form; e.preventDefault(); $form = $(this).closest('form'); - return $(document).triggerHandler('markdown-preview:show', [$form]); + $(document).triggerHandler('markdown-preview:show', [$form]); }); - $(document).on('click', writeButtonSelector, function(e) { + $(document).on('click', writeButtonSelector, function (e) { var $form; e.preventDefault(); $form = $(this).closest('form'); - return $(document).triggerHandler('markdown-preview:hide', [$form]); + $(document).triggerHandler('markdown-preview:hide', [$form]); }); - -}).call(this); +}()); diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js.es6 index 6da6c1d0295..42e9847af91 100644 --- a/app/assets/javascripts/profile/gl_crop.js.es6 +++ b/app/assets/javascripts/profile/gl_crop.js.es6 @@ -1,13 +1,12 @@ -/* eslint-disable */ -((global) => { +/* 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); @@ -135,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)); } @@ -168,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 73858388261..6dbaae25f2a 100644 --- a/app/assets/javascripts/profile/profile.js.es6 +++ b/app/assets/javascripts/profile/profile.js.es6 @@ -1,6 +1,7 @@ -/* eslint-disable */ -((global) => { +/* 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); @@ -35,20 +36,16 @@ } onSubmitForm(e) { - e.preventDefault(); return this.saveForm(); } beforeUpdateUsername() { - $('.loading-username').show(); - $(this).find('.update-success').hide(); - return $(this).find('.update-failed').hide(); + $('.loading-username', this).removeClass('hidden'); } afterUpdateUsername() { - $('.loading-username').hide(); - $(this).find('.btn-save').enable(); - return $(this).find('.loading-gif').hide(); + $('.loading-username', this).addClass('hidden'); + $('button[type=submit]', this).enable(); } onUpdateNotifs(e, data) { @@ -97,5 +94,4 @@ return new Profile(); } }); - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js index 22bee0f6187..f50802bdf2e 100644 --- a/app/assets/javascripts/profile/profile_bundle.js +++ b/app/assets/javascripts/profile/profile_bundle.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren */ /*= require_tree . */ diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 2d0c6b16699..7cf630a1d76 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -1,4 +1,8 @@ -/* eslint-disable */ +/* 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 */ + (function() { this.Project = (function() { function Project() { @@ -90,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()); } } @@ -103,7 +107,5 @@ }; return Project; - })(); - }).call(this); diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js index 61877c6616d..a6d3ba9eb86 100644 --- a/app/assets/javascripts/project_avatar.js +++ b/app/assets/javascripts/project_avatar.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* 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 ddac5ed83e1..04fe84683f3 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -1,6 +1,8 @@ -/* eslint-disable */ +/* 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; @@ -69,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; @@ -90,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) { @@ -165,7 +167,5 @@ }; return ProjectFindFile; - })(); - }).call(this); diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js index fd95f8f2c19..208f25a0e33 100644 --- a/app/assets/javascripts/project_fork.js +++ b/app/assets/javascripts/project_fork.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* 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 f1c4a9fe542..6614d8952cd 100644 --- a/app/assets/javascripts/project_import.js +++ b/app/assets/javascripts/project_import.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */ +/* global Turbolinks */ + (function() { this.ProjectImport = (function() { function ProjectImport() { @@ -8,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 new file mode 100644 index 00000000000..8365f7118d5 --- /dev/null +++ b/app/assets/javascripts/project_label_subscription.js.es6 @@ -0,0 +1,53 @@ +/* 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 { + constructor(container) { + this.$container = $(container); + this.$buttons = this.$container.find('.js-subscribe-button'); + + this.$buttons.on('click', this.toggleSubscription.bind(this)); + } + + toggleSubscription(event) { + event.preventDefault(); + + const $btn = $(event.currentTarget); + const $span = $btn.find('span'); + const url = $btn.attr('data-url'); + const oldStatus = $btn.attr('data-status'); + + $btn.addClass('disabled'); + $span.toggleClass('hidden'); + + $.ajax({ + type: 'POST', + url: url + }).done(() => { + let newStatus, newAction; + + if (oldStatus === 'unsubscribed') { + [newStatus, newAction] = ['subscribed', 'Unsubscribe']; + } else { + [newStatus, newAction] = ['unsubscribed', 'Subscribe']; + } + + $span.toggleClass('hidden'); + $btn.removeClass('disabled'); + + this.$buttons.attr('data-status', newStatus); + this.$buttons.find('> span').text(newAction); + + 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'); + } + } + }); + } + } + + global.ProjectLabelSubscription = ProjectLabelSubscription; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js index 0d3fb31a9cf..3aa6f6771ce 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/project_new.js @@ -1,6 +1,7 @@ -/* eslint-disable */ +/* 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 e1acf3c8232..7b5e9953598 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* 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() { this.ProjectSelect = (function() { function ProjectSelect() { @@ -13,6 +15,7 @@ }, data: function(term, callback) { var finalCallback, projectsCallback; + var orderBy = $dropdown.data('order-by'); finalCallback = function(projects) { return callback(projects); }; @@ -32,7 +35,7 @@ if (this.groupId) { return Api.groupProjects(this.groupId, term, projectsCallback); } else { - return Api.projects(term, this.orderBy, projectsCallback); + return Api.projects(term, orderBy, projectsCallback); } }, url: function(project) { @@ -97,7 +100,5 @@ } return ProjectSelect; - })(); - }).call(this); diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js index 21650f5f67a..aad130cf267 100644 --- a/app/assets/javascripts/project_show.js +++ b/app/assets/javascripts/project_show.js @@ -1,12 +1,11 @@ -/* eslint-disable */ +/* 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/project_variables.js.es6 b/app/assets/javascripts/project_variables.js.es6 new file mode 100644 index 00000000000..4ee2e49306d --- /dev/null +++ b/app/assets/javascripts/project_variables.js.es6 @@ -0,0 +1,43 @@ +(() => { + const HIDDEN_VALUE_TEXT = '******'; + + class ProjectVariables { + constructor() { + this.$revealBtn = $('.js-btn-toggle-reveal-values'); + this.$revealBtn.on('click', this.toggleRevealState.bind(this)); + } + + toggleRevealState(e) { + e.preventDefault(); + + const oldStatus = this.$revealBtn.attr('data-status'); + let newStatus = 'hidden'; + let newAction = 'Reveal Values'; + + if (oldStatus === 'hidden') { + newStatus = 'revealed'; + newAction = 'Hide Values'; + } + + this.$revealBtn.attr('data-status', newStatus); + + const $variables = $('.variable-value'); + + $variables.each((_, variable) => { + const $variable = $(variable); + let newText = HIDDEN_VALUE_TEXT; + + if (newStatus === 'revealed') { + newText = $variable.attr('data-value'); + } + + $variable.text(newText); + }); + + this.$revealBtn.text(newAction); + } + } + + window.gl = window.gl || {}; + window.gl.ProjectVariables = ProjectVariables; +})(); diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js index 3458cd89ae2..69a11dfaf39 100644 --- a/app/assets/javascripts/projects_list.js +++ b/app/assets/javascripts/projects_list.js @@ -1,6 +1,7 @@ -/* eslint-disable */ +/* 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() { - this.ProjectsList = { + window.ProjectsList = { init: function() { $(".projects-list-filter").off('keyup'); this.initSearch(); @@ -9,7 +10,7 @@ initSearch: function() { var debounceFilter, projectsListFilter; projectsListFilter = $('.projects-list-filter'); - debounceFilter = _.debounce(ProjectsList.filterResults, 500); + debounceFilter = _.debounce(window.ProjectsList.filterResults, 500); return projectsListFilter.on('keyup', function(e) { if (projectsListFilter.val() !== '') { return debounceFilter(); @@ -46,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 2d60947a666..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,6 +1,7 @@ -/* eslint-disable */ +/* 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) { @@ -24,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 c45c9d8ff22..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,6 +1,8 @@ -/* eslint-disable */ +/* 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() { @@ -42,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]"]'); @@ -50,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_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 index e3f226e9a2a..03f4531abf5 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, no-unused-vars */ + class ProtectedBranchDropdown { constructor(options) { this.onSelect = options.onSelect; @@ -75,3 +76,5 @@ class ProtectedBranchDropdown { this.$dropdownFooter.toggleClass('hidden', !branchName); } } + +window.ProtectedBranchDropdown = ProtectedBranchDropdown; 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 ac3142ffb07..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,6 +1,8 @@ -/* eslint-disable */ +/* 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) { @@ -12,7 +14,6 @@ } buildDropdowns() { - // Allowed to merge dropdown new gl.ProtectedBranchAccessDropdown({ $dropdown: this.$allowedToMergeDropdown, @@ -33,7 +34,7 @@ const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); // Do not update if one dropdown has not selected any option - if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; + if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; $.ajax({ type: 'POST', @@ -61,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 705378a364d..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,9 +1,10 @@ -/* eslint-disable */ +/* 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 @@ -13,6 +14,5 @@ }); }); } - } - + }; })(window); diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js index 17e34163831..15b3affd469 100644 --- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js +++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js @@ -1,2 +1 @@ -/* eslint-disable */ /*= require_tree . */ diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js new file mode 100644 index 00000000000..0caf8ba4344 --- /dev/null +++ b/app/assets/javascripts/render_gfm.js @@ -0,0 +1,15 @@ +/* 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 +// +(function() { + $.fn.renderGFM = function() { + this.find('.js-syntax-highlight').syntaxHighlight(); + this.find('.js-render-math').renderMath(); + }; + + $(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 new file mode 100644 index 00000000000..6cef449babf --- /dev/null +++ b/app/assets/javascripts/render_math.js @@ -0,0 +1,54 @@ +/* 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 +// +// ### Example Markup +// +// <code class="js-render-math"></div> +// +(function() { + // Only load once + var katexLoaded = false; + + // Loop over all math elements and render math + var renderWithKaTeX = function (elements) { + elements.each(function () { + var mathNode = $('<span></span>'); + var $this = $(this); + + var display = $this.attr('data-math-style') === 'display'; + try { + katex.render($this.text(), mathNode.get(0), { displayMode: display }); + mathNode.insertAfter($this); + $this.remove(); + } catch (err) { + // What can we do?? + console.log(err.message); + } + }); + }; + + $.fn.renderMath = function() { + var $this = this; + if ($this.length === 0) return; + + if (katexLoaded) renderWithKaTeX($this); + else { + // Request CSS file so it is in the cache + $.get(gon.katex_css_url, function() { + var css = $('<link>', + { rel: 'stylesheet', + type: 'text/css', + href: gon.katex_css_url, + }); + css.appendTo('head'); + + // Load KaTeX js + $.getScript(gon.katex_js_url, function() { + katexLoaded = true; + renderWithKaTeX($this); // Run KaTeX + }); + }); + } + }; +}).call(this); diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index df38937858f..76a0f993ea0 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -1,6 +1,8 @@ -/* eslint-disable */ +/* 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) { @@ -16,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); @@ -198,7 +200,5 @@ }; return Sidebar; - })(); - }).call(this); diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index d79e6f014f6..489e567259c 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* 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() { this.Search = (function() { function Search() { @@ -10,6 +12,9 @@ selectable: true, filterable: true, fieldName: 'group_id', + search: { + fields: ['name'] + }, data: function(term, callback) { return Api.groups(term, {}, function(data) { data.unshift({ @@ -38,6 +43,9 @@ selectable: true, filterable: true, fieldName: 'project_id', + search: { + fields: ['name'] + }, data: function(term, callback) { return Api.projects(term, 'id', function(data) { data.unshift({ @@ -88,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 5fa94556501..480755899fb 100644 --- a/app/assets/javascripts/search_autocomplete.js.es6 +++ b/app/assets/javascripts/search_autocomplete.js.es6 @@ -1,6 +1,6 @@ -/* eslint-disable */ -((global) => { +/* 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, @@ -104,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) { @@ -141,8 +141,9 @@ } getCategoryContents() { - var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils; + var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName, utils; userId = gon.current_user_id; + userName = gon.current_username; utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; if (utils.isInGroupsPage() && groupOptions) { options = groupOptions[utils.getGroupSlug()]; @@ -157,10 +158,10 @@ header: "" + name }, { text: 'Issues assigned to me', - url: issuesPath + "/?assignee_id=" + userId + url: issuesPath + "/?assignee_username=" + userName }, { text: "Issues I've created", - url: issuesPath + "/?author_id=" + userId + url: issuesPath + "/?author_username=" + userName }, 'separator', { text: 'Merge requests assigned to me', url: mrPath + "/?assignee_id=" + userId @@ -214,7 +215,7 @@ this.dropdown.addClass('open').trigger('shown.bs.dropdown'); return this.searchInput.removeClass('disabled'); } - }; + } // Saves last length of the entered text onSearchInputKeyDown() { @@ -278,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 @@ -303,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]); } @@ -329,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') { @@ -359,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) { @@ -382,8 +383,7 @@ this.disableAutocomplete(); return this.searchInput.val('').focus(); } - }; - + } } global.SearchAutocomplete = SearchAutocomplete; @@ -424,5 +424,4 @@ }; } }); - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index 8d8ab6dda5e..c56ee429b8e 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,6 +1,10 @@ -/* eslint-disable */ +/* 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) { @@ -47,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()); } @@ -74,7 +78,6 @@ }; return Shortcuts; - })(); $(document).on('click.more_help', '.js-more-help-button', function(e) { @@ -95,5 +98,4 @@ } }; })(); - }).call(this); diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js index 704a8bd3a57..d50ddd98de1 100644 --- a/app/assets/javascripts/shortcuts_blob.js +++ b/app/assets/javascripts/shortcuts_blob.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* 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 */ /*= require shortcuts */ @@ -23,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 befe4eccdba..603fefbf15a 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* 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 */ /*= require shortcuts */ @@ -34,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 90ed4267661..8469837533b 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* 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 */ /*= require shortcuts_navigation */ @@ -32,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 25ec7dbc067..6f919de3da7 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -1,4 +1,8 @@ -/* eslint-disable */ +/* 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 */ +/* global sidebar */ /*= require mousetrap */ /*= require shortcuts_navigation */ @@ -72,7 +76,5 @@ }; return ShortcutsIssuable; - })(ShortcutsNavigation); - }).call(this); diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index 19c6b7d30ab..afeda0dd5fe 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* 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 */ /*= require shortcuts */ @@ -62,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 002e979a2c6..79896e35cbb 100644 --- a/app/assets/javascripts/shortcuts_network.js +++ b/app/assets/javascripts/shortcuts_network.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* 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 */ /*= require shortcuts_navigation */ @@ -22,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 a23ca449c4b..05234643c18 100644 --- a/app/assets/javascripts/sidebar.js.es6 +++ b/app/assets/javascripts/sidebar.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign */ +/* global Cookies */ + ((global) => { let singleton; @@ -92,5 +94,4 @@ } global.Sidebar = Sidebar; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/signin_tabs_memoizer.js.es6 b/app/assets/javascripts/signin_tabs_memoizer.js.es6 new file mode 100644 index 00000000000..d811d1cd53a --- /dev/null +++ b/app/assets/javascripts/signin_tabs_memoizer.js.es6 @@ -0,0 +1,49 @@ +/* eslint no-param-reassign: ["error", { "props": false }]*/ +/* eslint no-new: "off" */ +((global) => { + /** + * Memorize the last selected tab after reloading a page. + * Does that setting the current selected tab in the localStorage + */ + class ActiveTabMemoizer { + constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) { + this.currentTabKey = currentTabKey; + this.tabSelector = tabSelector; + this.bootstrap(); + } + + bootstrap() { + const tabs = document.querySelectorAll(this.tabSelector); + if (tabs.length > 0) { + tabs[0].addEventListener('click', (e) => { + if (e.target && e.target.nodeName === 'A') { + const anchorName = e.target.getAttribute('href'); + this.saveData(anchorName); + } + }); + } + + this.showTab(); + } + + showTab() { + const anchorName = this.readData(); + if (anchorName) { + const tab = document.querySelector(`${this.tabSelector} a[href="${anchorName}"]`); + if (tab) { + tab.click(); + } + } + } + + saveData(val) { + localStorage.setItem(this.currentTabKey, val); + } + + readData() { + return localStorage.getItem(this.currentTabKey); + } + } + + global.ActiveTabMemoizer = ActiveTabMemoizer; +})(window); diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 8e54ca4f0dc..5b20c63384c 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -1,8 +1,9 @@ -/* eslint-disable */ +/* 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); }; }; - this.SingleFileDiff = (function() { + window.SingleFileDiff = (function() { var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; WRAPPER = '<div class="diff-content diff-wrap-lines"></div>'; @@ -13,7 +14,7 @@ COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>'; - function SingleFileDiff(file, forceLoad, cb) { + function SingleFileDiff(file) { this.file = file; this.toggleDiff = bind(this.toggleDiff, this); this.content = $('.diff-content', this.file); @@ -31,29 +32,28 @@ this.content.after(this.collapsedContent); this.$toggleIcon.addClass('fa-caret-down'); } - $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff); - if (forceLoad) { - this.toggleDiff(null, cb); - } + + $('.file-title, .click-to-expand', this.file).on('click', (function (e) { + this.toggleDiff($(e.target)); + }).bind(this)); } - SingleFileDiff.prototype.toggleDiff = function(e, cb) { - var $target = $(e.target); + SingleFileDiff.prototype.toggleDiff = function($target, cb) { if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return; this.isOpen = !this.isOpen; if (!this.isOpen && !this.hasError) { this.content.hide(); this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down'); this.collapsedContent.show(); - if (typeof DiffNotesApp !== 'undefined') { - DiffNotesApp.compileComponents(); + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + gl.diffNotesCompileComponents(); } } else if (this.content) { this.collapsedContent.hide(); this.content.show(); this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right'); - if (typeof DiffNotesApp !== 'undefined') { - DiffNotesApp.compileComponents(); + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + gl.diffNotesCompileComponents(); } } else { this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right'); @@ -76,8 +76,8 @@ } _this.collapsedContent.after(_this.content); - if (typeof DiffNotesApp !== 'undefined') { - DiffNotesApp.compileComponents(); + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + gl.diffNotesCompileComponents(); } if (cb) cb(); @@ -86,15 +86,13 @@ }; return SingleFileDiff; - })(); - $.fn.singleFileDiff = function(forceLoad, cb) { + $.fn.singleFileDiff = function() { return this.each(function() { - if (!$.data(this, 'singleFileDiff') || forceLoad) { - return $.data(this, 'singleFileDiff', new SingleFileDiff(this, forceLoad, cb)); + if (!$.data(this, 'singleFileDiff')) { + return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this)); } }); }; - }).call(this); diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js.es6 new file mode 100644 index 00000000000..40f67637c7c --- /dev/null +++ b/app/assets/javascripts/smart_interval.js.es6 @@ -0,0 +1,157 @@ +/* +* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable +* and controllable by a public API. +* +* */ + +(() => { + class SmartInterval { + /** + * @param { function } opts.callback Function to be called on each iteration (required) + * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially + * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this + * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this + * when the page is hidden + * @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor + * @param { boolean } opts.lazyStart Configure if timer is initialized on + * instantiation or lazily + * @param { boolean } opts.immediateExecution Configure if callback should + * be executed before the first interval. + */ + constructor(opts = {}) { + this.cfg = { + callback: opts.callback, + startingInterval: opts.startingInterval, + maxInterval: opts.maxInterval, + hiddenInterval: opts.hiddenInterval, + incrementByFactorOf: opts.incrementByFactorOf, + lazyStart: opts.lazyStart, + immediateExecution: opts.immediateExecution, + }; + + this.state = { + intervalId: null, + currentInterval: this.cfg.startingInterval, + pageVisibility: 'visible', + }; + + this.initInterval(); + } + /* public */ + + start() { + const cfg = this.cfg; + const state = this.state; + + if (cfg.immediateExecution) { + cfg.immediateExecution = false; + cfg.callback(); + } + + state.intervalId = window.setInterval(() => { + cfg.callback(); + + if (this.getCurrentInterval() === cfg.maxInterval) { + return; + } + + this.incrementInterval(); + this.resume(); + }, this.getCurrentInterval()); + } + + // cancel the existing timer, setting the currentInterval back to startingInterval + cancel() { + this.setCurrentInterval(this.cfg.startingInterval); + this.stopTimer(); + } + + onVisibilityHidden() { + if (this.cfg.hiddenInterval) { + this.setCurrentInterval(this.cfg.hiddenInterval); + this.resume(); + } else { + this.cancel(); + } + } + + // start a timer, using the existing interval + resume() { + this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped + this.start(); + } + + onVisibilityVisible() { + this.cancel(); + this.start(); + } + + destroy() { + this.cancel(); + document.removeEventListener('visibilitychange', this.handleVisibilityChange); + $(document).off('visibilitychange').off('page:before-unload'); + } + + /* private */ + + initInterval() { + const cfg = this.cfg; + + if (!cfg.lazyStart) { + this.start(); + } + + this.initVisibilityChangeHandling(); + this.initPageUnloadHandling(); + } + + initVisibilityChangeHandling() { + // cancel interval when tab no longer shown (prevents cached pages from polling) + document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); + } + + initPageUnloadHandling() { + // prevent interval continuing after page change, when kept in cache by Turbolinks + $(document).on('page:before-unload', () => this.cancel()); + } + + handleVisibilityChange(e) { + this.state.pageVisibility = e.target.visibilityState; + const intervalAction = this.isPageVisible() ? + this.onVisibilityVisible : + this.onVisibilityHidden; + + intervalAction.apply(this); + } + + getCurrentInterval() { + return this.state.currentInterval; + } + + setCurrentInterval(newInterval) { + this.state.currentInterval = newInterval; + } + + incrementInterval() { + const cfg = this.cfg; + const currentInterval = this.getCurrentInterval(); + if (cfg.hiddenInterval && !this.isPageVisible()) return; + let nextInterval = currentInterval * cfg.incrementByFactorOf; + + if (nextInterval > cfg.maxInterval) { + nextInterval = cfg.maxInterval; + } + + this.setCurrentInterval(nextInterval); + } + + isPageVisible() { return this.state.pageVisibility === 'visible'; } + + stopTimer() { + const state = this.state; + + state.intervalId = window.clearInterval(state.intervalId); + } + } + gl.SmartInterval = SmartInterval; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index 083dc23c796..cfb4ff82a73 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -1,13 +1,14 @@ -/* eslint-disable */ +/* 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 c3afc3f2246..2128007113f 100644 --- a/app/assets/javascripts/snippets_list.js.es6 +++ b/app/assets/javascripts/snippets_list.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, max-len */ + (global => { global.gl = global.gl || {}; @@ -8,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 cfd1e2204d5..531fd0e9c32 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */ +/* global Flash */ + (function() { this.Star = (function() { function Star() { @@ -24,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 new file mode 100644 index 00000000000..d8191605128 --- /dev/null +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -0,0 +1,51 @@ +(() => { +/* +* SubbableResource can be extended to provide a pubsub-style service for one-off REST +* calls. Subscribe by passing a callback or render method you will use to handle responses. + * +* */ + + class SubbableResource { + constructor(resourcePath) { + this.endpoint = resourcePath; + + // TODO: Switch to axios.create + this.resource = $.ajax; + this.subscribers = []; + } + + subscribe(callback) { + this.subscribers.push(callback); + } + + publish(newResponse) { + const responseCopy = _.extend({}, newResponse); + this.subscribers.forEach((fn) => { + fn(responseCopy); + }); + return newResponse; + } + + get(payload) { + return this.resource(payload) + .then(data => this.publish(data)); + } + + post(payload) { + return this.resource(payload) + .then(data => this.publish(data)); + } + + put(payload) { + return this.resource(payload) + .then(data => this.publish(data)); + } + + delete(payload) { + return this.resource(payload) + .then(data => this.publish(data)); + } + } + + gl.SubbableResource = SubbableResource; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js deleted file mode 100644 index f9915593657..00000000000 --- a/app/assets/javascripts/subscription.js +++ /dev/null @@ -1,52 +0,0 @@ -/* eslint-disable */ -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.Subscription = (function() { - function Subscription(container) { - this.toggleSubscription = bind(this.toggleSubscription, this); - var $container; - this.$container = $(container); - this.url = this.$container.attr('data-url'); - this.subscribe_button = this.$container.find('.js-subscribe-button'); - this.subscription_status = this.$container.find('.subscription-status'); - this.subscribe_button.unbind('click').click(this.toggleSubscription); - } - - Subscription.prototype.toggleSubscription = function(event) { - var action, btn, current_status; - btn = $(event.currentTarget); - action = btn.find('span').text(); - current_status = this.subscription_status.attr('data-status'); - btn.addClass('disabled'); - - if ($('html').hasClass('issue-boards-page')) { - this.url = this.$container.attr('data-url'); - } - - return $.post(this.url, (function(_this) { - return function() { - var status; - btn.removeClass('disabled'); - - if ($('html').hasClass('issue-boards-page')) { - Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'subscribed', !gl.issueBoards.BoardsStore.detail.issue.subscribed); - } else { - status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed'; - _this.subscription_status.attr('data-status', status); - action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe'; - btn.find('span').text(action); - _this.subscription_status.find('>div').toggleClass('hidden'); - if (btn.attr('data-original-title')) { - return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle'); - } - } - }; - })(this)); - }; - - return Subscription; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/subscription.js.es6 b/app/assets/javascripts/subscription.js.es6 new file mode 100644 index 00000000000..62d1604fe9e --- /dev/null +++ b/app/assets/javascripts/subscription.js.es6 @@ -0,0 +1,50 @@ +/* global Vue */ + +(() => { + class Subscription { + constructor(containerElm) { + this.containerElm = containerElm; + + const subscribeButton = containerElm.querySelector('.js-subscribe-button'); + if (subscribeButton) { + // remove class so we don't bind twice + subscribeButton.classList.remove('js-subscribe-button'); + subscribeButton.addEventListener('click', this.toggleSubscription.bind(this)); + } + } + + toggleSubscription(event) { + const button = event.currentTarget; + const buttonSpan = button.querySelector('span'); + if (!buttonSpan || button.classList.contains('disabled')) { + return; + } + button.classList.add('disabled'); + + const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe'; + const toggleActionUrl = this.containerElm.dataset.url; + + $.post(toggleActionUrl, () => { + button.classList.remove('disabled'); + + // hack to allow this to work with the issue boards Vue object + if (document.querySelector('html').classList.contains('issue-boards-page')) { + Vue.set( + gl.issueBoards.BoardsStore.detail.issue, + 'subscribed', + !gl.issueBoards.BoardsStore.detail.issue.subscribed, + ); + } else { + buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe'; + } + }); + } + + static bindAll(selector) { + [].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm)); + } + } + + window.gl = window.gl || {}; + window.gl.Subscription = Subscription; +})(); diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js index 2ca65cb762d..187356f0bf9 100644 --- a/app/assets/javascripts/subscription_select.js +++ b/app/assets/javascripts/subscription_select.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* 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 77ad4f30b7a..115716bff6a 100644 --- a/app/assets/javascripts/syntax_highlight.js +++ b/app/assets/javascripts/syntax_highlight.js @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len */ + // Syntax Highlighter // // Applies a syntax highlighting color scheme CSS class to any element with the @@ -11,6 +12,7 @@ (function() { $.fn.syntaxHighlight = function() { var $children; + if ($(this).hasClass('js-syntax-highlight')) { // Given the element itself, apply highlighting return $(this).addClass(gon.user_color_scheme); @@ -22,9 +24,4 @@ } } }; - - $(document).on('ready page:load', function() { - return $('.js-syntax-highlight').syntaxHighlight(); - }); - }).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 93a3d67ee9f..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,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */ +/* global Api */ + /*= require ../blob/template_selector */ ((global) => { @@ -10,7 +12,7 @@ this.issuableType = this.wrapper.data('issuable-type'); this.titleInput = $(`#${this.issuableType}_title`); - let initialQuery = { + const initialQuery = { name: this.dropdown.data('selected') }; @@ -21,7 +23,7 @@ }); $('.no-template', this.dropdown.parent()).on('click', () => { - this.currentTemplate = ''; + this.currentTemplate.content = ''; this.setInputValueToTemplateContent(); $('.dropdown-toggle-text', this.dropdown).text('Choose a template'); }); @@ -45,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 0a3890e85fe..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,5 @@ -/* eslint-disable */ +/* eslint-disable no-new, comma-dangle, class-methods-use-this, no-param-reassign */ + ((global) => { class IssuableTemplateSelectors { constructor({ $dropdowns, editor } = {}) { @@ -18,7 +19,7 @@ } initEditor() { - let editor = $('.markdown-area'); + const editor = $('.markdown-area'); // Proxy ace-editor's .setValue to jQuery's .val editor.setValue = editor.val; editor.getValue = editor.val; diff --git a/app/assets/javascripts/terminal/terminal.js.es6 b/app/assets/javascripts/terminal/terminal.js.es6 new file mode 100644 index 00000000000..6b9422b1816 --- /dev/null +++ b/app/assets/javascripts/terminal/terminal.js.es6 @@ -0,0 +1,62 @@ +/* global Terminal */ + +(() => { + class GLTerminal { + + constructor(options) { + this.options = options || {}; + + this.options.cursorBlink = options.cursorBlink || true; + this.options.screenKeys = options.screenKeys || true; + this.container = document.querySelector(options.selector); + + this.setSocketUrl(); + this.createTerminal(); + $(window).off('resize.terminal').on('resize.terminal', () => { + this.terminal.fit(); + }); + } + + setSocketUrl() { + const { protocol, hostname, port } = window.location; + const wsProtocol = protocol === 'https:' ? 'wss://' : 'ws://'; + const path = this.container.dataset.projectPath; + + this.socketUrl = `${wsProtocol}${hostname}:${port}${path}`; + } + + createTerminal() { + this.terminal = new Terminal(this.options); + this.socket = new WebSocket(this.socketUrl, ['terminal.gitlab.com']); + this.socket.binaryType = 'arraybuffer'; + + this.terminal.open(this.container); + this.socket.onopen = () => { this.runTerminal(); }; + this.socket.onerror = () => { this.handleSocketFailure(); }; + } + + runTerminal() { + const decoder = new TextDecoder('utf-8'); + const encoder = new TextEncoder('utf-8'); + + this.terminal.on('data', (data) => { + this.socket.send(encoder.encode(data)); + }); + + this.socket.addEventListener('message', (ev) => { + this.terminal.write(decoder.decode(ev.data)); + }); + + this.isTerminalInitialized = true; + this.terminal.fit(); + } + + handleSocketFailure() { + this.terminal.write('\r\nConnection failure'); + } + + } + + window.gl = window.gl || {}; + gl.Terminal = GLTerminal; +})(); diff --git a/app/assets/javascripts/terminal/terminal_bundle.js.es6 b/app/assets/javascripts/terminal/terminal_bundle.js.es6 new file mode 100644 index 00000000000..33d2c1e1a17 --- /dev/null +++ b/app/assets/javascripts/terminal/terminal_bundle.js.es6 @@ -0,0 +1,7 @@ +//= require xterm/encoding-indexes +//= require xterm/encoding +//= require xterm/xterm.js +//= require xterm/fit.js +//= require ./terminal.js + +$(() => new gl.Terminal({ selector: '#terminal' })); diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6 index 213e80825b7..ef9c0a885fb 100644 --- a/app/assets/javascripts/todos.js.es6 +++ b/app/assets/javascripts/todos.js.es6 @@ -1,6 +1,8 @@ -/* eslint-disable */ -((global) => { +/* 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); @@ -46,7 +48,7 @@ clicked: function() { return $dropdown.closest('form.filter-form').submit(); } - }) + }); } doneClicked(e) { @@ -72,7 +74,7 @@ allDoneClicked(e) { e.preventDefault(); e.stopImmediatePropagation(); - $target = $(e.currentTarget); + const $target = $(e.currentTarget); $target.disable(); return $.ajax({ type: 'POST', diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js index 70aff4b9a2f..d124ca4f88b 100644 --- a/app/assets/javascripts/tree.js +++ b/app/assets/javascripts/tree.js @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* 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() { function TreeView() { @@ -63,7 +64,5 @@ }; return TreeView; - })(); - }).call(this); diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js.es6 index 35f2b1e2b25..500b78fc5d8 100644 --- a/app/assets/javascripts/u2f/authenticate.js +++ b/app/assets/javascripts/u2f/authenticate.js.es6 @@ -1,24 +1,33 @@ -/* eslint-disable */ +/* 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 */ + // Authenticate U2F (universal 2nd factor) devices for users to authenticate with. // // State Flow #1: setup -> in_progress -> authenticated -> POST to server // State Flow #2: setup -> in_progress -> error -> setup (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + const global = window.gl || (window.gl = {}); + + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; - this.U2FAuthenticate = (function() { - function U2FAuthenticate(container, u2fParams) { + global.U2FAuthenticate = (function() { + function U2FAuthenticate(container, form, u2fParams, fallbackButton, fallbackUI) { this.container = container; this.renderNotSupported = bind(this.renderNotSupported, this); this.renderAuthenticated = bind(this.renderAuthenticated, this); this.renderError = bind(this.renderError, this); this.renderInProgress = bind(this.renderInProgress, this); - this.renderSetup = bind(this.renderSetup, this); this.renderTemplate = bind(this.renderTemplate, this); this.authenticate = bind(this.authenticate, this); this.start = bind(this.start, this); this.appId = u2fParams.app_id; this.challenge = u2fParams.challenge; + this.form = form; + this.fallbackButton = fallbackButton; + this.fallbackUI = fallbackUI; + if (this.fallbackButton) this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this)); this.signRequests = u2fParams.sign_requests.map(function(request) { // The U2F Javascript API v1.1 requires a single challenge, with // _no challenges per-request_. The U2F Javascript API v1.0 requires a @@ -37,7 +46,7 @@ U2FAuthenticate.prototype.start = function() { if (U2FUtil.isU2FSupported()) { - return this.renderSetup(); + return this.renderInProgress(); } else { return this.renderNotSupported(); } @@ -48,7 +57,7 @@ return function(response) { var error; if (response.errorCode) { - error = new U2FError(response.errorCode); + error = new U2FError(response.errorCode, 'authenticate'); return _this.renderError(error); } else { return _this.renderAuthenticated(JSON.stringify(response)); @@ -73,11 +82,6 @@ return this.container.html(template(params)); }; - U2FAuthenticate.prototype.renderSetup = function() { - this.renderTemplate('setup'); - return this.container.find('#js-login-u2f-device').on('click', this.renderInProgress); - }; - U2FAuthenticate.prototype.renderInProgress = function() { this.renderTemplate('inProgress'); return this.authenticate(); @@ -85,24 +89,30 @@ U2FAuthenticate.prototype.renderError = function(error) { this.renderTemplate('error', { - error_message: error.message() + error_message: error.message(), + error_code: error.errorCode }); - return this.container.find('#js-u2f-try-again').on('click', this.renderSetup); + return this.container.find('#js-u2f-try-again').on('click', this.renderInProgress); }; U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) { this.renderTemplate('authenticated'); - // Prefer to do this instead of interpolating using Underscore templates - // because of JSON escaping issues. - return this.container.find("#js-device-response").val(deviceResponse); + const container = this.container[0]; + container.querySelector('#js-device-response').value = deviceResponse; + container.querySelector(this.form).submit(); + this.fallbackButton.classList.add('hidden'); }; U2FAuthenticate.prototype.renderNotSupported = function() { return this.renderTemplate('notSupported'); }; - return U2FAuthenticate; + U2FAuthenticate.prototype.switchToFallbackUI = function() { + this.fallbackButton.classList.add('hidden'); + this.container[0].classList.add('hidden'); + this.fallbackUI.classList.remove('hidden'); + }; + return U2FAuthenticate; })(); - -}).call(this); +})(); diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js index aff605169e4..86b459e1866 100644 --- a/app/assets/javascripts/u2f/error.js +++ b/app/assets/javascripts/u2f/error.js @@ -1,28 +1,27 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, max-len */ +/* global u2f */ + (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.U2FError = (function() { - function U2FError(errorCode) { + function U2FError(errorCode, u2fFlowType) { this.errorCode = errorCode; this.message = bind(this.message, this); this.httpsDisabled = window.location.protocol !== 'https:'; - console.error("U2F Error Code: " + this.errorCode); + this.u2fFlowType = u2fFlowType; } U2FError.prototype.message = function() { - switch (false) { - case !(this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled): - return "U2F only works with HTTPS-enabled websites. Contact your administrator for more details."; - case this.errorCode !== u2f.ErrorCodes.DEVICE_INELIGIBLE: - return "This device has already been registered with us."; - default: - return "There was a problem communicating with your device."; + if (this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) { + return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.'; + } else if (this.errorCode === u2f.ErrorCodes.DEVICE_INELIGIBLE) { + if (this.u2fFlowType === 'authenticate') return 'This device has not been registered with us.'; + if (this.u2fFlowType === 'register') return 'This device has already been registered with us.'; } + return "There was a problem communicating with your device."; }; return U2FError; - })(); - }).call(this); diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index 22fbf9f3a91..69d1ff3a39e 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -1,10 +1,14 @@ -/* eslint-disable */ +/* 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 */ + // Register U2F (universal 2nd factor) devices for users to authenticate with. // // 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) { @@ -35,7 +39,7 @@ return function(response) { var error; if (response.errorCode) { - error = new U2FError(response.errorCode); + error = new U2FError(response.errorCode, 'register'); return _this.renderError(error); } else { return _this.renderRegistered(JSON.stringify(response)); @@ -72,7 +76,8 @@ U2FRegister.prototype.renderError = function(error) { this.renderTemplate('error', { - error_message: error.message() + error_message: error.message(), + error_code: error.errorCode }); return this.container.find('#js-u2f-try-again').on('click', this.renderSetup); }; @@ -89,7 +94,5 @@ }; return U2FRegister; - })(); - }).call(this); diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js index 2eab2d5ae23..34e88220b12 100644 --- a/app/assets/javascripts/u2f/util.js +++ b/app/assets/javascripts/u2f/util.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* 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 5e869e99fdb..059e6c628b3 100644 --- a/app/assets/javascripts/user.js.es6 +++ b/app/assets/javascripts/user.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign */ +/* global Cookies */ + ((global) => { global.User = class { constructor({ action }) { @@ -28,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 2b310da319c..313fb17aee8 100644 --- a/app/assets/javascripts/user_tabs.js.es6 +++ b/app/assets/javascripts/user_tabs.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* 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 @@ -106,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); } @@ -134,7 +135,7 @@ content on the Users#show page. } const $calendarWrap = this.$parentEl.find('.user-calendar'); $calendarWrap.load($calendarWrap.data('href')); - new Activities(); + new gl.Activities(); return this.loaded['activity'] = true; } @@ -144,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/username_validator.js.es6 b/app/assets/javascripts/username_validator.js.es6 index c4dde575c6e..137cefa3b8e 100644 --- a/app/assets/javascripts/username_validator.js.es6 +++ b/app/assets/javascripts/username_validator.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */ + ((global) => { const debounceTimeoutDuration = 1000; const invalidInputClass = 'gl-field-error-outline'; @@ -77,7 +78,7 @@ this.renderState(); return $.ajax({ type: 'GET', - url: `/users/${username}/exists`, + url: `${gon.relative_url_root}/users/${username}/exists`, dataType: 'json', success: (res) => this.setAvailabilityState(res.exists) }); diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index 0ec878e7e60..7ffc546ffc1 100644 --- a/app/assets/javascripts/users/calendar.js +++ b/app/assets/javascripts/users/calendar.js @@ -1,6 +1,9 @@ -/* eslint-disable */ +/* 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) { @@ -17,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); @@ -25,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); @@ -36,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]; @@ -71,7 +74,7 @@ } return extraWidthPadding; - } + }; Calendar.prototype.renderSvg = function(group) { var width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group); @@ -206,6 +209,7 @@ } }); } else { + this.currentSelectedDate = ''; return $('.user-calendar-activities').html(''); } }; @@ -217,7 +221,5 @@ }; return Calendar; - })(); - }).call(this); diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js index 22bee0f6187..f50802bdf2e 100644 --- a/app/assets/javascripts/users/users_bundle.js +++ b/app/assets/javascripts/users/users_bundle.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* eslint-disable func-names, space-before-function-paren */ /*= require_tree . */ diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 7a2221dbaf5..77d2764cdf0 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -1,6 +1,10 @@ -/* eslint-disable */ +/* 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() { @@ -112,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); @@ -274,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); @@ -367,7 +371,7 @@ }; UsersSelect.prototype.user = function(user_id, callback) { - if(!/^\d+$/.test(user_id)) { + if (!/^\d+$/.test(user_id)) { return false; } @@ -417,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_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6 new file mode 100644 index 00000000000..62a22e39a3b --- /dev/null +++ b/app/assets/javascripts/vue_common_component/commit.js.es6 @@ -0,0 +1,163 @@ +/*= require vue */ +/* global Vue */ +(() => { + window.gl = window.gl || {}; + + window.gl.CommitComponent = Vue.component('commit-component', { + + props: { + /** + * Indicates the existance of a tag. + * Used to render the correct icon, if true will render `fa-tag` icon, + * if false will render `fa-code-fork` icon. + */ + tag: { + type: Boolean, + required: false, + default: false, + }, + + /** + * If provided is used to render the branch name and url. + * Should contain the following properties: + * name + * ref_url + */ + commitRef: { + type: Object, + required: false, + default: () => ({}), + }, + + /** + * Used to link to the commit sha. + */ + commitUrl: { + type: String, + required: false, + default: '', + }, + + /** + * Used to show the commit short sha that links to the commit url. + */ + shortSha: { + type: String, + required: false, + default: '', + }, + + /** + * If provided shows the commit tile. + */ + title: { + type: String, + required: false, + default: '', + }, + + /** + * If provided renders information about the author of the commit. + * When provided should include: + * `avatar_url` to render the avatar icon + * `web_url` to link to user profile + * `username` to render alt and title tags + */ + author: { + type: Object, + required: false, + default: () => ({}), + }, + + commitIconSvg: { + type: String, + required: false, + }, + }, + + computed: { + /** + * Used to verify if all the properties needed to render the commit + * ref section were provided. + * + * TODO: Improve this! Use lodash _.has when we have it. + * + * @returns {Boolean} + */ + hasCommitRef() { + return this.commitRef && this.commitRef.name && this.commitRef.ref_url; + }, + + /** + * Used to verify if all the properties needed to render the commit + * author section were provided. + * + * TODO: Improve this! Use lodash _.has when we have it. + * + * @returns {Boolean} + */ + hasAuthor() { + return this.author && + this.author.avatar_url && + this.author.web_url && + this.author.username; + }, + + /** + * If information about the author is provided will return a string + * to be rendered as the alt attribute of the img tag. + * + * @returns {String} + */ + userImageAltDescription() { + return this.author && + this.author.username ? `${this.author.username}'s avatar` : null; + }, + }, + + template: ` + <div class="branch-commit"> + + <div v-if="hasCommitRef" class="icon-container"> + <i v-if="tag" class="fa fa-tag"></i> + <i v-if="!tag" class="fa fa-code-fork"></i> + </div> + + <a v-if="hasCommitRef" + class="monospace branch-name" + :href="commitRef.ref_url"> + {{commitRef.name}} + </a> + + <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div> + + <a class="commit-id monospace" + :href="commitUrl"> + {{shortSha}} + </a> + + <p class="commit-title"> + <span v-if="title"> + <a v-if="hasAuthor" + class="avatar-image-container" + :href="author.web_url"> + <img + class="avatar has-tooltip s20" + :src="author.avatar_url" + :alt="userImageAltDescription" + :title="author.username" /> + </a> + + <a class="commit-row-message" + :href="commitUrl"> + {{title}} + </a> + </span> + <span v-else> + Cant find HEAD commit for this branch + </span> + </p> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/vue_pagination/index.js.es6 b/app/assets/javascripts/vue_pagination/index.js.es6 new file mode 100644 index 00000000000..605824fa939 --- /dev/null +++ b/app/assets/javascripts/vue_pagination/index.js.es6 @@ -0,0 +1,148 @@ +/* global Vue, gl */ +/* eslint-disable no-param-reassign, no-plusplus */ + +((gl) => { + const PAGINATION_UI_BUTTON_LIMIT = 4; + const UI_LIMIT = 6; + const SPREAD = '...'; + const PREV = 'Prev'; + const NEXT = 'Next'; + const FIRST = '<< First'; + const LAST = 'Last >>'; + + gl.VueGlPagination = Vue.extend({ + props: { + + /** + This function will take the information given by the pagination component + And make a new Turbolinks call + + Here is an example `change` method: + + change(pagenum, apiScope) { + Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); + }, + */ + + change: { + type: Function, + required: true, + }, + + /** + pageInfo will come from the headers of the API call + in the `.then` clause of the VueResource API call + there should be a function that contructs the pageInfo for this component + + This is an example: + + const pageInfo = headers => ({ + perPage: +headers['X-Per-Page'], + page: +headers['X-Page'], + total: +headers['X-Total'], + totalPages: +headers['X-Total-Pages'], + nextPage: +headers['X-Next-Page'], + previousPage: +headers['X-Prev-Page'], + }); + */ + + pageInfo: { + type: Object, + required: true, + }, + }, + methods: { + changePage(e) { + let apiScope = gl.utils.getParameterByName('scope'); + + if (!apiScope) apiScope = 'all'; + + const text = e.target.innerText; + const { totalPages, nextPage, previousPage } = this.pageInfo; + + switch (text) { + case SPREAD: + break; + case LAST: + this.change(totalPages, apiScope); + break; + case NEXT: + this.change(nextPage, apiScope); + break; + case PREV: + this.change(previousPage, apiScope); + break; + case FIRST: + this.change(1, apiScope); + break; + default: + this.change(+text, apiScope); + break; + } + }, + }, + computed: { + prev() { + return this.pageInfo.previousPage; + }, + next() { + return this.pageInfo.nextPage; + }, + getItems() { + const total = this.pageInfo.totalPages; + const page = this.pageInfo.page; + const items = []; + + if (page > 1) items.push({ title: FIRST }); + + if (page > 1) { + items.push({ title: PREV, prev: true }); + } else { + items.push({ title: PREV, disabled: true, prev: true }); + } + + if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true }); + + const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); + const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); + + for (let i = start; i <= end; i++) { + const isActive = i === page; + items.push({ title: i, active: isActive, page: true }); + } + + if (total - page > PAGINATION_UI_BUTTON_LIMIT) { + items.push({ title: SPREAD, separator: true, page: true }); + } + + if (page === total) { + items.push({ title: NEXT, disabled: true, next: true }); + } else if (total - page >= 1) { + items.push({ title: NEXT, next: true }); + } + + if (total - page >= 1) items.push({ title: LAST, last: true }); + + return items; + }, + }, + template: ` + <div class="gl-pagination"> + <ul class="pagination clearfix"> + <li v-for='item in getItems' + :class='{ + page: item.page, + prev: item.prev, + next: item.next, + separator: item.separator, + active: item.active, + disabled: item.disabled + }' + > + <a @click="changePage($event)">{{item.title}}</a> + </li> + </ul> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 new file mode 100644 index 00000000000..edd01f17a97 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6 @@ -0,0 +1,42 @@ +/* global Vue, VueResource, gl */ +/*= require vue_common_component/commit */ +/*= require vue_pagination/index */ +/*= require vue-resource +/*= require boards/vue_resource_interceptor */ +/*= require ./status.js.es6 */ +/*= require ./store.js.es6 */ +/*= require ./pipeline_url.js.es6 */ +/*= require ./stage.js.es6 */ +/*= require ./stages.js.es6 */ +/*= require ./pipeline_actions.js.es6 */ +/*= require ./time_ago.js.es6 */ +/*= require ./pipelines.js.es6 */ + +(() => { + const project = document.querySelector('.pipelines'); + const entry = document.querySelector('.vue-pipelines-index'); + const svgs = document.querySelector('.pipeline-svgs'); + + Vue.use(VueResource); + + if (!entry) return null; + return new Vue({ + el: entry, + data: { + scope: project.dataset.url, + store: new gl.PipelineStore(), + svgs: svgs.dataset, + }, + components: { + 'vue-pipelines': gl.VuePipelines, + }, + template: ` + <vue-pipelines + :scope='scope' + :store='store' + :svgs='svgs' + > + </vue-pipelines> + `, + }); +})(); diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 new file mode 100644 index 00000000000..ad5cb30cc42 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -0,0 +1,99 @@ +/* global Vue, Flash, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VuePipelineActions = Vue.extend({ + props: ['pipeline', 'svgs'], + computed: { + actions() { + return this.pipeline.details.manual_actions.length > 0; + }, + artifacts() { + return this.pipeline.details.artifacts.length > 0; + }, + }, + methods: { + download(name) { + return `Download ${name} artifacts`; + }, + }, + template: ` + <td class="pipeline-actions hidden-xs"> + <div class="controls pull-right"> + <div class="btn-group inline"> + <div class="btn-group"> + <a + v-if='actions' + class="dropdown-toggle btn btn-default js-pipeline-dropdown-manual-actions" + data-toggle="dropdown" + title="Manual build" + alt="Manual Build" + > + <span v-html='svgs.iconPlay'></span> + <i class="fa fa-caret-down"></i> + </a> + <ul class="dropdown-menu dropdown-menu-align-right"> + <li v-for='action in pipeline.details.manual_actions'> + <a + rel="nofollow" + data-method="post" + :href='action.path' + title="Manual build" + > + <span v-html='svgs.iconPlay'></span> + <span title="Manual build">{{action.name}}</span> + </a> + </li> + </ul> + </div> + <div class="btn-group"> + <a + v-if='artifacts' + class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download" + data-toggle="dropdown" + type="button" + > + <i class="fa fa-download"></i> + <i class="fa fa-caret-down"></i> + </a> + <ul class="dropdown-menu dropdown-menu-align-right"> + <li v-for='artifact in pipeline.details.artifacts'> + <a + rel="nofollow" + :href='artifact.path' + > + <i class="fa fa-download"></i> + <span>{{download(artifact.name)}}</span> + </a> + </li> + </ul> + </div> + </div> + <div class="cancel-retry-btns inline"> + <a + v-if='pipeline.flags.retryable' + class="btn has-tooltip" + title="Retry" + rel="nofollow" + data-method="post" + :href='pipeline.retry_path' + > + <i class="fa fa-repeat"></i> + </a> + <a + v-if='pipeline.flags.cancelable' + class="btn btn-remove has-tooltip" + title="Cancel" + rel="nofollow" + data-method="post" + :href='pipeline.cancel_path' + data-original-title="Cancel" + > + <i class="fa fa-remove"></i> + </a> + </div> + </div> + </td> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 new file mode 100644 index 00000000000..ae5649f0519 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 @@ -0,0 +1,63 @@ +/* global Vue, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VuePipelineUrl = Vue.extend({ + props: [ + 'pipeline', + ], + computed: { + user() { + return !!this.pipeline.user; + }, + }, + template: ` + <td> + <a :href='pipeline.path'> + <span class="pipeline-id">#{{pipeline.id}}</span> + </a> + <span>by</span> + <a + v-if='user' + :href='pipeline.user.web_url' + > + <img + v-if='user' + class="avatar has-tooltip s20 " + :title='pipeline.user.name' + data-container="body" + :src='pipeline.user.avatar_url' + > + </a> + <span + v-if='!user' + class="api monospace" + > + API + </span> + <span + v-if='pipeline.flags.latest' + class="label label-success has-tooltip" + title="Latest pipeline for this branch" + data-original-title="Latest pipeline for this branch" + > + latest + </span> + <span + v-if='pipeline.flags.yaml_errors' + class="label label-danger has-tooltip" + :title='pipeline.yaml_errors' + :data-original-title='pipeline.yaml_errors' + > + yaml invalid + </span> + <span + v-if='pipeline.flags.stuck' + class="label label-warning" + > + stuck + </span> + </td> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 new file mode 100644 index 00000000000..b2ed05503c9 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -0,0 +1,131 @@ +/* global Vue, Turbolinks, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VuePipelines = Vue.extend({ + components: { + runningPipeline: gl.VueRunningPipeline, + pipelineActions: gl.VuePipelineActions, + stages: gl.VueStages, + commit: gl.CommitComponent, + pipelineUrl: gl.VuePipelineUrl, + pipelineHead: gl.VuePipelineHead, + glPagination: gl.VueGlPagination, + statusScope: gl.VueStatusScope, + timeAgo: gl.VueTimeAgo, + }, + data() { + return { + pipelines: [], + timeLoopInterval: '', + intervalId: '', + apiScope: 'all', + pageInfo: {}, + pagenum: 1, + count: { all: 0, running_or_pending: 0 }, + pageRequest: false, + }; + }, + props: ['scope', 'store', 'svgs'], + created() { + const pagenum = gl.utils.getParameterByName('p'); + const scope = gl.utils.getParameterByName('scope'); + if (pagenum) this.pagenum = pagenum; + if (scope) this.apiScope = scope; + this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope); + }, + methods: { + change(pagenum, apiScope) { + Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); + }, + author(pipeline) { + if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; + if (pipeline.commit.author) return pipeline.commit.author; + return { + avatar_url: pipeline.commit.author_gravatar_url, + web_url: `mailto:${pipeline.commit.author_email}`, + username: pipeline.commit.author_name, + }; + }, + ref(pipeline) { + const { ref } = pipeline; + return { name: ref.name, tag: ref.tag, ref_url: ref.path }; + }, + commitTitle(pipeline) { + return pipeline.commit ? pipeline.commit.title : ''; + }, + commitSha(pipeline) { + return pipeline.commit ? pipeline.commit.short_id : ''; + }, + commitUrl(pipeline) { + return pipeline.commit ? pipeline.commit.commit_path : ''; + }, + match(string) { + return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); + }, + }, + template: ` + <div> + <div class="pipelines realtime-loading" v-if='pipelines.length < 1'> + <i class="fa fa-spinner fa-spin"></i> + </div> + <div class="table-holder" v-if='pipelines.length'> + <table class="table ci-table"> + <thead> + <tr> + <th class="pipeline-status">Status</th> + <th class="pipeline-info">Pipeline</th> + <th class="pipeline-commit">Commit</th> + <th class="pipeline-stages">Stages</th> + <th class="pipeline-date"></th> + <th class="pipeline-actions hidden-xs"></th> + </tr> + </thead> + <tbody> + <tr class="commit" v-for='pipeline in pipelines'> + <status-scope + :pipeline='pipeline' + :match='match' + :svgs='svgs' + > + </status-scope> + <pipeline-url :pipeline='pipeline'></pipeline-url> + <td> + <commit + :commit-icon-svg='svgs.commitIconSvg' + :author='author(pipeline)' + :tag="pipeline.ref.tag" + :title='commitTitle(pipeline)' + :commit-ref='ref(pipeline)' + :short-sha='commitSha(pipeline)' + :commit-url='commitUrl(pipeline)' + > + </commit> + </td> + <stages + :pipeline='pipeline' + :svgs='svgs' + :match='match' + > + </stages> + <time-ago :pipeline='pipeline' :svgs='svgs'></time-ago> + <pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions> + </tr> + </tbody> + </table> + </div> + <div class="pipelines realtime-loading" v-if='pageRequest'> + <i class="fa fa-spinner fa-spin"></i> + </div> + <gl-pagination + v-if='pageInfo.total > pageInfo.perPage' + :pagenum='pagenum' + :change='change' + :count='count.all' + :pageInfo='pageInfo' + > + </gl-pagination> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 new file mode 100644 index 00000000000..4e85f16ebc5 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -0,0 +1,102 @@ +/* global Vue, Flash, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VueStage = Vue.extend({ + data() { + return { + builds: '', + spinner: '<span class="fa fa-spinner fa-spin"></span>', + }; + }, + props: { + stage: { + type: Object, + required: true, + }, + svgs: { + type: DOMStringMap, + required: true, + }, + match: { + type: Function, + required: true, + }, + }, + methods: { + fetchBuilds(e) { + const areaExpanded = e.currentTarget.attributes['aria-expanded']; + + if (areaExpanded && (areaExpanded.textContent === 'true')) return null; + + return this.$http.get(this.stage.dropdown_path) + .then((response) => { + this.builds = JSON.parse(response.body).html; + }, () => { + const flash = new Flash('Something went wrong on our end.'); + return flash; + }); + }, + keepGraph(e) { + const { target } = e; + + if (target.className.indexOf('js-ci-action-icon') >= 0) return null; + + if ( + target.parentElement && + (target.parentElement.className.indexOf('js-ci-action-icon') >= 0) + ) return null; + + return e.stopPropagation(); + }, + }, + computed: { + buildsOrSpinner() { + return this.builds ? this.builds : this.spinner; + }, + dropdownClass() { + if (this.builds) return 'js-builds-dropdown-container'; + return 'js-builds-dropdown-loading builds-dropdown-loading'; + }, + buildStatus() { + return `Build: ${this.stage.status.label}`; + }, + tooltip() { + return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`; + }, + svg() { + const { icon } = this.stage.status; + const stageIcon = icon.replace(/icon/i, 'stage_icon'); + return this.svgs[this.match(stageIcon)]; + }, + triggerButtonClass() { + return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`; + }, + }, + template: ` + <div> + <button + @click='fetchBuilds($event)' + :class="triggerButtonClass" + :title='stage.title' + data-placement="top" + data-toggle="dropdown" + type="button" + > + <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 + @click='keepGraph($event)' + :class="dropdownClass" + class="js-builds-dropdown-list scrollable-menu" + v-html="buildsOrSpinner" + > + </div> + </ul> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/stages.js.es6 b/app/assets/javascripts/vue_pipelines_index/stages.js.es6 new file mode 100644 index 00000000000..cb176b3f0c6 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/stages.js.es6 @@ -0,0 +1,21 @@ +/* global Vue, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VueStages = Vue.extend({ + components: { + 'vue-stage': gl.VueStage, + }, + props: ['pipeline', 'svgs', 'match'], + template: ` + <td class="stage-cell"> + <div + class="stage-container dropdown js-mini-pipeline-graph" + v-for='stage in pipeline.details.stages' + > + <vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage> + </div> + </td> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/status.js.es6 b/app/assets/javascripts/vue_pipelines_index/status.js.es6 new file mode 100644 index 00000000000..05175082704 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/status.js.es6 @@ -0,0 +1,34 @@ +/* global Vue, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VueStatusScope = Vue.extend({ + props: [ + 'pipeline', 'svgs', 'match', + ], + computed: { + cssClasses() { + const cssObject = { 'ci-status': true }; + cssObject[`ci-${this.pipeline.details.status.group}`] = true; + return cssObject; + }, + svg() { + return this.svgs[this.match(this.pipeline.details.status.icon)]; + }, + detailsPath() { + const { status } = this.pipeline.details; + return status.has_details ? status.details_path : false; + }, + }, + template: ` + <td class="commit-link"> + <a + :class='cssClasses' + :href='detailsPath' + v-html='svg + pipeline.details.status.text' + > + </a> + </td> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6 new file mode 100644 index 00000000000..1982142853a --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6 @@ -0,0 +1,69 @@ +/* global gl, Flash */ +/* eslint-disable no-param-reassign, no-underscore-dangle */ +/*= require vue_realtime_listener/index.js */ + +((gl) => { + const pageValues = (headers) => { + const normalizedHeaders = {}; + + Object.keys(headers).forEach((e) => { + normalizedHeaders[e.toUpperCase()] = headers[e]; + }); + + const paginationInfo = { + perPage: +normalizedHeaders['X-PER-PAGE'], + page: +normalizedHeaders['X-PAGE'], + total: +normalizedHeaders['X-TOTAL'], + totalPages: +normalizedHeaders['X-TOTAL-PAGES'], + nextPage: +normalizedHeaders['X-NEXT-PAGE'], + previousPage: +normalizedHeaders['X-PREV-PAGE'], + }; + + return paginationInfo; + }; + + gl.PipelineStore = class { + fetchDataLoop(Vue, pageNum, url, apiScope) { + const updatePipelineNums = (count) => { + const { all } = count; + const running = count.running_or_pending; + document.querySelector('.js-totalbuilds-count').innerHTML = all; + document.querySelector('.js-running-count').innerHTML = running; + }; + + const goFetch = () => + this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`) + .then((response) => { + const pageInfo = pageValues(response.headers); + this.pageInfo = Object.assign({}, this.pageInfo, pageInfo); + + const res = JSON.parse(response.body); + this.count = Object.assign({}, this.count, res.count); + this.pipelines = Object.assign([], this.pipelines, res.pipelines); + + updatePipelineNums(this.count); + this.pageRequest = false; + }, () => { + this.pageRequest = false; + return new Flash('Something went wrong on our end.'); + }); + + goFetch(); + + const startTimeLoops = () => { + this.timeLoopInterval = setInterval(() => { + this.$children + .filter(e => e.$options._componentTag === 'time-ago') + .forEach(e => e.changeTime()); + }, 10000); + }; + + startTimeLoops(); + + const removeIntervals = () => clearInterval(this.timeLoopInterval); + const startIntervals = () => startTimeLoops(); + + gl.VueRealtimeListener(removeIntervals, startIntervals); + } + }; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 new file mode 100644 index 00000000000..655110feba1 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 @@ -0,0 +1,73 @@ +/* global Vue, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VueTimeAgo = Vue.extend({ + data() { + return { + currentTime: new Date(), + }; + }, + props: ['pipeline', 'svgs'], + computed: { + timeAgo() { + return gl.utils.getTimeago(); + }, + localTimeFinished() { + return gl.utils.formatDate(this.pipeline.details.finished_at); + }, + timeStopped() { + const changeTime = this.currentTime; + const options = { + weekday: 'long', + year: 'numeric', + month: 'short', + day: 'numeric', + }; + options.timeZoneName = 'short'; + const finished = this.pipeline.details.finished_at; + if (!finished && changeTime) return false; + return ({ words: this.timeAgo.format(finished) }); + }, + duration() { + const { duration } = this.pipeline.details; + const date = new Date(duration * 1000); + + let hh = date.getUTCHours(); + let mm = date.getUTCMinutes(); + let ss = date.getSeconds(); + + if (hh < 10) hh = `0${hh}`; + if (mm < 10) mm = `0${mm}`; + if (ss < 10) ss = `0${ss}`; + + if (duration !== null) return `${hh}:${mm}:${ss}`; + return false; + }, + }, + methods: { + changeTime() { + this.currentTime = new Date(); + }, + }, + template: ` + <td> + <p class="duration" v-if='duration'> + <span v-html='svgs.iconTimer'></span> + {{duration}} + </p> + <p class="finished-at" v-if='timeStopped'> + <i class="fa fa-calendar"></i> + <time + data-toggle="tooltip" + data-placement="top" + data-container="body" + :data-original-title='localTimeFinished' + > + {{timeStopped.words}} + </time> + </p> + </td> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_realtime_listener/index.js.es6 b/app/assets/javascripts/vue_realtime_listener/index.js.es6 new file mode 100644 index 00000000000..23cac1466d2 --- /dev/null +++ b/app/assets/javascripts/vue_realtime_listener/index.js.es6 @@ -0,0 +1,18 @@ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VueRealtimeListener = (removeIntervals, startIntervals) => { + const removeAll = () => { + removeIntervals(); + window.removeEventListener('beforeunload', removeIntervals); + window.removeEventListener('focus', startIntervals); + window.removeEventListener('blur', removeIntervals); + document.removeEventListener('page:fetch', removeAll); + }; + + window.addEventListener('beforeunload', removeIntervals); + window.addEventListener('focus', startIntervals); + window.addEventListener('blur', removeIntervals); + document.addEventListener('page:fetch', removeAll); + }; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js deleted file mode 100644 index ad9b842db3c..00000000000 --- a/app/assets/javascripts/wikis.js +++ /dev/null @@ -1,38 +0,0 @@ -/* eslint-disable */ - -/*= require latinise */ - -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.Wikis = (function() { - function Wikis() { - this.slugify = bind(this.slugify, this); - $('.new-wiki-page').on('submit', (function(_this) { - return function(e) { - var field, path, slug; - $('[data-error~=slug]').addClass('hidden'); - field = $('#new_wiki_path'); - slug = _this.slugify(field.val()); - if (slug.length > 0) { - path = field.attr('data-wikis-path'); - location.href = path + '/' + slug; - return e.preventDefault(); - } - }; - })(this)); - } - - Wikis.prototype.dasherize = function(value) { - return value.replace(/[_\s]+/g, '-'); - }; - - Wikis.prototype.slugify = function(value) { - return this.dasherize(value.trim().toLowerCase().latinise()); - }; - - return Wikis; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6 new file mode 100644 index 00000000000..ecff5fd5bf4 --- /dev/null +++ b/app/assets/javascripts/wikis.js.es6 @@ -0,0 +1,73 @@ +/* eslint-disable no-param-reassign */ +/* global Breakpoints */ + +/*= require latinise */ +/*= require breakpoints */ +/*= require jquery.nicescroll */ + +((global) => { + const dasherize = str => str.replace(/[_\s]+/g, '-'); + const slugify = str => dasherize(str.trim().toLowerCase().latinise()); + + class Wikis { + constructor() { + this.bp = Breakpoints.get(); + this.sidebarEl = document.querySelector('.js-wiki-sidebar'); + this.sidebarExpanded = false; + $(this.sidebarEl).niceScroll(); + + const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle'); + for (let i = 0; i < sidebarToggles.length; i += 1) { + sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e)); + } + + this.newWikiForm = document.querySelector('form.new-wiki-page'); + if (this.newWikiForm) { + this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e)); + } + + window.addEventListener('resize', () => this.renderSidebar()); + this.renderSidebar(); + } + + handleNewWikiSubmit(e) { + if (!this.newWikiForm) return; + + const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); + const slug = slugify(slugInput.value); + + if (slug.length > 0) { + const wikisPath = slugInput.getAttribute('data-wikis-path'); + window.location.href = `${wikisPath}/${slug}`; + e.preventDefault(); + } + } + + handleToggleSidebar(e) { + e.preventDefault(); + this.sidebarExpanded = !this.sidebarExpanded; + this.renderSidebar(); + } + + sidebarCanCollapse() { + const bootstrapBreakpoint = this.bp.getBreakpointSize(); + return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; + } + + renderSidebar() { + if (!this.sidebarEl) return; + const { classList } = this.sidebarEl; + if (this.sidebarExpanded || !this.sidebarCanCollapse()) { + if (!classList.contains('right-sidebar-expanded')) { + classList.remove('right-sidebar-collapsed'); + classList.add('right-sidebar-expanded'); + } + } else if (classList.contains('right-sidebar-expanded')) { + classList.add('right-sidebar-collapsed'); + classList.remove('right-sidebar-expanded'); + } + } + } + + global.Wikis = Wikis; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index fa124e7052d..a8b7be7ad06 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* 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 */ + // Zen Mode (full screen) textarea // /*= provides zen_mode:enter */ @@ -90,7 +93,5 @@ }; return ZenMode; - })(); - }).call(this); diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index d5cca1b10fb..3cf49f4ff1b 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -1,4 +1,3 @@ -@import "framework/fonts"; @import "framework/variables"; @import "framework/mixins"; @import 'framework/tw_bootstrap_variables'; @@ -7,8 +6,10 @@ @import "framework/animations.scss"; @import "framework/avatar.scss"; +@import "framework/asciidoctor.scss"; @import "framework/blocks.scss"; @import "framework/buttons.scss"; +@import "framework/badges.scss"; @import "framework/calendar.scss"; @import "framework/callout.scss"; @import "framework/common.scss"; @@ -39,3 +40,11 @@ @import "framework/typography.scss"; @import "framework/zen.scss"; @import "framework/blank"; +@import "framework/wells.scss"; +@import "framework/page-header.scss"; +@import "framework/awards.scss"; +@import "framework/images.scss"; +@import "framework/broadcast-messages"; +@import "framework/emojis.scss"; +@import "framework/icons.scss"; +@import "framework/snippets.scss"; diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index f1d36efb3de..8d38fc78a19 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -50,3 +50,77 @@ .pulse { @include webkit-prefix(animation-name, pulse); } + +/* +* General hover animations +*/ + + +// Sass multiple transitions mixin | https://gist.github.com/tobiasahlin/7a421fb9306a4f518aab +// Usage: @include transition(width, height 0.3s ease-in-out); +// Output: -webkit-transition(width 0.2s, height 0.3s ease-in-out); +// transition(width 0.2s, height 0.3s ease-in-out); +// +// Pass in any number of transitions +@mixin transition($transitions...) { + $unfoldedTransitions: (); + @each $transition in $transitions { + $unfoldedTransitions: append($unfoldedTransitions, unfoldTransition($transition), comma); + } + + transition: $unfoldedTransitions; +} + +@function unfoldTransition ($transition) { + // Default values + $property: all; + $duration: $general-hover-transition-duration; + $easing: $general-hover-transition-curve; // Browser default is ease, which is what we want + $delay: null; // Browser default is 0, which is what we want + $defaultProperties: ($property, $duration, $easing, $delay); + + // Grab transition properties if they exist + $unfoldedTransition: (); + @for $i from 1 through length($defaultProperties) { + $p: null; + @if $i <= length($transition) { + $p: nth($transition, $i); + } @else { + $p: nth($defaultProperties, $i); + } + $unfoldedTransition: append($unfoldedTransition, $p); + } + + @return $unfoldedTransition; +} + +.btn, +.side-nav-toggle { + @include transition(background-color, border-color, color, box-shadow); +} + +.dropdown-menu-toggle, +.avatar-circle, +.header-user-avatar { + @include transition(border-color); +} + +.note-action-button .link-highlight, +.toolbar-btn, +.dropdown-toggle-caret, +.fa:not(.fa-bell) { + @include transition(color); +} + +a { + @include transition(background-color, color, border); +} + +.tree-table td, +.well-list > li { + @include transition(background-color, border-color); +} + +.stage-nav-item { + @include transition(background-color, box-shadow); +} diff --git a/app/assets/stylesheets/framework/asciidoctor.scss b/app/assets/stylesheets/framework/asciidoctor.scss new file mode 100644 index 00000000000..62493c32833 --- /dev/null +++ b/app/assets/stylesheets/framework/asciidoctor.scss @@ -0,0 +1,27 @@ +.admonitionblock td.icon { + width: 1%; + + [class^="fa icon-"] { + @extend .fa-2x; + } + + .icon-note { + @extend .fa-thumb-tack; + } + + .icon-tip { + @extend .fa-lightbulb-o; + } + + .icon-warning { + @extend .fa-exclamation-triangle; + } + + .icon-caution { + @extend .fa-fire; + } + + .icon-important { + @extend .fa-exclamation-circle; + } +} diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 202ed5ae8fe..8392b98f0a7 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -8,7 +8,7 @@ float: left; margin-right: 15px; border-radius: $avatar_radius; - border: 1px solid rgba(0, 0, 0, .1); + border: 1px solid $avatar-border; &.s16 { @include avatar-size(16px, 6px); } &.s20 { @include avatar-size(20px, 7px); } &.s24 { @include avatar-size(24px, 8px); } @@ -34,6 +34,7 @@ &.avatar-inline { float: none; + display: inline-block; margin-left: 4px; margin-bottom: 2px; @@ -41,10 +42,20 @@ &.s24 { margin-right: 4px; } } + &.center { + font-size: 14px; + line-height: 1.8em; + text-align: center; + } + &.avatar-tile { border-radius: 0; border: none; } + + &:not([href]):hover { + border-color: rgba($avatar-border, .2); + } } .identicon { @@ -57,7 +68,7 @@ &.s32 { font-size: 20px; line-height: 30px; } &.s40 { font-size: 16px; line-height: 38px; } &.s60 { font-size: 32px; line-height: 58px; } - &.s70 { font-size: 34px; line-height: 68px; } + &.s70 { font-size: 34px; line-height: 70px; } &.s90 { font-size: 36px; line-height: 88px; } &.s110 { font-size: 40px; line-height: 108px; font-weight: 300; } &.s140 { font-size: 72px; line-height: 138px; } @@ -73,6 +84,7 @@ border-radius: 0; border: none; height: auto; + width: 100%; margin: 0; align-self: center; } diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/framework/awards.scss index 486ad16ea26..49907417e26 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -1,7 +1,7 @@ .awards { .emoji-icon { - width: 19px; - height: 19px; + width: 20px; + height: 20px; } } @@ -12,10 +12,10 @@ z-index: 9; width: 300px; font-size: 14px; - background-color: $award-emoji-menu-bg; - border: 1px solid $award-emoji-menu-border; + background-color: $white-light; + border: 1px solid $border-white-light; border-radius: $border-radius-base; - box-shadow: 0 6px 12px rgba(0,0,0,.175); + box-shadow: 0 6px 12px $award-emoji-menu-shadow; pointer-events: none; opacity: 0; transform: scale(.2); @@ -97,8 +97,20 @@ padding: 5px 6px; outline: 0; - &:hover, + &.disabled { + cursor: default; + + &:hover, + &:focus, + &:active { + background-color: $white-light; + border-color: $border-color; + box-shadow: none; + } + } + &.active, + &:hover, &:active { background-color: $row-hover; border-color: $row-hover-border; @@ -127,7 +139,7 @@ .award-control-icon { float: left; margin-right: 5px; - font-size: 19px; + font-size: 18px; } .award-control-icon-loading { @@ -135,6 +147,7 @@ } .award-control-icon { - color: $award-emoji-new-btn-icon-color; + color: $border-gray-normal; + margin-top: 1px; } } diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss new file mode 100644 index 00000000000..47a8f44c709 --- /dev/null +++ b/app/assets/stylesheets/framework/badges.scss @@ -0,0 +1,6 @@ +.badge { + font-weight: normal; + background-color: $badge-bg; + color: $badge-color; + vertical-align: baseline; +} diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss index 540718197e0..a2fa2e7769b 100644 --- a/app/assets/stylesheets/framework/blank.scss +++ b/app/assets/stylesheets/framework/blank.scss @@ -32,14 +32,14 @@ .blank-state-title { margin-top: 0; margin-bottom: 5px; - font-size: 19px; + font-size: 18px; font-weight: normal; } .blank-state-text { margin-top: 0; margin-bottom: $gl-padding; - font-size: 15px; + font-size: 14px; > strong { font-weight: 600; diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 7e168092522..407c800feb7 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -1,20 +1,15 @@ -.light-well { - background-color: $background-color; - padding: 15px; -} - .centered-light-block { text-align: center; - color: $gl-gray; + color: $gl-text-color; margin: 20px; } .nothing-here-block { text-align: center; padding: 20px; - color: $gl-gray; + color: $gl-text-color; font-weight: normal; - font-size: 16px; + font-size: 14px; line-height: 36px; &.diff-collapsed { @@ -29,19 +24,19 @@ .row-content-block { margin-top: 0; margin-bottom: -$gl-padding; - background-color: $background-color; + background-color: $gray-light; padding: $gl-padding; margin-bottom: 0; border-top: 1px solid $white-dark; border-bottom: 1px solid $white-dark; - color: $gl-gray; + color: $gl-text-color; &.oneline-block { line-height: 42px; } &.white { - background-color: white; + background-color: $white-light; } &.top-block { @@ -123,7 +118,7 @@ .cover-block { text-align: center; - background: $background-color; + background: $gray-light; padding-top: 44px; position: relative; @@ -140,11 +135,11 @@ } .cover-title { - color: $gl-header-color; + color: $gl-text-color; font-size: 23px; h1 { - color: $gl-gray-dark; + color: $gl-text-color; margin-bottom: 6px; font-size: 23px; } @@ -158,7 +153,7 @@ p { padding: 0 $gl-padding; - color: #5c5d5e; + color: $gl-text-color; } } @@ -216,7 +211,7 @@ display: inline; font-weight: normal; font-size: 24px; - color: $gl-title-color; + color: $gl-text-color; } } } @@ -254,3 +249,36 @@ .content-block-small { padding: 10px 0; } + +.empty-state { + margin: 100px 0 0; + + .text-content { + max-width: 460px; + margin: 0 auto; + padding: $gl-padding; + } + + .svg-content { + text-align: center; + + svg { + max-width: 425px; + width: 100%; + padding: $gl-padding; + } + } + + .emoji-icon { + display: inline-block; + } + + @media(max-width: $screen-xs-max) { + margin-top: 50px; + text-align: center; + + .btn { + width: 100%; + } + } +} diff --git a/app/assets/stylesheets/framework/broadcast-messages.scss b/app/assets/stylesheets/framework/broadcast-messages.scss new file mode 100644 index 00000000000..9b54fb94cdc --- /dev/null +++ b/app/assets/stylesheets/framework/broadcast-messages.scss @@ -0,0 +1,21 @@ +.broadcast-message { + @extend .alert-warning; + padding: 10px; + text-align: center; + + div, + p { + display: inline; + margin: 0; + + a { + color: inherit; + text-decoration: underline; + } + } +} + +.broadcast-message-preview { + @extend .broadcast-message; + margin-bottom: 20px; +} diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index e7aff2d0cec..bb6129158d9 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -1,7 +1,7 @@ @mixin btn-default { border-radius: 3px; font-size: $gl-font-size; - font-weight: 500; + font-weight: 400; padding: $gl-vert-padding $gl-btn-padding; &:focus, @@ -15,7 +15,7 @@ @include btn-default; } -@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border) { +@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border, $active-background, $active-border) { background-color: $background; color: $text; border-color: $border; @@ -23,8 +23,14 @@ &:hover, &:focus { background-color: $hover-background; - color: $hover-text; border-color: $hover-border; + color: $hover-text; + } + + &:active { + background-color: $active-background; + border-color: $active-border; + color: $hover-text; } } @@ -62,31 +68,31 @@ } @mixin btn-green { - @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, #fff); + @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, $white-light); } @mixin btn-blue { - @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #fff); + @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, $white-light); } @mixin btn-blue-medium { - @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #fff); + @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, $white-light); } @mixin btn-orange { - @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #fff); + @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, $white-light); } @mixin btn-red { - @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, #fff); + @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, $white-light); } @mixin btn-gray { - @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-light, $gray-dark, $border-gray-dark, $gl-gray-dark); + @include btn-color($gray-light, $border-gray-normal, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-text-color); } @mixin btn-white { - @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $btn-white-active); + @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-gray-dark, $gl-text-color); } @mixin btn-with-margin { @@ -139,7 +145,11 @@ &.btn-new, &.btn-create, &.btn-save { - @include btn-outline($white-light, $green-normal, $green-normal, $green-light, $white-light, $green-light); + @include btn-outline($white-light, $border-green-light, $border-green-light, $green-light, $white-light, $border-green-light, $green-normal, $border-green-normal); + } + + &.btn-remove { + @include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal); } } @@ -161,11 +171,11 @@ } &.btn-close { - @include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light); + @include btn-outline($white-light, $border-orange-light, $border-orange-light, $orange-light, $white-light, $border-orange-light, $orange-normal, $border-orange-normal); } &.btn-spam { - @include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light); + @include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal); } &.btn-danger, @@ -195,7 +205,7 @@ } .fa-caret-down, - .fa-caret-up { + .fa-chevron-down { margin-left: 5px; } @@ -220,12 +230,19 @@ } } +.btn-terminal { + svg { + height: 14px; + width: 18px; + } +} + .btn-lg { padding: 12px 20px; } .btn-transparent { - color: $btn-transparent-color; + color: $gl-text-color-secondary; background-color: transparent; border: 0; @@ -279,8 +296,8 @@ .active { box-shadow: $gl-btn-active-background; - border: 1px solid #c6cacf !important; - background-color: #e4e7ed !important; + border: 1px solid $border-gray-dark !important; + background-color: $btn-active-gray-light !important; } } @@ -299,17 +316,17 @@ text-align: left; padding: 6px 16px; border-color: $border-color; - color: $btn-placeholder-gray; - background-color: $background-color; + color: $gray-darkest; + background-color: $gray-light; &:hover, &:active, &:focus { cursor: text; box-shadow: none; - border-color: $border-color; - color: $btn-placeholder-gray; - background-color: $background-color; + border-color: lighten($dropdown-input-focus-border, 20%); + color: $gray-darkest; + background-color: $gray-light; } } @@ -321,7 +338,7 @@ margin-left: 10px; i { - color: $gl-icon-color; + color: $gl-text-color-secondary; } } @@ -334,14 +351,20 @@ } .btn-static { - background-color: $background-color !important; - border: 1px solid lightgrey; + background-color: $gray-light !important; + border: 1px solid $border-gray-normal; cursor: default; &:active { - -moz-box-shadow: inset 0 0 0 white; - -webkit-box-shadow: inset 0 0 0 white; - box-shadow: inset 0 0 0 white; + -moz-box-shadow: inset 0 0 0 $white-light; + -webkit-box-shadow: inset 0 0 0 $white-light; + box-shadow: inset 0 0 0 $white-light; + } +} + +.btn-inverted { + &-secondary { + @include btn-outline($white-light, $border-blue-light, $border-blue-light, $blue-light, $white-light, $border-blue-light, $blue-normal, $border-blue-normal); } } diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index 8642b7530e2..ef921a8c6a9 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -2,7 +2,7 @@ padding-left: 0; padding-right: 0; - @media (min-width: $screen-sm-min) and (max-width: $screen-lg-min) { + @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { overflow-x: scroll; } } @@ -28,13 +28,13 @@ .user-contrib-cell { &:hover { cursor: pointer; - stroke: #000; + stroke: $black; } } .user-contrib-text { font-size: 12px; - fill: #959494; + fill: $calendar-user-contrib-text; } .calendar-hint { diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index f3b6ad88ad6..e0e46dd73af 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -11,7 +11,7 @@ padding: $gl-padding; border-left: 3px solid $border-color; color: $text-color; - background: $background-color; + background: $gray-light; } .bs-callout h4 { @@ -25,25 +25,25 @@ /* Variations */ .bs-callout-danger { - background-color: #fdf7f7; - border-color: #eed3d7; - color: #b94a48; + background-color: $callout-danger-bg; + border-color: $callout-danger-border; + color: $callout-danger-color; } .bs-callout-warning { - background-color: #faf8f0; - border-color: #faebcc; - color: #8a6d3b; + background-color: $callout-warning-bg; + border-color: $callout-warning-border; + color: $callout-warning-color; } .bs-callout-info { - background-color: #f4f8fa; - border-color: #bce8f1; - color: #34789a; + background-color: $callout-info-bg; + border-color: $callout-info-border; + color: $callout-info-color; } .bs-callout-success { - background-color: #dff0d8; - border-color: #5ca64d; - color: #3c763d; + background-color: $callout-success-bg; + border-color: $callout-success-border; + color: $callout-success-color; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index ad5ac589d0f..0ce94a26a7f 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -1,9 +1,9 @@ /** COLORS **/ -.cgray { color: $gl-gray; } -.clgray { color: #bbb; } -.cred { color: $gl-text-red; } -.cgreen { color: $gl-text-green; } -.cdark { color: #444; } +.cgray { color: $common-gray; } +.clgray { color: $common-gray-light; } +.cred { color: $common-red; } +.cgreen { color: $common-green; } +.cdark { color: $common-gray-dark; } /** COMMON CLASSES **/ .prepend-top-0 { margin-top: 0; } @@ -26,14 +26,15 @@ .append-bottom-default { margin-bottom: $gl-padding; } .inline { display: inline-block; } .center { text-align: center; } +.vertical-align-middle { vertical-align: middle; } .underlined-link { text-decoration: underline; } -.hint { font-style: italic; color: #999; } -.light { color: $gl-gray; } +.hint { font-style: italic; color: $hint-color; } +.light { color: $common-gray; } .slead { - color: $gl-gray; - font-size: 15px; + color: $common-gray; + font-size: 14px; margin-bottom: 12px; font-weight: normal; line-height: 24px; @@ -52,21 +53,38 @@ pre { } &.well-pre { - border: 1px solid #eee; + border: 1px solid $well-pre-bg; background: $gray-light; border-radius: 0; - color: #555; + color: $well-pre-color; + } + + &.wrap { + word-break: break-word; + white-space: pre-wrap; } } hr { margin: $gl-padding 0; + border-top: 1px solid darken($gray-normal, 8%); } .str-truncated { @include str-truncated; } +.block-truncated { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + > div, + .str-truncated { + display: inline; + } +} + .item-title { font-weight: 600; } /** FLASH message **/ @@ -87,14 +105,14 @@ table a code { .loading { margin: 20px auto; height: 40px; - color: #555; + color: $loading-color; font-size: 32px; text-align: center; } span.update-author { display: block; - color: #999; + color: $update-author-color; font-weight: normal; font-style: italic; @@ -105,7 +123,7 @@ span.update-author { } .user-mention { - color: #2fa0bb; + color: $user-mention-color; font-weight: bold; } @@ -114,7 +132,7 @@ span.update-author { } p.time { - color: #999; + color: $time-color; font-size: 90%; margin: 30px 3px 3px 2px; } @@ -150,7 +168,7 @@ li.note { .project_member_show { td:first-child { - color: #aaa; + color: $project-member-show-color; } } @@ -176,7 +194,7 @@ li.note { margin-top: 40px; pre { - background: white; + background: $white-light; border: none; font-size: 12px; } @@ -184,12 +202,12 @@ li.note { .error-message { padding: 10px; - background: #c67; + background: $error-bg; margin: 0; - color: #fff; + color: $white-light; a { - color: #fff; + color: $white-light; text-decoration: underline; } } @@ -197,22 +215,22 @@ li.note { .browser-alert { padding: 10px; text-align: center; - background: #c67; - color: #fff; + background: $error-bg; + color: $white-light; font-weight: bold; a { - color: #fff; + color: $white-light; text-decoration: underline; } } .warning_message { - border-left: 4px solid #ed9; - color: #b90; + border-left: 4px solid $warning-message-border; + color: $warning-message-color; padding: 10px; margin-bottom: 10px; - background: #ffffe6; + background: $warning-message-bg; padding-left: 20px; &.centered { @@ -222,7 +240,7 @@ li.note { .gitlab-promo { a { - color: #aaa; + color: $gl-promo-color; margin-right: 30px; } } @@ -245,7 +263,7 @@ li.note { position: relative; top: 2px; left: 5px; - color: #666; + color: $control-group-descr-color; } } } @@ -255,6 +273,7 @@ img.emoji { height: 20px; vertical-align: top; width: 20px; + margin-top: 1px; } .chart { @@ -270,7 +289,7 @@ img.emoji { table { td.permission-x { - background: #d9edf7 !important; + background: $table-permission-x-bg !important; text-align: center; } } @@ -299,6 +318,10 @@ table { .well { margin-bottom: $gl-padding; + + hr { + border-color: $gray-darker; + } } .search_box { @@ -319,13 +342,13 @@ table { .username { font-size: 18px; - color: #666; + color: $username-color; margin-top: 8px; } .description { font-size: $gl-font-size; - color: #666; + color: $description-color; margin-top: 8px; } } @@ -335,7 +358,7 @@ table { .profiler-button, .profiler-controls { - border-color: #eee !important; + border-color: $profiler-border !important; } } @@ -375,4 +398,32 @@ table { border-top: 1px solid $border-color; } -.hide-bottom-border { border-bottom: none !important; } +.hide-bottom-border { + border-bottom: none !important; +} + +.gl-accessibility { + &:focus { + top: 1px; + left: 1px; + width: auto; + height: 100%; + line-height: 50px; + padding: 0 10px; + clip: auto; + text-decoration: none; + color: $gl-text-color; + background: $gray-light; + z-index: 1; + } +} + +.str-truncated { + &-60 { + @include str-truncated(60%); + } + + &-100 { + @include str-truncated(100%); + } +} diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 583c17e4a83..755eddefa42 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -8,6 +8,12 @@ } } +@mixin chevron-active { + .fa-chevron-down { + color: $gray-darkest; + } +} + .open { .dropdown-menu, .dropdown-menu-nav { @@ -19,53 +25,32 @@ } } + .dropdown-toggle, .dropdown-menu-toggle { - border-color: $dropdown-toggle-hover-border-color; - - .fa { - color: $dropdown-toggle-hover-icon-color; - } + @include chevron-active; + border-color: $gray-darkest; } } -.dropdown-menu-toggle { - position: relative; - width: 160px; - padding: 6px 20px 6px 10px; - background-color: $dropdown-toggle-bg; - color: $dropdown-toggle-color; - font-size: 15px; +.dropdown-toggle { + padding: 6px 8px 6px 10px; + background-color: $white-light; + color: $gl-text-color; + font-size: 14px; text-align: left; border: 1px solid $border-color; border-radius: $border-radius-base; - text-overflow: ellipsis; white-space: nowrap; - overflow: hidden; - - .fa { - position: absolute; - top: 10px; - right: 8px; - color: $dropdown-toggle-icon-color; - &.fa-spinner { - font-size: 16px; - margin-top: -8px; - } + &[disabled] { + background-color: $input-bg-disabled; + cursor: not-allowed; } &.no-outline { outline: 0; } - &:hover, { - border-color: $dropdown-toggle-hover-border-color; - - .fa { - color: $dropdown-toggle-hover-icon-color; - } - } - &.large { width: 200px; } @@ -86,6 +71,51 @@ max-width: 100%; padding-right: 25px; } + + .fa { + color: $gray-darkest; + } + + .fa-chevron-down { + font-size: $dropdown-chevron-size; + position: relative; + top: -3px; + margin-left: 5px; + } + + &:hover { + @include chevron-active; + border-color: $gray-darkest; + } + + &:focus:active { + @include chevron-active; + border-color: $dropdown-toggle-active-border-color; + } +} + +.dropdown-menu-toggle { + @extend .dropdown-toggle; + padding-right: 20px; + position: relative; + width: 163px; + text-overflow: ellipsis; + overflow: hidden; + + .fa { + position: absolute; + + &.fa-spinner { + font-size: 16px; + margin-top: -8px; + } + } + + .fa-chevron-down { + position: absolute; + top: 11px; + right: 8px; + } } .dropdown-menu, @@ -98,10 +128,10 @@ width: 240px; margin-top: 2px; margin-bottom: 0; - font-size: 15px; + font-size: 14px; font-weight: normal; padding: 8px 0; - background-color: $dropdown-bg; + background-color: $white-light; border: 1px solid $dropdown-border-color; border-radius: $border-radius-base; box-shadow: 0 2px 4px $dropdown-shadow-color; @@ -158,7 +188,6 @@ &.is-focused { background-color: $dropdown-link-hover-bg; text-decoration: none; - outline: 0; } &.dropdown-menu-empty-link { @@ -172,7 +201,7 @@ } .icon-play { - fill: $table-text-gray; + fill: $gl-text-color-secondary; margin-right: 6px; height: 12px; width: 11px; @@ -180,7 +209,7 @@ } .dropdown-header { - color: $dropdown-header-color; + color: $gl-text-color-secondary; font-size: 13px; line-height: 22px; padding: 0 10px; @@ -193,7 +222,7 @@ .unclickable { cursor: not-allowed; padding: 5px 8px; - color: $dropdown-header-color; + color: $gl-text-color-secondary; } } @@ -351,7 +380,7 @@ position: absolute; top: 10px; right: 20px; - color: #c7c7c7; + color: $dropdown-input-fa-color; font-size: 12px; pointer-events: none; } @@ -504,7 +533,7 @@ .ui-datepicker-calendar { .ui-state-hover, .ui-state-active { - color: #fff; + color: $white-light; border: 0; } } @@ -563,8 +592,8 @@ } .ui-datepicker-title { - color: $gl-gray; - font-size: 15px; + color: $gl-text-color; + font-size: 14px; line-height: 1; font-weight: normal; } @@ -572,30 +601,30 @@ th { padding: 2px 0; - color: $calendar-header-color; + color: $note-disabled-comment-color; font-weight: normal; text-transform: lowercase; border-top: 1px solid $calendar-border-color; } .ui-datepicker-unselectable { - background-color: $calendar-unselectable-bg; + background-color: $gray-light; } } .dropdown-menu-inner-title { display: block; - color: $gl-title-color; + color: $gl-text-color; font-weight: 600; } .dropdown-menu-inner-content { display: block; - color: $gl-placeholder-color; + color: $gl-text-color-secondary; } .dropdown-toggle-text { &.is-default { - color: $gl-placeholder-color; + color: $gl-text-color-secondary; } } diff --git a/app/assets/stylesheets/pages/emojis.scss b/app/assets/stylesheets/framework/emojis.scss index f17797b2381..7158de65143 100644 --- a/app/assets/stylesheets/pages/emojis.scss +++ b/app/assets/stylesheets/framework/emojis.scss @@ -1,4 +1,4 @@ -.emoji-0023-20E3 { background-position: 0 0px; } +.emoji-0023-20E3 { background-position: 0 0; } .emoji-002A-20E3 { background-position: -20px 0; } .emoji-0030-20E3 { background-position: 0 -20px; } .emoji-0031-20E3 { background-position: -20px -20px; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index f49d7b92a00..c51912b4ac4 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -19,7 +19,7 @@ .file-title { position: relative; - background-color: $background-color; + background-color: $gray-light; border-bottom: 1px solid $border-color; margin: 0; text-align: left; @@ -59,10 +59,10 @@ } .file-content { - background: #fff; + background: $white-light; &.image_file { - background: #eee; + background: $file-image-bg; text-align: center; img { @@ -84,8 +84,8 @@ } &.blob-no-preview { - background: #eee; - text-shadow: 0 1px 2px #fff; + background: $blob-bg; + text-shadow: 0 1px 2px $white-light; padding: 100px 0; } @@ -99,7 +99,7 @@ } tr { - border-bottom: 1px solid #eee; + border-bottom: 1px solid $blame-border; } td { @@ -120,7 +120,7 @@ td.line-numbers { float: none; - border-left: 1px solid #ddd; + border-left: 1px solid $blame-line-numbers-border; i { float: none; @@ -134,7 +134,7 @@ } &.logs { - background: #eee; + background: $logs-bg; max-height: 700px; overflow-y: auto; @@ -143,14 +143,14 @@ padding: 10px 0; border-left: 1px solid $border-color; margin-bottom: 0; - background: white; + background: $white-light; li { - color: #888; + color: $logs-li-color; p { margin: 0; - color: #333; + color: $logs-p-color; line-height: 24px; padding-left: 10px; } @@ -182,3 +182,52 @@ span.idiff { border-bottom-right-radius: 2px; } } + +.file-stats { + ul { + list-style: none; + margin: 0; + padding: 10px 0; + + li { + padding: 3px 0; + line-height: 20px; + } + } + + .new-file { + a { + color: $gl-text-green; + } + } + + .renamed-file { + a { + color: $gl-text-orange; + } + } + + .deleted-file { + a { + color: $gl-text-red; + } + } + + .edit-file { + a { + color: $gl-text-color; + } + } + + a { + text-decoration: none; + + .new-file { + color: $notify-new-file; + } + + .deleted-file { + color: $notify-deleted-file; + } + } +} diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 19827943385..d957ec64654 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -23,3 +23,118 @@ } } +.filtered-search-container { + display: -webkit-flex; + display: flex; +} + +.filtered-search-input-container { + display: -webkit-flex; + display: flex; + position: relative; + width: 100%; + + .form-control { + padding-left: 25px; + padding-right: 25px; + + &:focus ~ .fa-filter { + color: $common-gray-dark; + } + } + + .fa-filter { + position: absolute; + top: 10px; + left: 10px; + color: $gray-darkest; + } + + .fa-times { + right: 10px; + color: $gray-darkest; + } + + .clear-search { + width: 35px; + background-color: transparent; + border: none; + position: absolute; + right: 0; + height: 100%; + outline: none; + + &:hover .fa-times { + color: $common-gray-dark; + } + } +} + +.dropdown-menu .filter-dropdown-item { + padding: 0; +} + +.filter-dropdown { + max-height: 215px; + overflow: auto; +} + +.filter-dropdown-item { + .btn { + border: none; + width: 100%; + text-align: left; + padding: 8px 16px; + text-overflow: ellipsis; + overflow: hidden; + border-radius: 0; + + .fa { + width: 15px; + } + + .dropdown-label-box { + border-color: $white-light; + border-style: solid; + border-width: 1px; + width: 17px; + height: 17px; + } + + &:hover, + &:focus { + background-color: $dropdown-hover-color; + color: $white-light; + text-decoration: none; + + .avatar { + border-color: $white-light; + } + } + } + + .dropdown-light-content { + font-size: 14px; + font-weight: 400; + } + + .dropdown-user { + display: -webkit-flex; + display: flex; + } + + .dropdown-user-details { + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + flex-direction: column; + } +} + +.hint-dropdown { + width: 250px; +} + +.filter-dropdown-loading { + padding: 8px 16px; +} diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index a9006de6d3e..eadb9409fee 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -38,7 +38,7 @@ } } -@media (max-width: $screen-md-min) { +@media (max-width: $screen-sm-max) { ul.notes { .flash-container.timeline-content { margin-left: 0; diff --git a/app/assets/stylesheets/framework/fonts.scss b/app/assets/stylesheets/framework/fonts.scss deleted file mode 100644 index 5f9685bc71a..00000000000 --- a/app/assets/stylesheets/framework/fonts.scss +++ /dev/null @@ -1,45 +0,0 @@ -// Disabling "SpaceAfterPropertyColon" linter because the linter doesn't like -// the way the `src` property is formatted in this file. -// scss-lint:disable SpaceAfterPropertyColon - -/* latin-ext */ -@font-face { - font-family: 'Source Sans Pro'; - font-style: normal; - font-weight: 300; - src: - local('Source Sans Pro Light'), - local('SourceSansPro-Light'), - font-url('SourceSansPro-Light.ttf.woff2') format('woff2'), - font-url('SourceSansPro-Light.ttf.woff') format('woff'); -} -@font-face { - font-family: 'Source Sans Pro'; - font-style: normal; - font-weight: 400; - src: - local('Source Sans Pro'), - local('SourceSansPro-Regular'), - font-url('SourceSansPro-Regular.ttf.woff2') format('woff2'), - font-url('SourceSansPro-Regular.ttf.woff') format('woff'); -} -@font-face { - font-family: 'Source Sans Pro'; - font-style: normal; - font-weight: 600; - src: - local('Source Sans Pro Semibold'), - local('SourceSansPro-Semibold'), - font-url('SourceSansPro-Semibold.ttf.woff2') format('woff2'), - font-url('SourceSansPro-Semibold.ttf.woff') format('woff'); -} -@font-face { - font-family: 'Source Sans Pro'; - font-style: normal; - font-weight: 700; - src: - local('Source Sans Pro Bold'), - local('SourceSansPro-Bold'), - font-url('SourceSansPro-Bold.ttf.woff2') format('woff2'), - font-url('SourceSansPro-Bold.ttf.woff') format('woff'); -} diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index f0727e9688a..25d6fbe465a 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -7,9 +7,9 @@ input { } input[type='text'].danger { - background: #f2dede!important; - border-color: #d66; - text-shadow: 0 1px 1px #fff; + background: $input-danger-bg !important; + border-color: $input-danger-border; + text-shadow: 0 1px 1px $white-light; } .datetime-controls { @@ -22,7 +22,7 @@ input[type='text'].danger { margin-top: 0; margin-bottom: -$gl-padding; padding: $gl-padding; - background-color: $background-color; + background-color: $gray-light; border-top: 1px solid $border-color; } @@ -68,6 +68,50 @@ label { } } +.help-form .form-group { + margin-left: 0; + margin-right: 0; + + .control-label { + font-weight: bold; + padding-top: 4px; + } + + .form-control { + height: 29px; + background: $white-light; + font-family: $monospace_font; + } + + .input-group-btn .btn { + padding: 3px $gl-btn-padding; + background-color: $gray-light; + border: 1px solid $border-color; + } + + .text-block { + line-height: 0.8; + padding-top: 9px; + + code { + line-height: 1.8; + } + + img { + margin-right: $gl-padding; + } + } + + @media(max-width: $screen-xs-max) { + padding: 0 $gl-padding; + + .control-label, + .text-block { + padding-left: 0; + } + } +} + .fieldset-form fieldset { margin-bottom: 20px; } @@ -109,7 +153,7 @@ label { } .form-control::-webkit-input-placeholder { - color: $gl-placeholder-color; + color: $gl-text-color-secondary; } .input-group { @@ -119,7 +163,7 @@ label { } .input-group-addon { - background-color: #f7f8fa; + background-color: $input-group-addon-bg; } .input-group-addon:not(:first-child):not(:last-child) { @@ -141,7 +185,7 @@ label { border: 1px solid $green-normal; &:focus { - box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 $green-normal; + box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $green-normal; border: 0 none; } } @@ -150,7 +194,7 @@ label { border: 1px solid $red-normal; &:focus { - box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 rgba(210, 40, 82, 0.6); + box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $gl-field-focus-shadow-error; border: 0 none; } } @@ -167,4 +211,3 @@ label { color: $gl-text-color; } } - diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index 91ab1503439..d6566dc4ec9 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -21,7 +21,6 @@ background: $color-darker; } - .sidebar-header, .sidebar-action-buttons { color: $color-light; background-color: lighten($color-darker, 5%); @@ -81,42 +80,65 @@ } } + .about-gitlab { + color: $color-light; + } } } } -$theme-charcoal: #3d454d; -$theme-charcoal-dark: #383f45; -$theme-charcoal-text: #b9bbbe; +$theme-charcoal-light: #b9bbbe; +$theme-charcoal: #485157; +$theme-charcoal-dark: #3d454d; +$theme-charcoal-darker: #383f45; +$theme-blue-light: #becde9; $theme-blue: #2980b9; -$theme-graphite: #666; -$theme-gray: #373737; +$theme-blue-dark: #1970a9; +$theme-blue-darker: #096099; + +$theme-graphite-light: #ccc; +$theme-graphite: #777; +$theme-graphite-dark: #666; +$theme-graphite-darker: #555; + +$theme-black-light: #979797; +$theme-black: #373737; +$theme-black-dark: #272727; +$theme-black-darker: #222; + +$theme-green-light: #adc; $theme-green: #019875; +$theme-green-dark: #018865; +$theme-green-darker: #017855; + +$theme-violet-light: #98c; $theme-violet: #548; +$theme-violet-dark: #436; +$theme-violet-darker: #325; body { &.ui_blue { - @include gitlab-theme(#becde9, $theme-blue, #1970a9, #096099); + @include gitlab-theme($theme-blue-light, $theme-blue, $theme-blue-dark, $theme-blue-darker); } &.ui_charcoal { - @include gitlab-theme($theme-charcoal-text, #485157, $theme-charcoal, $theme-charcoal-dark); + @include gitlab-theme($theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark, $theme-charcoal-darker); } &.ui_graphite { - @include gitlab-theme(#ccc, #777, $theme-graphite, #555); + @include gitlab-theme($theme-graphite-light, $theme-graphite, $theme-graphite-dark, $theme-graphite-darker); } - &.ui_gray { - @include gitlab-theme(#979797, $theme-gray, #272727, #222); + &.ui_black { + @include gitlab-theme($theme-black-light, $theme-black, $theme-black-dark, $theme-black-darker); } &.ui_green { - @include gitlab-theme(#adc, $theme-green, #018865, #017855); + @include gitlab-theme($theme-green-light, $theme-green, $theme-green-dark, $theme-green-darker); } &.ui_violet { - @include gitlab-theme(#98c, $theme-violet, #436, #325); + @include gitlab-theme($theme-violet-light, $theme-violet, $theme-violet-dark, $theme-violet-darker); } } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 5a34132112a..24a1ce2b84d 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -8,8 +8,8 @@ header { &.navbar-empty { height: $header-height; - background: #fff; - border-bottom: 1px solid $btn-gray-hover; + background: $white-light; + border-bottom: 1px solid $white-normal; .center-logo { margin: 8px 0; @@ -27,7 +27,7 @@ header { z-index: 100; margin-bottom: 0; height: $header-height; - background-color: $background-color; + background-color: $gray-light; border: none; border-bottom: 1px solid $border-color; @@ -45,7 +45,7 @@ header { padding: 0; .nav > li > a { - color: $gl-icon-color; + color: $gl-text-color-secondary; font-size: 18px; padding: 0; margin: ($header-height - 28) / 2 0; @@ -57,21 +57,34 @@ header { &.header-user-dropdown-toggle { margin-left: 14px; + + &:hover, + &:focus, + &:active { + .header-user-avatar { + border-color: rgba($avatar-border, .2); + } + } } &:hover, &:focus, &:active { - background-color: $background-color; + background-color: $gray-light; + color: darken($gl-text-color-secondary, 30%); + + .todos-pending-count { + background: darken($todo-alert-blue, 10%); + } } .fa-caret-down { - font-size: 15px; + font-size: 14px; } } .navbar-toggle { - color: #666; + color: $nav-toggle-gray; margin: 6px 0; border-radius: 0; position: absolute; @@ -79,11 +92,11 @@ header { padding: 6px 10px; &:hover { - background-color: $btn-gray-hover; + background-color: $white-normal; } &.active { - color: $gl-icon-color; + color: $gl-text-color-secondary; } } } @@ -95,10 +108,11 @@ header { font-size: 18px; padding: 6px 10px; border: none; - background-color: $background-color; + background-color: $gray-light; &:hover { - background-color: $btn-gray-hover; + background-color: $white-normal; + color: $gl-header-nav-hover-color; } } } @@ -151,8 +165,8 @@ header { position: relative; padding-right: 20px; margin: 0; - font-size: 19px; - max-width: 400px; + font-size: 18px; + max-width: 385px; display: inline-block; line-height: $header-height; font-weight: normal; @@ -175,6 +189,7 @@ header { &:hover { text-decoration: underline; + color: $gl-header-nav-hover-color; } } @@ -191,6 +206,10 @@ header { font-size: 10px; text-align: center; cursor: pointer; + + &:hover { + color: $gl-header-nav-hover-color; + } } .project-item-select { @@ -218,6 +237,14 @@ header { } } +.page-sidebar-pinned.right-sidebar-expanded { + @media (max-width: $screen-md-max) { + .header-content .title { + width: 300px; + } + } +} + @media (max-width: $screen-xs-max) { header .container-fluid { font-size: 18px; @@ -254,4 +281,5 @@ header { float: left; margin-right: 5px; border-radius: 50%; + border: 1px solid $avatar-border; } diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index 07c8874bf03..909a0f4afda 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -11,7 +11,7 @@ border-radius: 0; font-family: $monospace_font; font-size: $code_font_size; - line-height: $code_line_height !important; + line-height: 19px; margin: 0; overflow: auto; overflow-y: hidden; @@ -47,7 +47,7 @@ font-family: $monospace_font; display: block; font-size: $code_font_size !important; - line-height: $code_line_height !important; + line-height: 19px; white-space: nowrap; i { diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss new file mode 100644 index 00000000000..868f28cd356 --- /dev/null +++ b/app/assets/stylesheets/framework/icons.scss @@ -0,0 +1,60 @@ +.ci-status-icon-success { + color: $gl-success; + + svg { + fill: $gl-success; + } +} + +.ci-status-icon-failed { + color: $gl-danger; + + svg { + fill: $gl-danger; + } +} + +.ci-status-icon-pending, +.ci-status-icon-failed_with_warnings, +.ci-status-icon-success_with_warnings { + color: $gl-warning; + + svg { + fill: $gl-warning; + } +} + +.ci-status-icon-running { + color: $blue-normal; + + svg { + fill: $blue-normal; + } +} + +.ci-status-icon-canceled, +.ci-status-icon-disabled, +.ci-status-icon-not-found { + color: $gl-text-color; + + svg { + fill: $gl-text-color; + } +} + +.ci-status-icon-created, +.ci-status-icon-skipped { + color: $gray-darkest; + + svg { + fill: $gray-darkest; + } +} + +.ci-status-icon-manual { + color: $gl-text-color; + + svg { + fill: $gl-text-color; + } +} diff --git a/app/assets/stylesheets/pages/appearances.scss b/app/assets/stylesheets/framework/images.scss index 878f44116ba..09a569ad415 100644 --- a/app/assets/stylesheets/pages/appearances.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -4,7 +4,7 @@ } .appearance-light-logo-preview { - background-color: $background-color; + background-color: $gray-light; max-width: 72px; padding: 10px; margin-bottom: 10px; diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index ba3930e03bd..46632f15f35 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -20,7 +20,7 @@ display: block; float: left; margin-right: 10px; - color: #fff; + color: $white-light; font-size: $gl-font-size; line-height: 25px; @@ -37,6 +37,10 @@ } &.status-box-expired { - background: #cea61b; + background-color: $issue-status-expired; + } + + &.status-box-upcoming { + background: $gl-text-color-secondary; } } diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss index 30a5b837d69..18f2f316f02 100644 --- a/app/assets/stylesheets/framework/jquery.scss +++ b/app/assets/stylesheets/framework/jquery.scss @@ -4,13 +4,13 @@ &.ui-datepicker, &.ui-datepicker-inline { - border: 1px solid #ddd; + border: 1px solid $jq-ui-border; padding: 10px; width: 270px; .ui-datepicker-header { - background: #fff; - border-color: #ddd; + background: $white-light; + border-color: $jq-ui-border; .ui-datepicker-prev, .ui-datepicker-next { @@ -39,7 +39,7 @@ } &.ui-autocomplete { - border-color: #ddd; + border-color: $jq-ui-border; padding: 0; margin-top: 2px; z-index: 1001; @@ -50,9 +50,9 @@ } .ui-state-default { - border: 1px solid #fff; - background: #fff; - color: #777; + border: 1px solid $white-light; + background: $white-light; + color: $jq-ui-default-color; } .ui-state-highlight { @@ -66,7 +66,7 @@ .ui-state-focus { border: 1px solid $gl-primary; background: $gl-primary; - color: #fff; + color: $white-light; } } } diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 7baa4296abf..29d55c44699 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -6,7 +6,7 @@ html { body { &.navless { - background-color: white !important; + background-color: $white-light !important; } } @@ -26,6 +26,62 @@ body { .container-limited { max-width: $fixed-layout-width; + + &.limit-container-width { + max-width: $limited-layout-width; + } +} + +.alert-wrapper { + .alert { + margin-bottom: 0; + + &:last-child { + margin-bottom: $gl-padding; + } + } + + .alert-link-group { + float: right; + } + + /* Center alert text and alert action links on smaller screens */ + @media (max-width: $screen-sm-max) { + .alert { + text-align: center; + } + + .alert-link-group { + float: none; + } + } + + /* Stripe the background colors so that adjacent alert-warnings are distinct from one another */ + .alert-warning { + transition: background-color 0.15s, border-color 0.15s; + background-color: lighten($gl-warning, 4%); + border-color: lighten($gl-warning, 4%); + } + + .alert-warning + .alert-warning { + background-color: $gl-warning; + border-color: $gl-warning; + } + + .alert-warning + .alert-warning + .alert-warning { + background-color: darken($gl-warning, 4%); + border-color: darken($gl-warning, 4%); + } + + .alert-warning + .alert-warning + .alert-warning + .alert-warning { + background-color: darken($gl-warning, 8%); + border-color: darken($gl-warning, 8%); + } + + .alert-warning:only-of-type { + background-color: $gl-warning; + border-color: $gl-warning; + } } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index bc0610cc417..1c6698ad0c6 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -11,8 +11,8 @@ > li { padding: 10px 15px; min-height: 20px; - border-bottom: 1px solid #eee; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); + border-bottom: 1px solid $list-border-light; + border-bottom: 1px solid $list-border; &::after { content: " "; @@ -21,7 +21,7 @@ } &.disabled { - color: #888; + color: $list-text-disabled-color; } &.unstyled { @@ -31,12 +31,12 @@ } &.warning-row { - background-color: #fcf8e3; - border-color: #faebcc; - color: #8a6d3b; + background-color: $list-warning-row-bg; + border-color: $list-warning-row-border; + color: $list-warning-row-color; } - &.smoke { background-color: $background-color; } + &.smoke { background-color: $gray-light; } &:not(.ui-sort-disabled):hover { background: $row-hover; @@ -46,7 +46,7 @@ border-bottom: none; &.bottom { - background: $background-color; + background: $gray-light; } } @@ -59,7 +59,7 @@ p { padding-top: 1px; margin: 0; - color: $gray-dark; + color: $white-normal; img { position: relative; @@ -106,14 +106,14 @@ ul.task-list { } } +// Generic content list ul.content-list { @include basic-list; - margin: 0; padding: 0; - > li { - border-color: $table-border-color; + li { + border-color: $white-normal; font-size: $list-font-size; color: $list-text-color; @@ -128,7 +128,7 @@ ul.content-list { } a { - color: $gl-dark-link-color; + color: $gl-text-color; } .member-group-link { @@ -163,6 +163,10 @@ ul.content-list { &:last-child { margin-right: 0; + + @media(max-width: $screen-xs-max) { + margin: 0 auto; + } } } @@ -186,25 +190,97 @@ ul.content-list { &.list-placeholder { background-color: $gray-light; - border: dotted 1px $gray-dark; + border: dotted 1px $white-normal; margin: 1px 0; min-height: 52px; } } } -.panel > .content-list > li { - padding: $gl-padding-top $gl-padding; +// Content list using flexbox +.flex-list { + .flex-row { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + align-items: center; + white-space: nowrap; + } + + .row-main-content { + flex: 1 1 auto; + overflow: hidden; + padding-right: 8px; + } + + .row-fixed-content { + flex: 0 0 auto; + margin-left: auto; + } + + .row-title { + font-weight: 600; + } + + .row-second-line { + display: block; + } + + .dropdown { + .btn-block { + margin-bottom: 0; + line-height: inherit; + } + } + + .label-default { + color: $gl-text-color-secondary; + } +} + +// Table list +.table-list { + display: table; + width: 100%; + + .table-list-row { + display: table-row; + } + + .table-list-cell { + display: table-cell; + vertical-align: top; + padding: 10px 16px; + border-bottom: 1px solid $gray-darker; - &.commit { - @media (min-width: $screen-sm-min) { - padding-left: 46px + $gl-padding; + &.avatar-cell { + width: 36px; + padding-right: 0; + + img { + margin-right: 0; + } } } + + &.table-wide { + .table-list-cell { + &:last-of-type { + padding-right: 0; + } + + &:first-of-type { + padding-left: 0; + } + } + } +} + +.panel > .content-list > li { + padding: $gl-padding-top $gl-padding; } ul.controls { - padding-top: 1px; float: right; list-style: none; diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 6d28d98b283..5bff694658c 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -56,26 +56,24 @@ .md-header { .nav-links { - .active { - a { - border-bottom-color: #000; - } - } - a { padding-top: 0; - line-height: 1; - border-bottom: 1px solid $border-color; + line-height: 19px; &.btn.btn-xs { padding: 2px 5px; } + + &:focus { + margin-top: -10px; + padding-top: 10px; + } } } } .referenced-users { - color: #4c4e54; + color: $gl-text-color; padding-top: 10px; } @@ -87,8 +85,8 @@ .markdown-area { border-radius: 0; - background: #fff; - border: 1px solid #ddd; + background: $white-light; + border: 1px solid $md-area-border; min-height: 140px; max-height: 500px; padding: 5px; @@ -110,13 +108,13 @@ hr { // Darken 'whitesmoke' a bit to make it more visible in note bodies - border-color: darken(#f5f5f5, 8%); + border-color: darken($gray-normal, 8%); margin: 10px 0; } // Border around images in issue and MR comments. img:not(.emoji) { - border: 1px solid $table-border-gray; + border: 1px solid $white-normal; padding: 5px; margin: 5px 0; // Ensure that image does not exceed viewport @@ -137,7 +135,7 @@ .toolbar-btn { float: left; padding: 0 5px; - color: #959494; + color: $gl-text-color-secondary; background: transparent; border: 0; outline: 0; @@ -148,7 +146,19 @@ } } -.atwho-view small.description { - float: right; - padding: 3px 5px; +.atwho-view { + small.description { + float: right; + padding: 3px 5px; + } + + .avatar-inline { + margin-bottom: 0; + } + + .cur { + .avatar { + border: 1px solid $white-light; + } + } } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index f84ca36d10f..1acd06122a3 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -24,7 +24,7 @@ @include clearfix; padding: 10px 0; - border-bottom: 1px solid #eee; + border-bottom: 1px solid $list-border-light; display: block; margin: 0; @@ -46,7 +46,7 @@ &.light { a { - color: $gl-gray; + color: $gl-text-color; } } } @@ -67,8 +67,8 @@ } @mixin dark-diff-match-line { - color: rgba(255, 255, 255, 0.3); - background: rgba(255, 255, 255, 0.1); + color: $dark-diff-match-bg; + background: $dark-diff-match-color; } @mixin webkit-prefix($property, $value) { diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index c1ed43bc20f..8e2c56a8488 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -23,21 +23,21 @@ margin-right: 0; } - .issues-details-filters, + .issues-details-filters:not(.filtered-search-block), .dash-projects-filters, .check-all-holder { display: none; } - .rss-btn { + .issues-holder .issue-check { display: none; } - .project-home-links { + .rss-btn { display: none; } - .project-avatar { + .project-home-links { display: none; } @@ -54,7 +54,7 @@ } // Display Star and Fork buttons without counters on mobile. - .project-action-buttons { + .project-repo-buttons { display: block; .count-buttons .btn { @@ -71,7 +71,7 @@ display: none; } - .group-right-buttons { + .group-buttons { display: none; } @@ -133,9 +133,9 @@ right: 0; top: 30%; padding: 5px 15px; - background: #eee; + background: $show-aside-bg; font-size: 20px; - color: #777; + color: $show-aside-color; z-index: 100; - box-shadow: 0 1px 2px #ddd; + box-shadow: 0 1px 2px $show-aside-shadow; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index ce864c2de5e..401c2d0f6ee 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -49,50 +49,60 @@ padding: $gl-btn-padding; padding-bottom: 11px; margin-bottom: -1px; - font-size: 15px; + font-size: 14px; line-height: 28px; - color: #959494; + color: $gl-text-color-secondary; border-bottom: 2px solid transparent; &:hover, &:active, &:focus { text-decoration: none; + border-bottom: 2px solid $gray-darkest; + color: $black; + + .badge { + color: $black; + } } } &.active a { border-bottom: 2px solid $link-underline-blue; color: $black; - } + font-weight: 600; - .badge { - font-weight: normal; - background-color: #eee; - color: $btn-transparent-color; - vertical-align: baseline; + .badge { + color: $black; + } } } &.sub-nav { text-align: center; - background-color: $dark-background-color; + background-color: $gray-normal; .container-fluid { - background-color: $dark-background-color; + background-color: $gray-normal; margin-bottom: 0; } li { + &.active a { + border-bottom: none; + color: $link-underline-blue; + } + a { margin: 0; padding: 11px 10px 9px; - } - &.active a { - border-bottom: none; - color: $link-underline-blue; + &:hover, + &:active, + &:focus { + border-color: transparent; + } } } } @@ -100,17 +110,17 @@ .top-area { @include clearfix; - border-bottom: 1px solid $btn-gray-hover; + border-bottom: 1px solid $white-normal; .nav-text { padding-top: 16px; padding-bottom: 11px; display: inline-block; - width: 50%; line-height: 28px; + white-space: normal; /* Small devices (phones, tablets, 768px and lower) */ - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { width: 100%; } } @@ -148,30 +158,24 @@ } .nav-controls { - width: 50%; display: inline-block; float: right; text-align: right; padding: 11px 0; margin-bottom: 0; - > .dropdown { - margin-right: $gl-padding-top; - display: inline-block; - vertical-align: top; - - &:last-child { - margin-right: 0; - } - } - - > .btn { + > .btn, + > .btn-container, + > .dropdown, + > input, + > form { margin-right: $gl-padding-top; display: inline-block; vertical-align: top; &:last-child { margin-right: 0; + float: right; } } @@ -179,19 +183,21 @@ float: none; } - > form { - display: inline-block; - } - .icon-label { display: none; } - input { + .btn, + .dropdown, + .dropdown-toggle, + input, + form { height: 35px; + } + + input { display: inline-block; position: relative; - margin-right: $gl-padding-top; /* Medium devices (desktops, 992px and up) */ @media (min-width: $screen-md-min) { width: 200px; } @@ -215,6 +221,7 @@ .btn, form, .dropdown, + .dropdown-toggle, .dropdown-menu-toggle, .form-control { margin: 0 0 10px; @@ -253,8 +260,26 @@ .nav-text, .nav-controls { width: auto; + + @media (max-width: $screen-xs-max) { + width: 100%; + } + } + } + + &.multi-line { + .nav-text { + line-height: 20px; + } + + .nav-controls { + padding: 17px 0; } } + + pre { + width: 100%; + } } .layout-nav { @@ -262,7 +287,7 @@ top: $header-height; width: 100%; z-index: 11; - background: $background-color; + background: $gray-light; border-bottom: 1px solid $border-color; transition: padding $sidebar-transition-duration; text-align: center; @@ -290,7 +315,7 @@ .fa-caret-down { margin-left: 5px; - color: $gl-icon-color; + color: $gl-text-color-secondary; } .dropdown { @@ -310,37 +335,9 @@ height: 51px; li { - a { padding-top: 10px; } - - a, - i { - color: $layout-link-gray; - } - - &.active { - - a, - i { - color: $black; - } - - svg { - path, - polygon { - fill: $black; - } - } - } - - &:hover { - a, - i { - color: $black; - } - } } } } @@ -353,7 +350,7 @@ } .fade-right { - @include fade(left, $background-color); + @include fade(left, $gray-light); right: -5px; .fa { @@ -362,7 +359,7 @@ } .fade-left { - @include fade(right, $background-color); + @include fade(right, $gray-light); left: -5px; .fa { @@ -373,7 +370,7 @@ &.sub-nav-scroll { .fade-right { - @include fade(left, $dark-background-color); + @include fade(left, $gray-normal); right: 0; .fa { @@ -382,7 +379,7 @@ } .fade-left { - @include fade(right, $dark-background-color); + @include fade(right, $gray-normal); left: 0; .fa { @@ -436,3 +433,40 @@ } } } + +@media (max-width: $screen-xs-max) { + .top-area { + flex-flow: row wrap; + + .nav-controls { + $controls-margin: $btn-xs-side-margin - 2px; + flex: 0 0 100%; + + &.controls-flex { + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: center; + padding: 0 0 $gl-padding-top; + } + + .controls-item, + .controls-item-full, + .controls-item:last-child { + flex: 1 1 35%; + display: block; + width: 100%; + margin: $controls-margin; + + .btn, + .dropdown { + margin: 0; + } + } + + .controls-item-full { + flex: 1 1 100%; + } + } + } +} diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss new file mode 100644 index 00000000000..5f4211147f3 --- /dev/null +++ b/app/assets/stylesheets/framework/page-header.scss @@ -0,0 +1,62 @@ +.page-content-header { + line-height: 34px; + padding: 10px 0; + margin-bottom: 0; + + @media (min-width: $screen-sm-min) { + display: flex; + align-items: center; + + .header-main-content { + flex: 1; + } + } + + .header-action-buttons { + i { + color: $gl-text-color-secondary; + font-size: 13px; + margin-right: 3px; + } + + @media (max-width: $screen-xs-max) { + .btn { + width: 100%; + margin-top: 10px; + } + + .dropdown { + width: 100%; + } + } + } + + .avatar { + @extend .avatar-inline; + margin-left: 0; + + @media (min-width: $screen-sm-min) { + margin-left: 4px; + } + } + + .commit-committer-link, + .commit-author-link { + color: $gl-text-color; + font-weight: bold; + } + + .commit-info { + &.branches { + margin-left: 8px; + } + } + + .ci-status-link { + svg { + position: relative; + top: 2px; + margin: 0 2px 0 3px; + } + } +} diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss index cb2c351c368..b37c1d0d670 100644 --- a/app/assets/stylesheets/framework/pagination.scss +++ b/app/assets/stylesheets/framework/pagination.scss @@ -43,7 +43,7 @@ /** * Small screen pagination */ -@media (max-width: $screen-xs) { +@media (max-width: $screen-xs-min) { .gl-pagination { .pagination li a { padding: 6px 10px; @@ -62,7 +62,7 @@ /** * Medium screen pagination */ -@media (min-width: $screen-xs) and (max-width: $screen-md-max) { +@media (min-width: $screen-xs-min) and (max-width: $screen-md-max) { .gl-pagination { .page { display: none; diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index 5ba0486177f..efe93724013 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -18,6 +18,20 @@ margin-top: -2px; margin-left: 5px; } + + &.split { + display: flex; + align-items: center; + } + + .left { + flex: 1 1 auto; + } + + .right { + flex: 0 0 auto; + text-align: right; + } } .panel-body { @@ -34,3 +48,11 @@ line-height: inherit; } } + +.panel-default { + .table-list-row:last-child { + .table-list-cell { + border-bottom: 0; + } + } +} diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 920ce249b9a..9ab17e67d4c 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -6,7 +6,7 @@ .select2-container, .select2-container.select2-drop-above { .select2-choice { - background: #fff; + background: $white-light; border-color: $input-border; height: 35px; padding: $gl-vert-padding $gl-input-padding; @@ -39,7 +39,7 @@ } &:hover { - background-color: $gray-dark; + background-color: $white-normal; border-color: $border-white-normal; color: $gl-text-color; } @@ -47,7 +47,7 @@ } .select2-drop { - box-shadow: rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0; + box-shadow: $select2-drop-shadow1 0 0 1px 0, $select2-drop-shadow2 0 2px 18px 0; border-radius: $border-radius-default; border: none; min-width: 175px; @@ -59,7 +59,7 @@ } .select2-drop { - color: #7f8fa4; + color: $gl-grayish-blue; } .select2-highlighted { @@ -108,7 +108,7 @@ border-color: $input-border; color: $gl-text-color; line-height: 15px; - background-color: $background-color; + background-color: $gray-light; background-image: none; .select2-search-choice-close { @@ -156,7 +156,7 @@ .select2-search input { padding: 2px 25px 2px 5px; - background: #fff image-url('select2.png'); + background: $white-light image-url('select2.png'); background-repeat: no-repeat; background-position: right 0 bottom 6px; border: 1px solid $input-border; @@ -169,7 +169,7 @@ } .select2-search input.select2-active { - background-color: #fff; + background-color: $white-light; background-image: image-url('select2-spinner.gif') !important; background-repeat: no-repeat; background-position: right 5px center !important; @@ -206,7 +206,7 @@ .select2-highlighted { .group-result { .group-path { - color: #fff; + color: $white-light; } } } @@ -221,7 +221,7 @@ } .group-path { - color: #999; + color: $group-path-color; } } @@ -241,7 +241,7 @@ .namespace-result { .namespace-kind { - color: #aaa; + color: $namespace-kind-color; font-weight: normal; } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 44c445c0543..f0b03710c79 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -23,7 +23,7 @@ .sidebar-wrapper { z-index: 1000; - background: $background-color; + background: $gray-light; .nicescroll-rails-hr { // TODO: Figure out why nicescroll doesn't hide horizontal bar @@ -36,7 +36,7 @@ transition: padding $sidebar-transition-duration; .container-fluid { - background: #fff; + background: $white-light; padding: 0 $gl-padding; &.container-blank { @@ -59,11 +59,6 @@ padding: 0 !important; } - .sidebar-header { - padding: 11px 22px 12px; - font-size: 20px; - } - li { &.separate-item { padding-top: 10px; @@ -106,6 +101,17 @@ padding: 0 8px; border-radius: 6px; } + + .about-gitlab { + padding: 7px $gl-sidebar-padding; + font-size: $gl-font-size; + line-height: 24px; + display: block; + text-decoration: none; + font-weight: normal; + position: absolute; + bottom: 10px; + } } .sidebar-action-buttons { @@ -177,7 +183,9 @@ &.right-sidebar-expanded { .line-resolve-all-container { - display: none; + @media (min-width: $sidebar-breakpoint) { + display: none; + } } } } @@ -220,7 +228,7 @@ header.header-sidebar-pinned { padding-right: 0; @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - &:not(.build-sidebar) { + &:not(.build-sidebar):not(.wiki-sidebar) { padding-right: $sidebar_collapsed_width; } } @@ -228,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/pages/snippets.scss b/app/assets/stylesheets/framework/snippets.scss index 857eb76131a..5f7e1b17cc7 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/framework/snippets.scss @@ -1,3 +1,13 @@ +.snippet-row { + .title { + margin-bottom: 2px; + } + + .snippet-filename { + padding: 0 2px; + } +} + .snippet-form-holder .file-holder .file-title { padding: 2px; } @@ -12,23 +22,19 @@ .snippet-file-content { border-radius: 3px; - margin-bottom: $gl-padding; - - .btn-clipboard { - @extend .btn; - } } -.project-snippets .awards { - border-bottom: 1px solid $table-border-color; - padding-bottom: $gl-padding; +.snippet-header { + padding: $gl-padding 0; } .snippet-title { font-size: 24px; font-weight: 600; - padding: $gl-padding; - padding-left: 0; +} + +.snippet-edited-ago { + color: $gray-darkest; } .snippet-actions { diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 9a90d3794fd..6d9fa74a030 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -14,11 +14,11 @@ table { .warning, .danger, .info { - color: #fff; + color: $white-light; a:not(.btn) { text-decoration: underline; - color: #fff; + color: $white-light; } } @@ -31,14 +31,31 @@ table { } th { - background-color: $background-color; + background-color: $gray-light; font-weight: normal; border-bottom: none; + + &.wide { + width: 55%; + } } td { - border-color: $table-border-color; + border-color: $white-normal; } } } } + +.responsive-table { + @media (max-width: $screen-sm-max) { + th { + width: 100%; + } + + td { + width: 100%; + float: left; + } + } +} diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 875cded8b4e..ff185cd8767 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -6,8 +6,8 @@ .timeline-entry { padding: $gl-padding $gl-btn-padding 11px; - border-color: $table-border-color; - color: $gl-gray; + border-color: $white-normal; + color: $gl-text-color; border-bottom: 1px solid $border-white-light; &:target { @@ -32,7 +32,7 @@ .system-note { .note-text { - color: $gl-gray !important; + color: $gl-text-color !important; } } diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 59f4594bb83..12d56359d7d 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -91,19 +91,19 @@ // Labels .label { padding: 4px 5px; - font-size: 13px; + font-size: 12px; font-style: normal; font-weight: normal; display: inline-block; &.label-gray { - background-color: #f8fafc; - color: $gl-gray; + background-color: $label-gray-bg; + color: $gl-text-color; text-shadow: none; } &.label-inverse { - background-color: #333; + background-color: $label-inverse-bg; } } @@ -158,7 +158,7 @@ font-weight: normal; a { - color: #777; + color: $panel-heading-link-color; } } } @@ -172,7 +172,7 @@ .alert { a:not(.btn) { @extend .alert-link; - color: #fff; + color: $white-light; text-decoration: underline; } } diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index 44fe37d3a4a..0fc89d5976a 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -48,7 +48,7 @@ $font-size-base: $gl-font-size; $padding-base-vertical: $gl-vert-padding; $padding-base-horizontal: $gl-padding; -$component-active-color: #fff; +$component-active-color: $white-light; $component-active-bg: $brand-info; //== Forms @@ -65,20 +65,20 @@ $legend-color: $text-color; // //## -$pagination-color: $gl-gray; -$pagination-bg: #fff; +$pagination-color: $gl-text-color; +$pagination-bg: $white-light; $pagination-border: $border-color; -$pagination-hover-color: $gl-gray; +$pagination-hover-color: $gl-text-color; $pagination-hover-bg: $row-hover; $pagination-hover-border: $border-color; $pagination-active-color: $blue-dark; -$pagination-active-bg: #fff; +$pagination-active-bg: $white-light; $pagination-active-border: $border-color; $pagination-disabled-color: #cdcdcd; -$pagination-disabled-bg: $background-color; +$pagination-disabled-bg: $gray-light; $pagination-disabled-border: $border-color; @@ -86,19 +86,19 @@ $pagination-disabled-border: $border-color; // //## Define colors for form feedback states and, by default, alerts. -$state-success-text: #fff; +$state-success-text: $white-light; $state-success-bg: $brand-success; $state-success-border: $brand-success; -$state-info-text: #fff; +$state-info-text: $white-light; $state-info-bg: $brand-info; $state-info-border: $brand-info; -$state-warning-text: #fff; +$state-warning-text: $white-light; $state-warning-bg: $brand-warning; $state-warning-border: $brand-warning; -$state-danger-text: #fff; +$state-danger-text: $white-light; $state-danger-bg: $brand-danger; $state-danger-border: $brand-danger; @@ -117,10 +117,13 @@ $alert-border-radius: 0; $panel-border-radius: 2px; $panel-default-text: $text-color; $panel-default-border: $border-color; -$panel-default-heading-bg: $background-color; -$panel-footer-bg: $background-color; +$panel-default-heading-bg: $gray-light; +$panel-footer-bg: $gray-light; $panel-inner-border: $border-color; +$badge-bg: $badge-bg; +$badge-color: $badge-color; + //== Wells // //## @@ -135,14 +138,14 @@ $well-border: #eee; $code-color: #c7254e; $code-bg: #f9f2f4; -$kbd-color: #fff; +$kbd-color: $white-light; $kbd-bg: #333; //== Buttons // //## $btn-default-color: $gl-text-color; -$btn-default-bg: #fff; +$btn-default-bg: $white-light; $btn-default-border: #e7e9ed; //== Nav @@ -153,8 +156,8 @@ $nav-link-padding: 13px $gl-padding; //== Code // //## -$pre-bg: $background-color !default; -$pre-color: $gl-gray !default; +$pre-bg: $gray-light !default; +$pre-color: $gl-text-color !default; $pre-border-color: $border-color; -$table-bg-accent: $background-color; +$table-bg-accent: $gray-light; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 070e42d63d2..54958973f15 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -1,5 +1,5 @@ @mixin md-typography { - color: $md-text-color; + color: $gl-text-color; word-wrap: break-word; a { @@ -10,7 +10,7 @@ max-width: 100%; } - *:first-child { + *:first-child:not(.katex-display) { margin-top: 0; } @@ -33,15 +33,15 @@ padding: 3px 5px; font-size: 11px; line-height: 10px; - color: #555; + color: $kdb-color; vertical-align: middle; - background-color: #fcfcfc; + background-color: $kdb-bg; border-width: 1px; border-style: solid; - border-color: #ccc #ccc #bbb; + border-color: $kdb-border $kdb-border $kdb-border-bottom; border-image: none; border-radius: 3px; - box-shadow: 0 -1px 0 #bbb inset; + box-shadow: 0 -1px 0 $kdb-shadow inset; } h1 { @@ -50,14 +50,14 @@ margin: 16px 0 10px; padding: 0 0 0.3em; border-bottom: 1px solid $white-dark; - color: $gl-gray-dark; + color: $gl-text-color; } h2 { font-size: 1.5em; font-weight: 600; margin: 16px 0 10px; - color: $gl-gray-dark; + color: $gl-text-color; } h3 { @@ -81,7 +81,7 @@ } blockquote { - color: #7f8fa4; + color: $gl-grayish-blue; font-size: inherit; padding: 8px 21px; margin: 12px 0; @@ -94,13 +94,13 @@ } blockquote p { - color: #7f8fa4 !important; + color: $gl-grayish-blue !important; font-size: inherit; line-height: 1.5; } p { - color: #5c5d5e; + color: $gl-text-color; margin: 6px 0 0; } @@ -108,10 +108,10 @@ @extend .table; @extend .table-bordered; margin: 12px 0; - color: #5c5d5e; + color: $gl-text-color; th { - background: #f8fafc; + background: $label-gray-bg; } } @@ -182,6 +182,7 @@ left: -16px; position: absolute; text-decoration: none; + outline: none; &::after { content: image-url('icon_anchor.svg'); @@ -201,7 +202,7 @@ * */ body { - -webkit-text-shadow: rgba(255,255,255,0.01) 0 0 1px; + -webkit-text-shadow: $body-text-shadow 0 0 1px; } .page-title { @@ -229,7 +230,7 @@ h3, h4, h5, h6 { - color: $gl-title-color; + color: $gl-text-color; font-weight: 600; } @@ -291,7 +292,7 @@ h2, h3, h4 { small { - color: $gl-gray; + color: $gl-text-color; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index e0d00759c9c..07cb669a46e 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -5,77 +5,77 @@ $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; /* * Color schema */ +$darken-normal-factor: 7%; +$darken-dark-factor: 10%; +$darken-border-factor: 5%; + $white-light: #fff; -$white-normal: #ededed; -$white-dark: #ececec; +$white-normal: #f0f0f0; +$white-dark: #eaeaea; $gray-lightest: #fdfdfd; $gray-light: #fafafa; $gray-lighter: #f9f9f9; $gray-normal: #f5f5f5; -$gray-dark: #ededed; +$gray-dark: darken($gray-light, $darken-dark-factor); $gray-darker: #eee; -$gray-darkest: #c9c9c9; +$gray-darkest: #c4c4c4; -$green-light: #38ae67; -$green-normal: #2faa60; -$green-dark: #2ca05b; +$green-light: #3cbd70; +$green-normal: darken($green-light, $darken-normal-factor); +$green-dark: darken($green-light, $darken-dark-factor); $blue-light: #2ea8e5; -$blue-normal: #2d9fd8; -$blue-dark: #2897ce; +$blue-normal: darken($blue-light, $darken-normal-factor); +$blue-dark: darken($blue-light, $darken-dark-factor); $blue-medium-light: #3498cb; -$blue-medium: #2f8ebf; -$blue-medium-dark: #2d86b4; +$blue-medium: darken($blue-medium-light, $darken-normal-factor); +$blue-medium-dark: darken($blue-medium-light, $darken-dark-factor); $blue-light-transparent: rgba(44, 159, 216, 0.05); $orange-light: #fc8a51; -$orange-normal: #e75e40; -$orange-dark: #ce5237; +$orange-normal: darken($orange-light, $darken-normal-factor); +$orange-dark: darken($orange-light, $darken-dark-factor); $red-light: #e52c5a; -$red-normal: #d22852; -$red-dark: darken($red-normal, 5%); +$red-normal: darken($red-light, $darken-normal-factor); +$red-dark: darken($red-light, $darken-dark-factor); $black: #000; $black-transparent: rgba(0, 0, 0, 0.3); -$border-white-light: #f1f2f4; -$border-white-normal: #d6dae2; -$border-white-dark: #c6cacf; +$border-white-light: darken($white-light, $darken-border-factor); +$border-white-normal: darken($white-normal, $darken-border-factor); -$border-gray-light: #dcdcdc; -$border-gray-normal: #d7d7d7; -$border-gray-dark: #c6cacf; +$border-gray-light: darken($gray-light, $darken-border-factor); +$border-gray-normal: darken($gray-normal, $darken-border-factor); +$border-gray-dark: darken($white-normal, $darken-border-factor); $border-green-extra-light: #9adb84; -$border-green-light: #2faa60; -$border-green-normal: #2ca05b; -$border-green-dark: #279654; - -$border-blue-light: #2d9fd8; -$border-blue-normal: #2897ce; -$border-blue-dark: #258dc1; +$border-green-light: darken($green-light, $darken-border-factor); +$border-green-normal: darken($green-normal, $darken-border-factor); +$border-green-dark: darken($green-dark, $darken-border-factor); -$border-orange-light: #fc6d26; -$border-orange-normal: #ce5237; -$border-orange-dark: #c14e35; +$border-blue-light: darken($blue-light, $darken-border-factor); +$border-blue-normal: darken($blue-normal, $darken-border-factor); +$border-blue-dark: darken($blue-dark, $darken-border-factor); -$border-red-light: #d22852; -$border-red-normal: #ca264f; -$border-red-dark: darken($border-red-normal, 5%); +$border-orange-light: darken($orange-light, $darken-border-factor); +$border-orange-normal: darken($orange-normal, $darken-border-factor); +$border-orange-dark: darken($orange-dark, $darken-border-factor); -$help-well-bg: $gray-light; -$help-well-border: #e5e5e5; +$border-red-light: darken($red-light, $darken-border-factor); +$border-red-normal: darken($red-normal, $darken-border-factor); +$border-red-dark: darken($red-dark, $darken-border-factor); $warning-message-bg: #fbf2d9; $warning-message-color: #9e8e60; @@ -86,52 +86,54 @@ $warning-message-border: #f0e2bb; */ $border-color: #e5e5e5; $focus-border-color: #3aabf0; -$table-border-color: #f0f0f0; -$background-color: $gray-light; -$dark-background-color: #f5f5f5; -$table-text-gray: #8f8f8f; -$widget-expand-item: #e8f2f7; -$widget-inner-border: #eef0f2; +$sidebar-collapsed-icon-color: #999; +$well-expand-item: #e8f2f7; +$well-inner-border: #eef0f2; +$well-light-border: #f1f1f1; +$well-light-text-color: #5b6169; /* * Text */ -$gl-font-size: 15px; -$gl-title-color: #333; -$gl-text-color: #5c5c5c; -$gl-text-color-light: #8c8c8c; +$gl-font-size: 14px; +$gl-text-color: rgba(0, 0, 0, .85); +$gl-text-color-secondary: rgba(0, 0, 0, .55); +$gl-text-color-disabled: rgba(0, 0, 0, .35); $gl-text-green: #4a2; $gl-text-red: #d12f19; $gl-text-orange: #d90; $gl-link-color: #3777b0; -$gl-dark-link-color: #333; -$gl-placeholder-color: #8f8f8f; -$gl-icon-color: $gl-placeholder-color; $gl-grayish-blue: #7f8fa4; $gl-gray: $gl-text-color; $gl-gray-dark: #313236; -$gl-gray-light: $gl-placeholder-color; $gl-header-color: #4c4e54; +$gl-header-nav-hover-color: #434343; /* * Lists */ $list-font-size: $gl-font-size; -$list-title-color: $gl-title-color; +$list-title-color: $gl-text-color; $list-text-color: $gl-text-color; +$list-text-disabled-color: $gl-text-color-disabled; +$list-border-light: #eee; +$list-border: rgba(0, 0, 0, 0.05); $list-text-height: 42px; +$list-warning-row-bg: #fcf8e3; +$list-warning-row-border: #faebcc; +$list-warning-row-color: #8a6d3b; /* * Markdown */ -$md-text-color: $gl-text-color; $md-link-color: $gl-link-color; +$md-area-border: #ddd; /* * Code */ -$code_font_size: 13px; -$code_line_height: 1.5; +$code_font_size: 12px; +$code_line_height: 1.6; /* * Padding @@ -149,22 +151,58 @@ $gl-sidebar-padding: 22px; $row-hover: #f7faff; $row-hover-border: #b2d7ff; $progress-color: #c0392b; -$avatar_radius: 50%; $header-height: 50px; $fixed-layout-width: 1280px; +$limited-layout-width: 990px; $gl-avatar-size: 40px; $error-exclamation-point: #e62958; $border-radius-default: 2px; -$btn-transparent-color: #8f8f8f; $settings-icon-size: 18px; -$provider-btn-group-border: #e5e5e5; $provider-btn-not-active-color: #4688f1; $link-underline-blue: #4a8bee; +$active-item-blue: #4a8bee; $layout-link-gray: #7e7c7c; -$todo-alert-blue: #428bca; $btn-side-margin: 10px; $btn-sm-side-margin: 7px; $btn-xs-side-margin: 5px; +$issue-status-expired: #cea61b; +$issuable-sidebar-color: $gl-text-color-secondary; +$show-aside-bg: #eee; +$show-aside-color: #777; +$show-aside-shadow: #ddd; +$group-path-color: #999; +$namespace-kind-color: #aaa; +$panel-heading-link-color: #777; +$graph-author-email-color: #777; +$count-arrow-border: #dce0e5; +$save-project-loader-color: #555; +$divergence-graph-bar-bg: #ccc; +$divergence-graph-separator-bg: #ccc; +$general-hover-transition-duration: 150ms; +$general-hover-transition-curve: linear; + + +/* +* Common component specific colors +*/ +$hint-color: #999; +$well-pre-bg: #eee; +$well-pre-color: #555; +$loading-color: #555; +$update-author-color: #999; +$user-mention-color: #2fa0bb; +$time-color: #999; +$project-member-show-color: #aaa; +$gl-promo-color: #aaa; +$error-bg: #c67; +$warning-message-bg: #ffffe6; +$warning-message-border: #ed9; +$warning-message-color: #b90; +$control-group-descr-color: #666; +$table-permission-x-bg: #d9edf7; +$username-color: #666; +$description-color: #666; +$profiler-border: #eee; /* tanuki logo colors */ $tanuki-red: #e24329; @@ -195,101 +233,312 @@ $line-removed-dark: #fac5cd; $line-number-old: #f9d7dc; $line-number-new: #ddfbe6; $line-number-select: #fbf2da; -$match-line: $gray-light; -$table-border-gray: #f0f0f0; $line-target-blue: #f6faff; $line-select-yellow: #fcf8e7; $line-select-yellow-dark: #f0e2bd; +$dark-diff-match-bg: rgba(255, 255, 255, 0.3); +$dark-diff-match-color: rgba(255, 255, 255, 0.1); +$file-mode-changed: #777; +$file-mode-changed: #777; +$diff-image-bg: #ddd; +$diff-image-info-color: grey; +$diff-swipe-border: #999; +$diff-view-modes-color: grey; +$diff-view-modes-border: #c1c1c1; /* * Fonts */ $monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; -$regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif; +$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; /* * Dropdowns */ $dropdown-width: 300px; -$dropdown-bg: #fff; $dropdown-link-color: #555; $dropdown-link-hover-bg: $row-hover; $dropdown-empty-row-bg: rgba(#000, .04); -$dropdown-border-color: rgba(#000, .1); +$dropdown-border-color: $border-color; $dropdown-shadow-color: rgba(#000, .1); $dropdown-divider-color: rgba(#000, .1); -$dropdown-header-color: #959494; $dropdown-title-btn-color: #bfbfbf; $dropdown-input-color: #555; +$dropdown-input-fa-color: #c7c7c7; $dropdown-input-focus-border: $focus-border-color; $dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4); $dropdown-loading-bg: rgba(#fff, .6); +$dropdown-chevron-size: 10px; +$dropdown-toggle-active-border-color: darken($border-color, 14%); + -$dropdown-toggle-bg: #fff; -$dropdown-toggle-color: #626262; -$dropdown-toggle-border-color: #eaeaea; -$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%); -$dropdown-toggle-icon-color: #c4c4c4; -$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color; +/* +* Filtered Search +*/ +$dropdown-hover-color: #3b86ff; /* * Buttons */ $btn-active-gray: #ececec; -$btn-placeholder-gray: #c7c7c7; +$btn-active-gray-light: e4e7ed; $btn-white-active: #848484; -$btn-gray-hover: #eee; + +/* +* Badges +*/ +$badge-bg: #eee; +$badge-color: $gl-text-color-secondary; /* * Award emoji */ -$award-emoji-menu-bg: #fff; -$award-emoji-menu-border: #f1f2f4; -$award-emoji-new-btn-icon-color: #dcdcdc; +$award-emoji-menu-shadow: rgba(0,0,0,.175); /* * Search Box */ $search-input-border-color: rgba(#4688f1, .8); $search-input-focus-shadow-color: $dropdown-input-focus-shadow; -$search-input-width: 244px; -$location-badge-color: #aaa; -$location-badge-bg: $gray-normal; +$search-input-width: 220px; $location-badge-active-bg: #4f91f8; $location-icon-color: #e7e9ed; -$location-icon-active-color: #807e7e; /* * Notes */ -$notes-light-color: #8e8e8e; -$notes-action-color: #c3c3c3; -$notes-role-color: #8e8e8e; -$notes-role-border-color: #e4e4e4; - +$notes-light-color: $gl-text-color-secondary; +$notes-role-color: $gl-text-color-secondary; $note-disabled-comment-color: #b2b2b2; -$note-form-border-color: #e5e5e5; -$note-toolbar-color: #959494; +$note-targe3-outside: #fffff0; +$note-targe3-inside: #ffffd3; +$note-line2-border: #ddd; -$zen-control-hover-color: #111; -$calendar-header-color: #b8b8b8; +/* +* Zen +*/ +$zen-control-color: #555; + +/* +* Calendar +*/ $calendar-hover-bg: #ecf3fe; $calendar-border-color: rgba(#000, .1); -$calendar-unselectable-bg: $gray-light; +$calendar-user-contrib-text: #959494; /* * Cycle Analytics */ $cycle-analytics-box-padding: 30px; $cycle-analytics-box-text-color: #8c8c8c; +$cycle-analytics-big-font: 19px; +$cycle-analytics-dark-text: $gl-text-color; +$cycle-analytics-light-gray: #bfbfbf; +$cycle-analytics-dismiss-icon-color: #b2b2b2; /* - * Personal Access Tokens - */ -$personal-access-tokens-disabled-label-color: #bbb; +* CI +*/ +$ci-skipped-color: #888; + +/* +* Boards +*/ +$issue-boards-font-size: 14px; +$issue-boards-card-shadow: rgba(186, 186, 186, 0.5); + +/* +* Avatar +*/ +$avatar_radius: 50%; +$avatar-border: rgba(0, 0, 0, .1); +$gl-avatar-size: 40px; + +/* +* Builds +*/ +$builds-trace-bg: #111; + +/* +* Callout +*/ +$callout-danger-bg: #fdf7f7; +$callout-danger-border: #eed3d7; +$callout-danger-color: #b94a48; +$callout-warning-bg: #faf8f0; +$callout-warning-border: #faebcc; +$callout-warning-color: #8a6d3b; +$callout-info-bg: #f4f8fa; +$callout-info-border: #bce8f1; +$callout-info-color: #34789a; +$callout-success-bg: #dff0d8; +$callout-success-border: #5ca64d; +$callout-success-color: #3c763d; + +/* +* Commit Page +*/ +$commit-max-width-marker-color: rgba(0, 0, 0, 0.0); +$commit-message-text-area-bg: rgba(0, 0, 0, 0.0); + +/* +* Common +*/ +$common-gray: $gl-text-color; +$common-gray-light: #bbb; +$common-gray-dark: #444; +$common-red: $gl-text-red; +$common-green: $gl-text-green; + +/* +* Editor +*/ +$editor-cancel-color: #b94a48; + +/* +* Events +*/ +$events-pre-color: #777; +$events-note-icon-color: #777; +$events-body-border: #ddd; + +/* +* Files +*/ +$file-image-bg: #eee; +$blob-bg: #eee; +$blame-border: #eee; +$blame-line-numbers-border: #ddd; +$logs-bg: #eee; +$logs-li-color: #888; +$logs-p-color: #333; -$ci-output-bg: #1d1f21; -$ci-text-color: #c5c8c6; +/* +* Forms +*/ +$input-danger-bg: #f2dede; +$input-danger-border: #d66; +$input-group-addon-bg: #f7f8fa; +$gl-field-focus-shadow: rgba(0, 0, 0, 0.075); +$gl-field-focus-shadow-error: rgba(210, 40, 82, 0.6); + +/* +* Help +*/ +$document-index-color: #888; +$help-shortcut-color: #999; +$help-shortcut-mapping-color: #555; +$help-shortcut-header-color: #333; + +/* +* Issues +*/ +$issues-today-bg: #f3fff2; +$issues-today-border: #e1e8d5; +$compare-display-color: #888; + +/* +* jQuery UI +*/ +$jq-ui-border: #ddd; +$jq-ui-default-color: #777; + +/* +* Label +*/ +$label-gray-bg: #f8fafc; +$label-inverse-bg: #333; +$label-remove-border: rgba(0, 0, 0, .1); +$label-border-radius: 100px; + +/* +* Lint +*/ +$lint-incorrect-color: red; +$lint-correct-color: #47a447; + +/* +* Login +*/ +$login-brand-holder-color: #888; +$login-devise-error-color: #a00; + +/* +* Nav +*/ +$nav-link-gray: #959494; +$nav-badge-bg: #eee; +$nav-toggle-gray: #666; + +/* +* Notify +*/ +$notify-details: #777; +$notify-footer: #777; +$notify-new-file: #090; +$notify-deleted-file: #b00; -$issue-boards-font-size: 15px; +/* +* Projects +*/ +$project-option-descr-color: #54565b; +$project-breadcrumb-color: #999; +$project-private-forks-notice-odd: #2aa056; +$project-network-controls-color: #888; + +/* +* Runners +*/ +$runner-state-shared-bg: #32b186; +$runner-state-specific-bg: #3498db; +$runner-status-online-color: $green-normal; +$runner-status-offline-color: $gray-darkest; +$runner-status-paused-color: $red-normal; + +/* +Stat Graph +*/ +$stat-graph-common-bg: #f3f3f3; +$stat-graph-area-fill: #1db34f; +$stat-graph-axis-fill: #aaa; +$stat-graph-orange-fill: #f17f49; +$stat-graph-selection-fill: #333; +$stat-graph-selection-stroke: #333; + +/* +* Selects +*/ +$select2-drop-shadow1: rgba(76, 86, 103, 0.247059); +$select2-drop-shadow2: rgba(31, 37, 50, 0.317647); + + +/* +* Todo +*/ +$todo-alert-blue: #428bca; +$todo-body-pre-color: #777; +$todo-body-border: #ddd; + +/* +* Typography +*/ +$kdb-bg: #fcfcfc; +$kdb-color: #555; +$kdb-border: #ccc; +$kdb-border-bottom: #bbb; +$kdb-shadow: #bbb; +$body-text-shadow: rgba(255,255,255,0.01); + +/* +* UI Dev Kit +*/ +$ui-dev-kit-example-color: #bbb; +$ui-dev-kit-example-border: #ddd; + +/* +Pipeline Graph +*/ +$stage-hover-bg: #eaf3fc; +$stage-hover-border: #d1e7fc; +$action-icon-color: #d6d6d6;
\ No newline at end of file diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss new file mode 100644 index 00000000000..32eb750180f --- /dev/null +++ b/app/assets/stylesheets/framework/wells.scss @@ -0,0 +1,58 @@ +.info-well { + background: $gray-light; + color: $gl-text-color; + border: 1px solid $border-color; + border-radius: $border-radius-default; + + .well-segment { + padding: $gl-padding; + + &:not(:last-of-type) { + border-bottom: 1px solid $well-inner-border; + } + + &.branch-info { + .monospace, + .commit-info { + margin-left: 4px; + } + } + } + + .icon-container { + display: inline-block; + margin-right: 8px; + + svg { + position: relative; + top: 2px; + height: 16px; + width: 16px; + } + + &.commit-icon { + svg { + path { + fill: $gl-text-color; + } + } + } + } + + .label.label-gray { + background-color: $well-expand-item; + } +} + +.light-well { + background-color: $gray-light; + padding: 15px; +} + +.well-centered { + h1 { + font-weight: normal; + text-align: center; + font-size: 48px; + } +} diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index ff02ebdd34c..97ade638db6 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -1,6 +1,6 @@ .zen-backdrop { &.fullscreen { - background-color: white; + background-color: $white-light; position: fixed; top: 0; bottom: 0; @@ -12,7 +12,7 @@ border: none; box-shadow: none; border-radius: 0; - color: #000; + color: $black; font-size: 20px; line-height: 26px; padding: 30px; @@ -34,13 +34,13 @@ .zen-control { padding: 0; - color: #555; + color: $zen-control-color; background: none; border: 0; } .zen-control-full { - color: $note-toolbar-color; + color: $gl-text-color-secondary; &:hover { color: $gl-link-color; @@ -57,6 +57,6 @@ font-size: 36px; &:hover { - color: $zen-control-hover-color; + color: $black; } } diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index d22d9b01495..cb923166b25 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -1,27 +1,109 @@ /* https://github.com/MozMorris/tomorrow-pygments */ + +/* +* Dark syntax colors +*/ +$dark-new-bg: rgba(51, 255, 51, 0.1); +$dark-new-idiff: rgba(51, 255, 51, 0.2); +$dark-old-bg: rgba(255, 51, 51, 0.2); +$dark-old-idiff: rgba(255, 51, 51, 0.25); +$dark-border: #808080; +$dark-code-border: #666; +$dark-main-bg: #1d1f21; +$dark-main-color: #1d1f21; +$dark-line-color: #c5c8c6; +$dark-line-num-color: rgba(255, 255, 255, 0.3); +$dark-diff-not-empty-bg: #557; +$dark-highlight-bg: #ffe792; +$dark-highlight-color: $black; +$dark-pre-hll-bg: #373b41; +$dark-hll-bg: #373b41; +$dark-c: #969896; +$dark-err: #c66; +$dark-k: #b294bb; +$dark-l: #de935f; +$dark-n: #c5c8c6; +$dark-o: #8abeb7; +$dark-p: #c5c8c6; +$dark-cm: #969896; +$dark-cp: #969896; +$dark-c1: #969896; +$dark-cs: #969896; +$dark-gd: #c66; +$dark-gh: #c5c8c6; +$dark-gi: #b5bd68; +$dark-gp: #969896; +$dark-gu: #8abeb7; +$dark-kc: #b294bb; +$dark-kd: #b294bb; +$dark-kn: #8abeb7; +$dark-kp: #b294bb; +$dark-kr: #b294bb; +$dark-kt: #f0c674; +$dark-ld: #b5bd68; +$dark-m: #de935f; +$dark-s: #b5bd68; +$dark-na: #81a2be; +$dark-nb: #c5c8c6; +$dark-nc: #f0c674; +$dark-no: #c66; +$dark-nd: #8abeb7; +$dark-ni: #c5c8c6; +$dark-ne: #c66; +$dark-nf: #81a2be; +$dark-nl: #c5c8c6; +$dark-nn: #f0c674; +$dark-nx: #81a2be; +$dark-py: #c5c8c6; +$dark-nt: #8abeb7; +$dark-nv: #c66; +$dark-ow: #8abeb7; +$dark-w: #c5c8c6; +$dark-mf: #de935f; +$dark-mh: #de935f; +$dark-mi: #de935f; +$dark-mo: #de935f; +$dark-sb: #b5bd68; +$dark-sc: #c5c8c6; +$dark-sd: #969896; +$dark-s2: #b5bd68; +$dark-se: #de935f; +$dark-sh: #b5bd68; +$dark-si: #de935f; +$dark-sx: #b5bd68; +$dark-sr: #b5bd68; +$dark-s1: #b5bd68; +$dark-ss: #b5bd68; +$dark-bp: #c5c8c6; +$dark-vc: #c66; +$dark-vg: #c66; +$dark-vi: #c66; +$dark-il: #de935f; + .code.dark { // Line numbers .line-numbers, .diff-line-num { - background-color: #1d1f21; + background-color: $dark-main-bg; } .diff-line-num, .diff-line-num a { - color: rgba(255, 255, 255, 0.3); + color: $dark-main-color; + color: $dark-line-num-color; } // Code itself pre.code, .diff-line-num { - border-color: #666; + border-color: $dark-code-border; } &, pre.code, .line_holder .line_content { - background-color: #1d1f21; - color: #c5c8c6; + background-color: $dark-main-bg; + color: $dark-line-color; } // Diff line @@ -32,18 +114,18 @@ td.diff-line-num.hll:not(.empty-cell), td.line_content.hll:not(.empty-cell) { - background-color: #557; - border-color: darken(#557, 15%); + background-color: $dark-diff-not-empty-bg; + border-color: darken($dark-diff-not-empty-bg, 15%); } .diff-line-num.new, .line_content.new { - @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080); + @include diff_background($dark-new-bg, $dark-new-idiff, $dark-border); } .diff-line-num.old, .line_content.old { - @include diff_background(rgba(255, 51, 51, 0.2), rgba(255, 51, 51, 0.25), #808080); + @include diff_background($dark-old-bg, $dark-old-idiff, $dark-border); } .line_content.match { @@ -53,77 +135,77 @@ // highlight line via anchor pre .hll { - background-color: #557 !important; + background-color: $dark-pre-hll-bg !important; } // Search result highlight span.highlight_word { - background-color: #ffe792 !important; - color: #000 !important; + background-color: $dark-highlight-bg !important; + color: $dark-highlight-color !important; } - .hll { background-color: #373b41; } - .c { color: #969896; } /* Comment */ - .err { color: #c66; } /* Error */ - .k { color: #b294bb; } /* Keyword */ - .l { color: #de935f; } /* Literal */ - .n { color: #c5c8c6; } /* Name */ - .o { color: #8abeb7; } /* Operator */ - .p { color: #c5c8c6; } /* Punctuation */ - .cm { color: #969896; } /* Comment.Multiline */ - .cp { color: #969896; } /* Comment.Preproc */ - .c1 { color: #969896; } /* Comment.Single */ - .cs { color: #969896; } /* Comment.Special */ - .gd { color: #c66; } /* Generic.Deleted */ + .hll { background-color: $dark-hll-bg; } + .c { color: $dark-c; } /* Comment */ + .err { color: $dark-err; } /* Error */ + .k { color: $dark-k; } /* Keyword */ + .l { color: $dark-l; } /* Literal */ + .n { color: $dark-n; } /* Name */ + .o { color: $dark-o; } /* Operator */ + .p { color: $dark-p; } /* Punctuation */ + .cm { color: $dark-cm; } /* Comment.Multiline */ + .cp { color: $dark-cp; } /* Comment.Preproc */ + .c1 { color: $dark-c1; } /* Comment.Single */ + .cs { color: $dark-cs; } /* Comment.Special */ + .gd { color: $dark-gd; } /* Generic.Deleted */ .ge { font-style: italic; } /* Generic.Emph */ - .gh { color: #c5c8c6; font-weight: bold; } /* Generic.Heading */ - .gi { color: #b5bd68; } /* Generic.Inserted */ - .gp { color: #969896; font-weight: bold; } /* Generic.Prompt */ + .gh { color: $dark-gh; font-weight: bold; } /* Generic.Heading */ + .gi { color: $dark-gi; } /* Generic.Inserted */ + .gp { color: $dark-gp; font-weight: bold; } /* Generic.Prompt */ .gs { font-weight: bold; } /* Generic.Strong */ - .gu { color: #8abeb7; font-weight: bold; } /* Generic.Subheading */ - .kc { color: #b294bb; } /* Keyword.Constant */ - .kd { color: #b294bb; } /* Keyword.Declaration */ - .kn { color: #8abeb7; } /* Keyword.Namespace */ - .kp { color: #b294bb; } /* Keyword.Pseudo */ - .kr { color: #b294bb; } /* Keyword.Reserved */ - .kt { color: #f0c674; } /* Keyword.Type */ - .ld { color: #b5bd68; } /* Literal.Date */ - .m { color: #de935f; } /* Literal.Number */ - .s { color: #b5bd68; } /* Literal.String */ - .na { color: #81a2be; } /* Name.Attribute */ - .nb { color: #c5c8c6; } /* Name.Builtin */ - .nc { color: #f0c674; } /* Name.Class */ - .no { color: #c66; } /* Name.Constant */ - .nd { color: #8abeb7; } /* Name.Decorator */ - .ni { color: #c5c8c6; } /* Name.Entity */ - .ne { color: #c66; } /* Name.Exception */ - .nf { color: #81a2be; } /* Name.Function */ - .nl { color: #c5c8c6; } /* Name.Label */ - .nn { color: #f0c674; } /* Name.Namespace */ - .nx { color: #81a2be; } /* Name.Other */ - .py { color: #c5c8c6; } /* Name.Property */ - .nt { color: #8abeb7; } /* Name.Tag */ - .nv { color: #c66; } /* Name.Variable */ - .ow { color: #8abeb7; } /* Operator.Word */ - .w { color: #c5c8c6; } /* Text.Whitespace */ - .mf { color: #de935f; } /* Literal.Number.Float */ - .mh { color: #de935f; } /* Literal.Number.Hex */ - .mi { color: #de935f; } /* Literal.Number.Integer */ - .mo { color: #de935f; } /* Literal.Number.Oct */ - .sb { color: #b5bd68; } /* Literal.String.Backtick */ - .sc { color: #c5c8c6; } /* Literal.String.Char */ - .sd { color: #969896; } /* Literal.String.Doc */ - .s2 { color: #b5bd68; } /* Literal.String.Double */ - .se { color: #de935f; } /* Literal.String.Escape */ - .sh { color: #b5bd68; } /* Literal.String.Heredoc */ - .si { color: #de935f; } /* Literal.String.Interpol */ - .sx { color: #b5bd68; } /* Literal.String.Other */ - .sr { color: #b5bd68; } /* Literal.String.Regex */ - .s1 { color: #b5bd68; } /* Literal.String.Single */ - .ss { color: #b5bd68; } /* Literal.String.Symbol */ - .bp { color: #c5c8c6; } /* Name.Builtin.Pseudo */ - .vc { color: #c66; } /* Name.Variable.Class */ - .vg { color: #c66; } /* Name.Variable.Global */ - .vi { color: #c66; } /* Name.Variable.Instance */ - .il { color: #de935f; } /* Literal.Number.Integer.Long */ + .gu { color: $dark-gu; font-weight: bold; } /* Generic.Subheading */ + .kc { color: $dark-kc; } /* Keyword.Constant */ + .kd { color: $dark-kd; } /* Keyword.Declaration */ + .kn { color: $dark-kn; } /* Keyword.Namespace */ + .kp { color: $dark-kp; } /* Keyword.Pseudo */ + .kr { color: $dark-kr; } /* Keyword.Reserved */ + .kt { color: $dark-kt; } /* Keyword.Type */ + .ld { color: $dark-ld; } /* Literal.Date */ + .m { color: $dark-m; } /* Literal.Number */ + .s { color: $dark-s; } /* Literal.String */ + .na { color: $dark-na; } /* Name.Attribute */ + .nb { color: $dark-nb; } /* Name.Builtin */ + .nc { color: $dark-nc; } /* Name.Class */ + .no { color: $dark-no; } /* Name.Constant */ + .nd { color: $dark-nd; } /* Name.Decorator */ + .ni { color: $dark-ni; } /* Name.Entity */ + .ne { color: $dark-ne; } /* Name.Exception */ + .nf { color: $dark-nf; } /* Name.Function */ + .nl { color: $dark-nl; } /* Name.Label */ + .nn { color: $dark-nn; } /* Name.Namespace */ + .nx { color: $dark-nx; } /* Name.Other */ + .py { color: $dark-py; } /* Name.Property */ + .nt { color: $dark-nt; } /* Name.Tag */ + .nv { color: $dark-nv; } /* Name.Variable */ + .ow { color: $dark-ow; } /* Operator.Word */ + .w { color: $dark-w; } /* Text.Whitespace */ + .mf { color: $dark-mf; } /* Literal.Number.Float */ + .mh { color: $dark-mh; } /* Literal.Number.Hex */ + .mi { color: $dark-mi; } /* Literal.Number.Integer */ + .mo { color: $dark-mo; } /* Literal.Number.Oct */ + .sb { color: $dark-sb; } /* Literal.String.Backtick */ + .sc { color: $dark-sc; } /* Literal.String.Char */ + .sd { color: $dark-sd; } /* Literal.String.Doc */ + .s2 { color: $dark-s2; } /* Literal.String.Double */ + .se { color: $dark-se; } /* Literal.String.Escape */ + .sh { color: $dark-sh; } /* Literal.String.Heredoc */ + .si { color: $dark-si; } /* Literal.String.Interpol */ + .sx { color: $dark-sx; } /* Literal.String.Other */ + .sr { color: $dark-sr; } /* Literal.String.Regex */ + .s1 { color: $dark-s1; } /* Literal.String.Single */ + .ss { color: $dark-ss; } /* Literal.String.Symbol */ + .bp { color: $dark-bp; } /* Name.Builtin.Pseudo */ + .vc { color: $dark-vc; } /* Name.Variable.Class */ + .vg { color: $dark-vg; } /* Name.Variable.Global */ + .vi { color: $dark-vi; } /* Name.Variable.Instance */ + .il { color: $dark-il; } /* Literal.Number.Integer.Long */ } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index db8da8aab10..d8510baad8a 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -1,27 +1,108 @@ /* https://github.com/richleland/pygments-css/blob/master/monokai.css */ + +/* +* Monokai Colors +*/ +$monokai-bg: #272822; +$monokai-border: #555; +$monokai-text-color: #f8f8f2; +$monokai-line-num-color: rgba(255, 255, 255, 0.3); +$monokai-line-empty-bg: #49483e; +$monokai-line-empty-border: darken($monokai-line-empty-bg, 15%); +$monokai-diff-border: #808080; +$monokai-highlight-bg: #ffe792; + +$monokai-new-bg: rgba(166, 226, 46, 0.1); +$monokai-new-idiff: rgba(166, 226, 46, 0.15); + +$monokai-old-bg: rgba(254, 147, 140, 0.15); +$monokai-old-idiff: rgba(254, 147, 140, 0.2); + +$monokai-hll: #49483e; +$monokai-c: #75715e; +$monokai-err-color: #960050; +$monokai-err-bg: #1e0010; +$monokai-k: #66d9ef; +$monokai-l: #ae81ff; +$monokai-n: #f8f8f2; +$monokai-o: #f92672; +$monokai-p: #f8f8f2; +$monokai-cm: #75715e; +$monokai-cp: #75715e; +$monokai-c1: #75715e; +$monokai-cs: #75715e; +$monokai-kc: #66d9ef; +$monokai-kd: #66d9ef; +$monokai-kn: #f92672; +$monokai-kp: #66d9ef; +$monokai-kr: #66d9ef; +$monokai-kt: #66d9ef; +$monokai-ld: #e6db74; +$monokai-m: #ae81ff; +$monokai-s: #e6db74; +$monokai-na: #a6e22e; +$monokai-nb: #f8f8f2; +$monokai-nc: #a6e22e; +$monokai-no: #66d9ef; +$monokai-nd: #a6e22e; +$monokai-ni: #f8f8f2; +$monokai-ne: #a6e22e; +$monokai-nf: #a6e22e; +$monokai-nl: #f8f8f2; +$monokai-nn: #f8f8f2; +$monokai-nx: #a6e22e; +$monokai-py: #f8f8f2; +$monokai-nt: #f92672; +$monokai-nv: #f8f8f2; +$monokai-ow: #f92672; +$monokai-w: #f8f8f2; +$monokai-mf: #ae81ff; +$monokai-mh: #ae81ff; +$monokai-mi: #ae81ff; +$monokai-mo: #ae81ff; +$monokai-sb: #e6db74; +$monokai-sc: #e6db74; +$monokai-sd: #e6db74; +$monokai-s2: #e6db74; +$monokai-se: #ae81ff; +$monokai-sh: #e6db74; +$monokai-si: #e6db74; +$monokai-sx: #e6db74; +$monokai-sr: #e6db74; +$monokai-s1: #e6db74; +$monokai-ss: #e6db74; +$monokai-bp: #f8f8f2; +$monokai-vc: #f8f8f2; +$monokai-vg: #f8f8f2; +$monokai-vi: #f8f8f2; +$monokai-il: #ae81ff; +$monokai-gu: #75715e; +$monokai-gd: #f92672; +$monokai-gi: #a6e22e; + .code.monokai { // Line numbers .line-numbers, .diff-line-num { - background-color: #272822; + background-color: $monokai-bg; } .diff-line-num, .diff-line-num a { - color: rgba(255, 255, 255, 0.3); + color: $monokai-line-num-color; } // Code itself pre.code, .diff-line-num { - border-color: #555; + border-color: $monokai-border; } &, pre.code, .line_holder .line_content { - background-color: #272822; - color: #f8f8f2; + background-color: $monokai-bg; + color: $monokai-text-color; } // Diff line @@ -32,18 +113,18 @@ td.diff-line-num.hll:not(.empty-cell), td.line_content.hll:not(.empty-cell) { - background-color: #49483e; - border-color: darken(#49483e, 15%); + background-color: $monokai-line-empty-bg; + border-color: $monokai-line-empty-border; } .diff-line-num.new, .line_content.new { - @include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080); + @include diff_background($monokai-new-bg, $monokai-new-idiff, $monokai-diff-border); } .diff-line-num.old, .line_content.old { - @include diff_background(rgba(254, 147, 140, 0.15), rgba(254, 147, 140, 0.2), #808080); + @include diff_background($monokai-old-bg, $monokai-old-idiff, $monokai-diff-border); } .line_content.match { @@ -53,75 +134,75 @@ // highlight line via anchor pre .hll { - background-color: #49483e !important; + background-color: $monokai-hll !important; } // Search result highlight span.highlight_word { - background-color: #ffe792 !important; - color: #000 !important; + background-color: $monokai-highlight-bg !important; + color: $black !important; } - .hll { background-color: #49483e; } - .c { color: #75715e; } /* Comment */ - .err { color: #960050; background-color: #1e0010; } /* Error */ - .k { color: #66d9ef; } /* Keyword */ - .l { color: #ae81ff; } /* Literal */ - .n { color: #f8f8f2; } /* Name */ - .o { color: #f92672; } /* Operator */ - .p { color: #f8f8f2; } /* Punctuation */ - .cm { color: #75715e; } /* Comment.Multiline */ - .cp { color: #75715e; } /* Comment.Preproc */ - .c1 { color: #75715e; } /* Comment.Single */ - .cs { color: #75715e; } /* Comment.Special */ + .hll { background-color: $monokai-hll; } + .c { color: $monokai-c; } /* Comment */ + .err { color: $monokai-err-color; background-color: $monokai-err-bg; } /* Error */ + .k { color: $monokai-k; } /* Keyword */ + .l { color: $monokai-l; } /* Literal */ + .n { color: $monokai-n; } /* Name */ + .o { color: $monokai-o; } /* Operator */ + .p { color: $monokai-p; } /* Punctuation */ + .cm { color: $monokai-cm; } /* Comment.Multiline */ + .cp { color: $monokai-cp; } /* Comment.Preproc */ + .c1 { color: $monokai-c1; } /* Comment.Single */ + .cs { color: $monokai-cs; } /* Comment.Special */ .ge { font-style: italic; } /* Generic.Emph */ .gs { font-weight: bold; } /* Generic.Strong */ - .kc { color: #66d9ef; } /* Keyword.Constant */ - .kd { color: #66d9ef; } /* Keyword.Declaration */ - .kn { color: #f92672; } /* Keyword.Namespace */ - .kp { color: #66d9ef; } /* Keyword.Pseudo */ - .kr { color: #66d9ef; } /* Keyword.Reserved */ - .kt { color: #66d9ef; } /* Keyword.Type */ - .ld { color: #e6db74; } /* Literal.Date */ - .m { color: #ae81ff; } /* Literal.Number */ - .s { color: #e6db74; } /* Literal.String */ - .na { color: #a6e22e; } /* Name.Attribute */ - .nb { color: #f8f8f2; } /* Name.Builtin */ - .nc { color: #a6e22e; } /* Name.Class */ - .no { color: #66d9ef; } /* Name.Constant */ - .nd { color: #a6e22e; } /* Name.Decorator */ - .ni { color: #f8f8f2; } /* Name.Entity */ - .ne { color: #a6e22e; } /* Name.Exception */ - .nf { color: #a6e22e; } /* Name.Function */ - .nl { color: #f8f8f2; } /* Name.Label */ - .nn { color: #f8f8f2; } /* Name.Namespace */ - .nx { color: #a6e22e; } /* Name.Other */ - .py { color: #f8f8f2; } /* Name.Property */ - .nt { color: #f92672; } /* Name.Tag */ - .nv { color: #f8f8f2; } /* Name.Variable */ - .ow { color: #f92672; } /* Operator.Word */ - .w { color: #f8f8f2; } /* Text.Whitespace */ - .mf { color: #ae81ff; } /* Literal.Number.Float */ - .mh { color: #ae81ff; } /* Literal.Number.Hex */ - .mi { color: #ae81ff; } /* Literal.Number.Integer */ - .mo { color: #ae81ff; } /* Literal.Number.Oct */ - .sb { color: #e6db74; } /* Literal.String.Backtick */ - .sc { color: #e6db74; } /* Literal.String.Char */ - .sd { color: #e6db74; } /* Literal.String.Doc */ - .s2 { color: #e6db74; } /* Literal.String.Double */ - .se { color: #ae81ff; } /* Literal.String.Escape */ - .sh { color: #e6db74; } /* Literal.String.Heredoc */ - .si { color: #e6db74; } /* Literal.String.Interpol */ - .sx { color: #e6db74; } /* Literal.String.Other */ - .sr { color: #e6db74; } /* Literal.String.Regex */ - .s1 { color: #e6db74; } /* Literal.String.Single */ - .ss { color: #e6db74; } /* Literal.String.Symbol */ - .bp { color: #f8f8f2; } /* Name.Builtin.Pseudo */ - .vc { color: #f8f8f2; } /* Name.Variable.Class */ - .vg { color: #f8f8f2; } /* Name.Variable.Global */ - .vi { color: #f8f8f2; } /* Name.Variable.Instance */ - .il { color: #ae81ff; } /* Literal.Number.Integer.Long */ - .gu { color: #75715e; } /* Generic.Subheading & Diff Unified/Comment? */ - .gd { color: #f92672; } /* Generic.Deleted & Diff Deleted */ - .gi { color: #a6e22e; } /* Generic.Inserted & Diff Inserted */ + .kc { color: $monokai-kc; } /* Keyword.Constant */ + .kd { color: $monokai-kd; } /* Keyword.Declaration */ + .kn { color: $monokai-kn; } /* Keyword.Namespace */ + .kp { color: $monokai-kp; } /* Keyword.Pseudo */ + .kr { color: $monokai-kr; } /* Keyword.Reserved */ + .kt { color: $monokai-kt; } /* Keyword.Type */ + .ld { color: $monokai-ld; } /* Literal.Date */ + .m { color: $monokai-m; } /* Literal.Number */ + .s { color: $monokai-s; } /* Literal.String */ + .na { color: $monokai-na; } /* Name.Attribute */ + .nb { color: $monokai-nb; } /* Name.Builtin */ + .nc { color: $monokai-nc; } /* Name.Class */ + .no { color: $monokai-no; } /* Name.Constant */ + .nd { color: $monokai-nd; } /* Name.Decorator */ + .ni { color: $monokai-ni; } /* Name.Entity */ + .ne { color: $monokai-ne; } /* Name.Exception */ + .nf { color: $monokai-nf; } /* Name.Function */ + .nl { color: $monokai-nl; } /* Name.Label */ + .nn { color: $monokai-nn; } /* Name.Namespace */ + .nx { color: $monokai-nx; } /* Name.Other */ + .py { color: $monokai-py; } /* Name.Property */ + .nt { color: $monokai-nt; } /* Name.Tag */ + .nv { color: $monokai-nv; } /* Name.Variable */ + .ow { color: $monokai-ow; } /* Operator.Word */ + .w { color: $monokai-w; } /* Text.Whitespace */ + .mf { color: $monokai-mf; } /* Literal.Number.Float */ + .mh { color: $monokai-mh; } /* Literal.Number.Hex */ + .mi { color: $monokai-mi; } /* Literal.Number.Integer */ + .mo { color: $monokai-mo; } /* Literal.Number.Oct */ + .sb { color: $monokai-sb; } /* Literal.String.Backtick */ + .sc { color: $monokai-sc; } /* Literal.String.Char */ + .sd { color: $monokai-sd; } /* Literal.String.Doc */ + .s2 { color: $monokai-s2; } /* Literal.String.Double */ + .se { color: $monokai-se; } /* Literal.String.Escape */ + .sh { color: $monokai-sh; } /* Literal.String.Heredoc */ + .si { color: $monokai-si; } /* Literal.String.Interpol */ + .sx { color: $monokai-sx; } /* Literal.String.Other */ + .sr { color: $monokai-sr; } /* Literal.String.Regex */ + .s1 { color: $monokai-s1; } /* Literal.String.Single */ + .ss { color: $monokai-ss; } /* Literal.String.Symbol */ + .bp { color: $monokai-bp; } /* Name.Builtin.Pseudo */ + .vc { color: $monokai-vc; } /* Name.Variable.Class */ + .vg { color: $monokai-vg; } /* Name.Variable.Global */ + .vi { color: $monokai-vi; } /* Name.Variable.Instance */ + .il { color: $monokai-il; } /* Literal.Number.Integer.Long */ + .gu { color: $monokai-gu; } /* Generic.Subheading & Diff Unified/Comment? */ + .gd { color: $monokai-gd; } /* Generic.Deleted & Diff Deleted */ + .gi { color: $monokai-gi; } /* Generic.Inserted & Diff Inserted */ } diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index a87333146de..874aecb5e16 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -1,27 +1,112 @@ /* https://gist.github.com/qguv/7936275 */ + +/* +* Solarized dark colors +*/ +$solarized-dark-new-bg: rgba(133, 153, 0, 0.15); +$solarized-dark-new-idiff: rgba(133, 153, 0, 0.25); +$solarized-dark-old-bg: rgba(220, 50, 47, 0.3); +$solarized-dark-old-idiff: rgba(220, 50, 47, 0.25); +$solarized-dark-border: #113b46; +$solarized-dark-pre-bg: #002b36; +$solarized-dark-pre-color: #93a1a1; +$solarized-dark-pre-border: #113b46; +$solarized-dark-line-bg: #002b36; +$solarized-dark-line-color: rgba(255, 255, 255, 0.3); +$solarized-dark-highlight: #094554; +$solarized-dark-hll-bg: #174652; +$solarized-dark-c: #586e75; +$solarized-dark-err: #93a1a1; +$solarized-dark-g: #93a1a1; +$solarized-dark-k: #859900; +$solarized-dark-l: #93a1a1; +$solarized-dark-n: #93a1a1; +$solarized-dark-o: #859900; +$solarized-dark-x: #cb4b16; +$solarized-dark-p: #93a1a1; +$solarized-dark-cm: #586e75; +$solarized-dark-cp: #859900; +$solarized-dark-c1: #586e75; +$solarized-dark-cs: #859900; +$solarized-dark-gd: #2aa198; +$solarized-dark-ge: #93a1a1; +$solarized-dark-gr: #dc322f; +$solarized-dark-gh: #cb4b16; +$solarized-dark-gi: #859900; +$solarized-dark-go: #93a1a1; +$solarized-dark-gp: #93a1a1; +$solarized-dark-gs: #93a1a1; +$solarized-dark-gu: #cb4b16; +$solarized-dark-gt: #93a1a1; +$solarized-dark-kc: #cb4b16; +$solarized-dark-kd: #268bd2; +$solarized-dark-kn: #859900; +$solarized-dark-kp: #859900; +$solarized-dark-kr: #268bd2; +$solarized-dark-kt: #dc322f; +$solarized-dark-ld: #93a1a1; +$solarized-dark-m: #2aa198; +$solarized-dark-s: #2aa198; +$solarized-dark-na: #93a1a1; +$solarized-dark-nb: #b58900; +$solarized-dark-nc: #268bd2; +$solarized-dark-no: #cb4b16; +$solarized-dark-nd: #268bd2; +$solarized-dark-ni: #cb4b16; +$solarized-dark-ne: #cb4b16; +$solarized-dark-nf: #268bd2; +$solarized-dark-nl: #93a1a1; +$solarized-dark-nn: #93a1a1; +$solarized-dark-nx: #93a1a1; +$solarized-dark-py: #93a1a1; +$solarized-dark-nt: #268bd2; +$solarized-dark-nv: #268bd2; +$solarized-dark-ow: #859900; +$solarized-dark-w: #93a1a1; +$solarized-dark-mf: #2aa198; +$solarized-dark-mh: #2aa198; +$solarized-dark-mi: #2aa198; +$solarized-dark-mo: #2aa198; +$solarized-dark-sb: #586e75; +$solarized-dark-sc: #2aa198; +$solarized-dark-sd: #93a1a1; +$solarized-dark-s2: #2aa198; +$solarized-dark-se: #cb4b16; +$solarized-dark-sh: #93a1a1; +$solarized-dark-si: #2aa198; +$solarized-dark-sx: #2aa198; +$solarized-dark-sr: #dc322f; +$solarized-dark-s1: #2aa198; +$solarized-dark-ss: #2aa198; +$solarized-dark-bp: #268bd2; +$solarized-dark-vc: #268bd2; +$solarized-dark-vg: #268bd2; +$solarized-dark-vi: #268bd2; +$solarized-dark-il: #2aa198; + .code.solarized-dark { // Line numbers .line-numbers, .diff-line-num { - background-color: #002b36; + background-color: $solarized-dark-line-bg; } .diff-line-num, .diff-line-num a { - color: rgba(255, 255, 255, 0.3); + color: $solarized-dark-line-color; } // Code itself pre.code, .diff-line-num { - border-color: #113b46; + border-color: $solarized-dark-pre-border; } &, pre.code, .line_holder .line_content { - background-color: #002b36; - color: #93a1a1; + background-color: $solarized-dark-pre-bg; + color: $solarized-dark-pre-color; } // Diff line @@ -32,18 +117,18 @@ td.diff-line-num.hll:not(.empty-cell), td.line_content.hll:not(.empty-cell) { - background-color: #174652; - border-color: darken(#174652, 15%); + background-color: $solarized-dark-hll-bg; + border-color: darken($solarized-dark-hll-bg, 15%); } .diff-line-num.new, .line_content.new { - @include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46); + @include diff_background($solarized-dark-new-bg, $solarized-dark-new-idiff, $solarized-dark-border); } .diff-line-num.old, .line_content.old { - @include diff_background(rgba(220, 50, 47, 0.3), rgba(220, 50, 47, 0.25), #113b46); + @include diff_background($solarized-dark-old-bg, $solarized-dark-old-idiff, $solarized-dark-border); } .line_content.match { @@ -53,12 +138,12 @@ // highlight line via anchor pre .hll { - background-color: #174652 !important; + background-color: $solarized-dark-hll-bg !important; } // Search result highlight span.highlight_word { - background-color: #094554 !important; + background-color: $solarized-dark-highlight !important; } /* Solarized Dark @@ -79,72 +164,72 @@ green #859900 operators, other keywords */ - .c { color: #586e75; } /* Comment */ - .err { color: #93a1a1; } /* Error */ - .g { color: #93a1a1; } /* Generic */ - .k { color: #859900; } /* Keyword */ - .l { color: #93a1a1; } /* Literal */ - .n { color: #93a1a1; } /* Name */ - .o { color: #859900; } /* Operator */ - .x { color: #cb4b16; } /* Other */ - .p { color: #93a1a1; } /* Punctuation */ - .cm { color: #586e75; } /* Comment.Multiline */ - .cp { color: #859900; } /* Comment.Preproc */ - .c1 { color: #586e75; } /* Comment.Single */ - .cs { color: #859900; } /* Comment.Special */ - .gd { color: #2aa198; } /* Generic.Deleted */ - .ge { color: #93a1a1; font-style: italic; } /* Generic.Emph */ - .gr { color: #dc322f; } /* Generic.Error */ - .gh { color: #cb4b16; } /* Generic.Heading */ - .gi { color: #859900; } /* Generic.Inserted */ - .go { color: #93a1a1; } /* Generic.Output */ - .gp { color: #93a1a1; } /* Generic.Prompt */ - .gs { color: #93a1a1; font-weight: bold; } /* Generic.Strong */ - .gu { color: #cb4b16; } /* Generic.Subheading */ - .gt { color: #93a1a1; } /* Generic.Traceback */ - .kc { color: #cb4b16; } /* Keyword.Constant */ - .kd { color: #268bd2; } /* Keyword.Declaration */ - .kn { color: #859900; } /* Keyword.Namespace */ - .kp { color: #859900; } /* Keyword.Pseudo */ - .kr { color: #268bd2; } /* Keyword.Reserved */ - .kt { color: #dc322f; } /* Keyword.Type */ - .ld { color: #93a1a1; } /* Literal.Date */ - .m { color: #2aa198; } /* Literal.Number */ - .s { color: #2aa198; } /* Literal.String */ - .na { color: #93a1a1; } /* Name.Attribute */ - .nb { color: #b58900; } /* Name.Builtin */ - .nc { color: #268bd2; } /* Name.Class */ - .no { color: #cb4b16; } /* Name.Constant */ - .nd { color: #268bd2; } /* Name.Decorator */ - .ni { color: #cb4b16; } /* Name.Entity */ - .ne { color: #cb4b16; } /* Name.Exception */ - .nf { color: #268bd2; } /* Name.Function */ - .nl { color: #93a1a1; } /* Name.Label */ - .nn { color: #93a1a1; } /* Name.Namespace */ - .nx { color: #93a1a1; } /* Name.Other */ - .py { color: #93a1a1; } /* Name.Property */ - .nt { color: #268bd2; } /* Name.Tag */ - .nv { color: #268bd2; } /* Name.Variable */ - .ow { color: #859900; } /* Operator.Word */ - .w { color: #93a1a1; } /* Text.Whitespace */ - .mf { color: #2aa198; } /* Literal.Number.Float */ - .mh { color: #2aa198; } /* Literal.Number.Hex */ - .mi { color: #2aa198; } /* Literal.Number.Integer */ - .mo { color: #2aa198; } /* Literal.Number.Oct */ - .sb { color: #586e75; } /* Literal.String.Backtick */ - .sc { color: #2aa198; } /* Literal.String.Char */ - .sd { color: #93a1a1; } /* Literal.String.Doc */ - .s2 { color: #2aa198; } /* Literal.String.Double */ - .se { color: #cb4b16; } /* Literal.String.Escape */ - .sh { color: #93a1a1; } /* Literal.String.Heredoc */ - .si { color: #2aa198; } /* Literal.String.Interpol */ - .sx { color: #2aa198; } /* Literal.String.Other */ - .sr { color: #dc322f; } /* Literal.String.Regex */ - .s1 { color: #2aa198; } /* Literal.String.Single */ - .ss { color: #2aa198; } /* Literal.String.Symbol */ - .bp { color: #268bd2; } /* Name.Builtin.Pseudo */ - .vc { color: #268bd2; } /* Name.Variable.Class */ - .vg { color: #268bd2; } /* Name.Variable.Global */ - .vi { color: #268bd2; } /* Name.Variable.Instance */ - .il { color: #2aa198; } /* Literal.Number.Integer.Long */ + .c { color: $solarized-dark-c; } /* Comment */ + .err { color: $solarized-dark-err; } /* Error */ + .g { color: $solarized-dark-g; } /* Generic */ + .k { color: $solarized-dark-k; } /* Keyword */ + .l { color: $solarized-dark-l; } /* Literal */ + .n { color: $solarized-dark-n; } /* Name */ + .o { color: $solarized-dark-o; } /* Operator */ + .x { color: $solarized-dark-x; } /* Other */ + .p { color: $solarized-dark-p; } /* Punctuation */ + .cm { color: $solarized-dark-cm; } /* Comment.Multiline */ + .cp { color: $solarized-dark-cp; } /* Comment.Preproc */ + .c1 { color: $solarized-dark-c1; } /* Comment.Single */ + .cs { color: $solarized-dark-cs; } /* Comment.Special */ + .gd { color: $solarized-dark-gd; } /* Generic.Deleted */ + .ge { color: $solarized-dark-ge; font-style: italic; } /* Generic.Emph */ + .gr { color: $solarized-dark-gr; } /* Generic.Error */ + .gh { color: $solarized-dark-gh; } /* Generic.Heading */ + .gi { color: $solarized-dark-gi; } /* Generic.Inserted */ + .go { color: $solarized-dark-go; } /* Generic.Output */ + .gp { color: $solarized-dark-gp; } /* Generic.Prompt */ + .gs { color: $solarized-dark-gs; font-weight: bold; } /* Generic.Strong */ + .gu { color: $solarized-dark-gu; } /* Generic.Subheading */ + .gt { color: $solarized-dark-gt; } /* Generic.Traceback */ + .kc { color: $solarized-dark-kc; } /* Keyword.Constant */ + .kd { color: $solarized-dark-kd; } /* Keyword.Declaration */ + .kn { color: $solarized-dark-kn; } /* Keyword.Namespace */ + .kp { color: $solarized-dark-kp; } /* Keyword.Pseudo */ + .kr { color: $solarized-dark-kr; } /* Keyword.Reserved */ + .kt { color: $solarized-dark-kt; } /* Keyword.Type */ + .ld { color: $solarized-dark-ld; } /* Literal.Date */ + .m { color: $solarized-dark-m; } /* Literal.Number */ + .s { color: $solarized-dark-s; } /* Literal.String */ + .na { color: $solarized-dark-na; } /* Name.Attribute */ + .nb { color: $solarized-dark-nb; } /* Name.Builtin */ + .nc { color: $solarized-dark-nc; } /* Name.Class */ + .no { color: $solarized-dark-no; } /* Name.Constant */ + .nd { color: $solarized-dark-nd; } /* Name.Decorator */ + .ni { color: $solarized-dark-ni; } /* Name.Entity */ + .ne { color: $solarized-dark-ne; } /* Name.Exception */ + .nf { color: $solarized-dark-nf; } /* Name.Function */ + .nl { color: $solarized-dark-nl; } /* Name.Label */ + .nn { color: $solarized-dark-nn; } /* Name.Namespace */ + .nx { color: $solarized-dark-nx; } /* Name.Other */ + .py { color: $solarized-dark-py; } /* Name.Property */ + .nt { color: $solarized-dark-nt; } /* Name.Tag */ + .nv { color: $solarized-dark-nv; } /* Name.Variable */ + .ow { color: $solarized-dark-ow; } /* Operator.Word */ + .w { color: $solarized-dark-w; } /* Text.Whitespace */ + .mf { color: $solarized-dark-mf; } /* Literal.Number.Float */ + .mh { color: $solarized-dark-mh; } /* Literal.Number.Hex */ + .mi { color: $solarized-dark-mi; } /* Literal.Number.Integer */ + .mo { color: $solarized-dark-mo; } /* Literal.Number.Oct */ + .sb { color: $solarized-dark-sb; } /* Literal.String.Backtick */ + .sc { color: $solarized-dark-sc; } /* Literal.String.Char */ + .sd { color: $solarized-dark-sd; } /* Literal.String.Doc */ + .s2 { color: $solarized-dark-s2; } /* Literal.String.Double */ + .se { color: $solarized-dark-se; } /* Literal.String.Escape */ + .sh { color: $solarized-dark-sh; } /* Literal.String.Heredoc */ + .si { color: $solarized-dark-si; } /* Literal.String.Interpol */ + .sx { color: $solarized-dark-sx; } /* Literal.String.Other */ + .sr { color: $solarized-dark-sr; } /* Literal.String.Regex */ + .s1 { color: $solarized-dark-s1; } /* Literal.String.Single */ + .ss { color: $solarized-dark-ss; } /* Literal.String.Symbol */ + .bp { color: $solarized-dark-bp; } /* Name.Builtin.Pseudo */ + .vc { color: $solarized-dark-vc; } /* Name.Variable.Class */ + .vg { color: $solarized-dark-vg; } /* Name.Variable.Global */ + .vi { color: $solarized-dark-vi; } /* Name.Variable.Instance */ + .il { color: $solarized-dark-il; } /* Literal.Number.Integer.Long */ } diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index faff353ded7..499a1c108b8 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -1,15 +1,99 @@ /* https://gist.github.com/qguv/7936275 */ +/* +* Solarized light syntax colors +*/ +$solarized-light-matchline-bg: rgba(255, 255, 255, 0.4); +$solarized-light-new-bg: rgba(133, 153, 0, 0.2); +$solarized-light-new-idiff: rgba(133, 153, 0, 0.25); +$solarized-light-old-bg: rgba(220, 50, 47, 0.2); +$solarized-light-old-idiff: rgba(220, 50, 47, 0.25); +$solarized-light-border: #c5d0d4; +$solarized-light-pre-bg: #002b36; +$solarized-light-pre-bg: #fdf6e3; +$solarized-light-pre-color: #586e75; +$solarized-light-line-bg: #fdf6e3; +$solarized-light-highlight: #eee8d5; +$solarized-light-hll-bg: #ddd8c5; +$solarized-light-c: #93a1a1; +$solarized-light-err: #586e75; +$solarized-light-g: #586e75; +$solarized-light-k: #859900; +$solarized-light-l: #586e75; +$solarized-light-n: #586e75; +$solarized-light-o: #859900; +$solarized-light-x: #cb4b16; +$solarized-light-p: #586e75; +$solarized-light-cm: #93a1a1; +$solarized-light-cp: #859900; +$solarized-light-c1: #93a1a1; +$solarized-light-cs: #859900; +$solarized-light-gd: #2aa198; +$solarized-light-ge: #586e75; +$solarized-light-gr: #dc322f; +$solarized-light-gh: #cb4b16; +$solarized-light-gi: #859900; +$solarized-light-go: #586e75; +$solarized-light-gp: #586e75; +$solarized-light-gs: #586e75; +$solarized-light-gu: #cb4b16; +$solarized-light-gt: #586e75; +$solarized-light-kc: #cb4b16; +$solarized-light-kd: #268bd2; +$solarized-light-kn: #859900; +$solarized-light-kp: #859900; +$solarized-light-kr: #268bd2; +$solarized-light-kt: #dc322f; +$solarized-light-ld: #586e75; +$solarized-light-m: #2aa198; +$solarized-light-s: #2aa198; +$solarized-light-na: #586e75; +$solarized-light-nb: #b58900; +$solarized-light-nc: #268bd2; +$solarized-light-no: #cb4b16; +$solarized-light-nd: #268bd2; +$solarized-light-ni: #cb4b16; +$solarized-light-ne: #cb4b16; +$solarized-light-nf: #268bd2; +$solarized-light-nl: #586e75; +$solarized-light-nn: #586e75; +$solarized-light-nx: #586e75; +$solarized-light-py: #586e75; +$solarized-light-nt: #268bd2; +$solarized-light-nv: #268bd2; +$solarized-light-ow: #859900; +$solarized-light-w: #586e75; +$solarized-light-mf: #2aa198; +$solarized-light-mh: #2aa198; +$solarized-light-mi: #2aa198; +$solarized-light-mo: #2aa198; +$solarized-light-sb: #93a1a1; +$solarized-light-sc: #2aa198; +$solarized-light-sd: #586e75; +$solarized-light-s2: #2aa198; +$solarized-light-se: #cb4b16; +$solarized-light-sh: #586e75; +$solarized-light-si: #2aa198; +$solarized-light-sx: #2aa198; +$solarized-light-sr: #dc322f; +$solarized-light-s1: #2aa198; +$solarized-light-ss: #2aa198; +$solarized-light-bp: #268bd2; +$solarized-light-vc: #268bd2; +$solarized-light-vg: #268bd2; +$solarized-light-vi: #268bd2; +$solarized-light-il: #2aa198; + @mixin matchLine { color: $black-transparent; - background: rgba(255, 255, 255, 0.4); + background: $solarized-light-matchline-bg; } .code.solarized-light { // Line numbers .line-numbers, .diff-line-num { - background-color: #fdf6e3; + background-color: $solarized-light-line-bg; } .diff-line-num, @@ -20,14 +104,14 @@ // Code itself pre.code, .diff-line-num { - border-color: #c5d0d4; + border-color: $solarized-light-border; } &, pre.code, .line_holder .line_content { - background-color: #fdf6e3; - color: #586e75; + background-color: $solarized-light-pre-bg; + color: $solarized-light-pre-color; } // Diff line @@ -38,18 +122,19 @@ td.diff-line-num.hll:not(.empty-cell), td.line_content.hll:not(.empty-cell) { - background-color: #ddd8c5; - border-color: darken(#ddd8c5, 15%); + background-color: $solarized-light-hll-bg; + border-color: darken($solarized-light-hll-bg, 15%); } .diff-line-num.new, .line_content.new { - @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4); + @include diff_background($solarized-light-new-bg, + $solarized-light-new-idiff, $solarized-light-border); } .diff-line-num.old, .line_content.old { - @include diff_background(rgba(220, 50, 47, 0.2), rgba(220, 50, 47, 0.25), #c5d0d4); + @include diff_background($solarized-light-old-bg, $solarized-light-old-idiff, $solarized-light-border); } .line_content.match { @@ -59,12 +144,12 @@ // highlight line via anchor pre .hll { - background-color: #ddd8c5 !important; + background-color: $solarized-light-hll-bg !important; } // Search result highlight span.highlight_word { - background-color: #eee8d5 !important; + background-color: $solarized-light-highlight !important; } /* Solarized Light @@ -85,72 +170,72 @@ green #859900 operators, other keywords */ - .c { color: #93a1a1; } /* Comment */ - .err { color: #586e75; } /* Error */ - .g { color: #586e75; } /* Generic */ - .k { color: #859900; } /* Keyword */ - .l { color: #586e75; } /* Literal */ - .n { color: #586e75; } /* Name */ - .o { color: #859900; } /* Operator */ - .x { color: #cb4b16; } /* Other */ - .p { color: #586e75; } /* Punctuation */ - .cm { color: #93a1a1; } /* Comment.Multiline */ - .cp { color: #859900; } /* Comment.Preproc */ - .c1 { color: #93a1a1; } /* Comment.Single */ - .cs { color: #859900; } /* Comment.Special */ - .gd { color: #2aa198; } /* Generic.Deleted */ - .ge { color: #586e75; font-style: italic; } /* Generic.Emph */ - .gr { color: #dc322f; } /* Generic.Error */ - .gh { color: #cb4b16; } /* Generic.Heading */ - .gi { color: #859900; } /* Generic.Inserted */ - .go { color: #586e75; } /* Generic.Output */ - .gp { color: #586e75; } /* Generic.Prompt */ - .gs { color: #586e75; font-weight: bold; } /* Generic.Strong */ - .gu { color: #cb4b16; } /* Generic.Subheading */ - .gt { color: #586e75; } /* Generic.Traceback */ - .kc { color: #cb4b16; } /* Keyword.Constant */ - .kd { color: #268bd2; } /* Keyword.Declaration */ - .kn { color: #859900; } /* Keyword.Namespace */ - .kp { color: #859900; } /* Keyword.Pseudo */ - .kr { color: #268bd2; } /* Keyword.Reserved */ - .kt { color: #dc322f; } /* Keyword.Type */ - .ld { color: #586e75; } /* Literal.Date */ - .m { color: #2aa198; } /* Literal.Number */ - .s { color: #2aa198; } /* Literal.String */ - .na { color: #586e75; } /* Name.Attribute */ - .nb { color: #b58900; } /* Name.Builtin */ - .nc { color: #268bd2; } /* Name.Class */ - .no { color: #cb4b16; } /* Name.Constant */ - .nd { color: #268bd2; } /* Name.Decorator */ - .ni { color: #cb4b16; } /* Name.Entity */ - .ne { color: #cb4b16; } /* Name.Exception */ - .nf { color: #268bd2; } /* Name.Function */ - .nl { color: #586e75; } /* Name.Label */ - .nn { color: #586e75; } /* Name.Namespace */ - .nx { color: #586e75; } /* Name.Other */ - .py { color: #586e75; } /* Name.Property */ - .nt { color: #268bd2; } /* Name.Tag */ - .nv { color: #268bd2; } /* Name.Variable */ - .ow { color: #859900; } /* Operator.Word */ - .w { color: #586e75; } /* Text.Whitespace */ - .mf { color: #2aa198; } /* Literal.Number.Float */ - .mh { color: #2aa198; } /* Literal.Number.Hex */ - .mi { color: #2aa198; } /* Literal.Number.Integer */ - .mo { color: #2aa198; } /* Literal.Number.Oct */ - .sb { color: #93a1a1; } /* Literal.String.Backtick */ - .sc { color: #2aa198; } /* Literal.String.Char */ - .sd { color: #586e75; } /* Literal.String.Doc */ - .s2 { color: #2aa198; } /* Literal.String.Double */ - .se { color: #cb4b16; } /* Literal.String.Escape */ - .sh { color: #586e75; } /* Literal.String.Heredoc */ - .si { color: #2aa198; } /* Literal.String.Interpol */ - .sx { color: #2aa198; } /* Literal.String.Other */ - .sr { color: #dc322f; } /* Literal.String.Regex */ - .s1 { color: #2aa198; } /* Literal.String.Single */ - .ss { color: #2aa198; } /* Literal.String.Symbol */ - .bp { color: #268bd2; } /* Name.Builtin.Pseudo */ - .vc { color: #268bd2; } /* Name.Variable.Class */ - .vg { color: #268bd2; } /* Name.Variable.Global */ - .vi { color: #268bd2; } /* Name.Variable.Instance */ - .il { color: #2aa198; } /* Literal.Number.Integer.Long */ + .c { color: $solarized-light-c; } /* Comment */ + .err { color: $solarized-light-err; } /* Error */ + .g { color: $solarized-light-g; } /* Generic */ + .k { color: $solarized-light-k; } /* Keyword */ + .l { color: $solarized-light-l; } /* Literal */ + .n { color: $solarized-light-n; } /* Name */ + .o { color: $solarized-light-o; } /* Operator */ + .x { color: $solarized-light-x; } /* Other */ + .p { color: $solarized-light-p; } /* Punctuation */ + .cm { color: $solarized-light-cm; } /* Comment.Multiline */ + .cp { color: $solarized-light-cp; } /* Comment.Preproc */ + .c1 { color: $solarized-light-c1; } /* Comment.Single */ + .cs { color: $solarized-light-cs; } /* Comment.Special */ + .gd { color: $solarized-light-gd; } /* Generic.Deleted */ + .ge { color: $solarized-light-ge; font-style: italic; } /* Generic.Emph */ + .gr { color: $solarized-light-gr; } /* Generic.Error */ + .gh { color: $solarized-light-gh; } /* Generic.Heading */ + .gi { color: $solarized-light-gi; } /* Generic.Inserted */ + .go { color: $solarized-light-go; } /* Generic.Output */ + .gp { color: $solarized-light-gp; } /* Generic.Prompt */ + .gs { color: $solarized-light-gs; font-weight: bold; } /* Generic.Strong */ + .gu { color: $solarized-light-gu; } /* Generic.Subheading */ + .gt { color: $solarized-light-gt; } /* Generic.Traceback */ + .kc { color: $solarized-light-kc; } /* Keyword.Constant */ + .kd { color: $solarized-light-kd; } /* Keyword.Declaration */ + .kn { color: $solarized-light-kn; } /* Keyword.Namespace */ + .kp { color: $solarized-light-kp; } /* Keyword.Pseudo */ + .kr { color: $solarized-light-kr; } /* Keyword.Reserved */ + .kt { color: $solarized-light-kt; } /* Keyword.Type */ + .ld { color: $solarized-light-ld; } /* Literal.Date */ + .m { color: $solarized-light-m; } /* Literal.Number */ + .s { color: $solarized-light-s; } /* Literal.String */ + .na { color: $solarized-light-na; } /* Name.Attribute */ + .nb { color: $solarized-light-nb; } /* Name.Builtin */ + .nc { color: $solarized-light-nc; } /* Name.Class */ + .no { color: $solarized-light-no; } /* Name.Constant */ + .nd { color: $solarized-light-nd; } /* Name.Decorator */ + .ni { color: $solarized-light-ni; } /* Name.Entity */ + .ne { color: $solarized-light-ne; } /* Name.Exception */ + .nf { color: $solarized-light-nf; } /* Name.Function */ + .nl { color: $solarized-light-nl; } /* Name.Label */ + .nn { color: $solarized-light-nn; } /* Name.Namespace */ + .nx { color: $solarized-light-nx; } /* Name.Other */ + .py { color: $solarized-light-py; } /* Name.Property */ + .nt { color: $solarized-light-nt; } /* Name.Tag */ + .nv { color: $solarized-light-nv; } /* Name.Variable */ + .ow { color: $solarized-light-ow; } /* Operator.Word */ + .w { color: $solarized-light-w; } /* Text.Whitespace */ + .mf { color: $solarized-light-mf; } /* Literal.Number.Float */ + .mh { color: $solarized-light-mh; } /* Literal.Number.Hex */ + .mi { color: $solarized-light-mi; } /* Literal.Number.Integer */ + .mo { color: $solarized-light-mo; } /* Literal.Number.Oct */ + .sb { color: $solarized-light-sb; } /* Literal.String.Backtick */ + .sc { color: $solarized-light-sc; } /* Literal.String.Char */ + .sd { color: $solarized-light-sd; } /* Literal.String.Doc */ + .s2 { color: $solarized-light-s2; } /* Literal.String.Double */ + .se { color: $solarized-light-se; } /* Literal.String.Escape */ + .sh { color: $solarized-light-sh; } /* Literal.String.Heredoc */ + .si { color: $solarized-light-si; } /* Literal.String.Interpol */ + .sx { color: $solarized-light-sx; } /* Literal.String.Other */ + .sr { color: $solarized-light-sr; } /* Literal.String.Regex */ + .s1 { color: $solarized-light-s1; } /* Literal.String.Single */ + .ss { color: $solarized-light-ss; } /* Literal.String.Symbol */ + .bp { color: $solarized-light-bp; } /* Name.Builtin.Pseudo */ + .vc { color: $solarized-light-vc; } /* Name.Variable.Class */ + .vg { color: $solarized-light-vg; } /* Name.Variable.Global */ + .vi { color: $solarized-light-vi; } /* Name.Variable.Instance */ + .il { color: $solarized-light-il; } /* Literal.Number.Integer.Long */ } diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index d5367d5f3f0..b425c78e0d5 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -1,15 +1,82 @@ /* https://github.com/aahan/pygments-github-style */ +/* +* White Syntax Colors +*/ +$white-code-color: $gl-text-color; +$white-highlight: #fafe3d; +$white-pre-hll-bg: #f8eec7; +$white-hll-bg: #f8f8f8; +$white-c: #998; +$white-err: #a61717; +$white-err-bg: #e3d2d2; +$white-cm: #998; +$white-cp: #999; +$white-c1: #998; +$white-cs: #999; +$white-gd: $black; +$white-gd-bg: #fdd; +$white-gd-x: $black; +$white-gd-x-bg: #faa; +$white-gr: #a00; +$white-gh: #999; +$white-gi: $black; +$white-gi-bg: #dfd; +$white-gi-x: $black; +$white-gi-x-bg: #afa; +$white-go: #888; +$white-gp: #555; +$white-gu: #800080; +$white-gt: #a00; +$white-kt: #458; +$white-m: #099; +$white-s: #d14; +$white-n: #333; +$white-na: teal; +$white-nb: #0086b3; +$white-nc: #458; +$white-no: teal; +$white-ni: purple; +$white-ne: #900; +$white-nf: #900; +$white-nn: #555; +$white-nt: navy; +$white-nv: teal; +$white-w: #bbb; +$white-mf: #099; +$white-mh: #099; +$white-mi: #099; +$white-mo: #099; +$white-sb: #d14; +$white-sc: #d14; +$white-sd: #d14; +$white-s2: #d14; +$white-se: #d14; +$white-sh: #d14; +$white-si: #d14; +$white-sx: #d14; +$white-sr: #009926; +$white-s1: #d14; +$white-ss: #990073; +$white-bp: #999; +$white-vc: teal; +$white-vg: teal; +$white-vi: teal; +$white-il: #099; +$white-gc-color: #999; +$white-gc-bg: #eaf2f5; + + @mixin matchLine { color: $black-transparent; - background-color: $match-line; + background-color: $gray-light; } .code.white { // Line numbers .line-numbers, .diff-line-num { - background-color: $background-color; + background-color: $gray-light; } .diff-line-num, @@ -20,14 +87,14 @@ // Code itself pre.code, .diff-line-num { - border-color: $table-border-gray; + border-color: $white-normal; } &, pre.code, .line_holder .line_content { - background-color: #fff; - color: #333; + background-color: $white-light; + color: $white-code-color; } // Diff line @@ -83,75 +150,75 @@ // highlight line via anchor pre .hll { - background-color: #f8eec7 !important; + background-color: $white-pre-hll-bg !important; } // Search result highlight span.highlight_word { - background-color: #fafe3d !important; + background-color: $white-highlight !important; } - .hll { background-color: #f8f8f8; } - .c { color: #998; font-style: italic; } - .err { color: #a61717; background-color: #e3d2d2; } + .hll { background-color: $white-hll-bg; } + .c { color: $white-c; font-style: italic; } + .err { color: $white-err; background-color: $white-err-bg; } .k { font-weight: bold; } .o { font-weight: bold; } - .cm { color: #998; font-style: italic; } - .cp { color: #999; font-weight: bold; } - .c1 { color: #998; font-style: italic; } - .cs { color: #999; font-weight: bold; font-style: italic; } - .gd { color: #000; background-color: #fdd; } - .gd .x { color: #000; background-color: #faa; } + .cm { color: $white-cm; font-style: italic; } + .cp { color: $white-cp; font-weight: bold; } + .c1 { color: $white-c1; font-style: italic; } + .cs { color: $white-cs; font-weight: bold; font-style: italic; } + .gd { color: $white-gd; background-color: $white-gd-bg; } + .gd .x { color: $white-gd-x; background-color: $white-gd-x-bg; } .ge { font-style: italic; } - .gr { color: #a00; } - .gh { color: #999; } - .gi { color: #000; background-color: #dfd; } - .gi .x { color: #000; background-color: #afa; } - .go { color: #888; } - .gp { color: #555; } + .gr { color: $white-gr; } + .gh { color: $white-gh; } + .gi { color: $white-gi; background-color: $white-gi-bg; } + .gi .x { color: $white-gi-x; background-color: $white-gi-x-bg; } + .go { color: $white-go; } + .gp { color: $white-gp; } .gs { font-weight: bold; } - .gu { color: #800080; font-weight: bold; } - .gt { color: #a00; } + .gu { color: $white-gu; font-weight: bold; } + .gt { color: $white-gt; } .kc { font-weight: bold; } .kd { font-weight: bold; } .kn { font-weight: bold; } .kp { font-weight: bold; } .kr { font-weight: bold; } - .kt { color: #458; font-weight: bold; } - .m { color: #099; } - .s { color: #d14; } - .n { color: #333; } - .na { color: teal; } - .nb { color: #0086b3; } - .nc { color: #458; font-weight: bold; } - .no { color: teal; } - .ni { color: purple; } - .ne { color: #900; font-weight: bold; } - .nf { color: #900; font-weight: bold; } - .nn { color: #555; } - .nt { color: navy; } - .nv { color: teal; } + .kt { color: $white-kt; font-weight: bold; } + .m { color: $white-m; } + .s { color: $white-s; } + .n { color: $white-n; } + .na { color: $white-na; } + .nb { color: $white-nb; } + .nc { color: $white-nc; font-weight: bold; } + .no { color: $white-no; } + .ni { color: $white-ni; } + .ne { color: $white-ne; font-weight: bold; } + .nf { color: $white-nf; font-weight: bold; } + .nn { color: $white-nn; } + .nt { color: $white-nt; } + .nv { color: $white-nv; } .ow { font-weight: bold; } - .w { color: #bbb; } - .mf { color: #099; } - .mh { color: #099; } - .mi { color: #099; } - .mo { color: #099; } - .sb { color: #d14; } - .sc { color: #d14; } - .sd { color: #d14; } - .s2 { color: #d14; } - .se { color: #d14; } - .sh { color: #d14; } - .si { color: #d14; } - .sx { color: #d14; } - .sr { color: #009926; } - .s1 { color: #d14; } - .ss { color: #990073; } - .bp { color: #999; } - .vc { color: teal; } - .vg { color: teal; } - .vi { color: teal; } - .il { color: #099; } - .gc { color: #999; background-color: #eaf2f5; } + .w { color: $white-w; } + .mf { color: $white-mf; } + .mh { color: $white-mh; } + .mi { color: $white-mi; } + .mo { color: $white-mo; } + .sb { color: $white-sb; } + .sc { color: $white-sc; } + .sd { color: $white-sd; } + .s2 { color: $white-s2; } + .se { color: $white-se; } + .sh { color: $white-sh; } + .si { color: $white-si; } + .sx { color: $white-sx; } + .sr { color: $white-sr; } + .s1 { color: $white-s1; } + .ss { color: $white-ss; } + .bp { color: $white-bp; } + .vc { color: $white-vc; } + .vg { color: $white-vg; } + .vi { color: $white-vi; } + .il { color: $white-il; } + .gc { color: $white-gc-color; background-color: $white-gc-bg; } } diff --git a/app/assets/stylesheets/mailers/devise.scss b/app/assets/stylesheets/mailers/devise.scss index b2bce482fde..9f613710cf4 100644 --- a/app/assets/stylesheets/mailers/devise.scss +++ b/app/assets/stylesheets/mailers/devise.scss @@ -1,3 +1,5 @@ +@import "framework/variables"; + // NOTE: This stylesheet is for the exclusive use of the `devise_mailer` layout // used for Devise email templates, and _should not_ be included in any // application stylesheets. @@ -46,7 +48,7 @@ table { &#body { background-color: $message-background-color; - border: 1px solid #000; + border: 1px solid $black; border-radius: 4px; margin: 0 auto; width: 600px; diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss new file mode 100644 index 00000000000..60ff72c703e --- /dev/null +++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss @@ -0,0 +1,207 @@ +@import "framework/variables"; + +// This file is largely copied from `highlight/white.scss`, but modified to +// avoid all descendant selectors (`table td`). This is because the CSS inlining +// we use performs dramatically worse on descendant selectors than the +// alternatives. +// <https://gitlab.com/gitlab-org/gitlab-ee/issues/490#note_12283632> +// +// DO NOT ADD ANY DESCENDANT SELECTORS TO THIS FILE. Instead, use (in order of +// preference): plain class selectors, type (element name) selectors, or +// explicit child selectors. + +/* +* Highlighted Diff Email Syntax Colors +*/ +$highlighted-highlight-word: #fafe3d; +$highlighted-hll-bg: #f8f8f8; +$highlighted-c: #998; +$highlighted-err: #a61717; +$highlighted-err-bg: #e3d2d2; +$highlighted-cm: #998; +$highlighted-cp: #999; +$highlighted-c1: #998; +$highlighted-cs: #999; +$highlighted-gd: #000; +$highlighted-gd-bg: #fdd; +$highlighted-gd-x: #000; +$highlighted-gd-x-bg: #faa; +$highlighted-gr: #a00; +$highlighted-gh: #999; +$highlighted-gi: #000; +$highlighted-gi-bg: #dfd; +$highlighted-gi-x: #000; +$highlighted-gi-x-bg: #afa; +$highlighted-go: #888; +$highlighted-gp: #555; +$highlighted-gu: #800080; +$highlighted-gt: #a00; +$highlighted-kt: #458; +$highlighted-m: #099; +$highlighted-s: #d14; +$highlighted-n: #333; +$highlighted-na: teal; +$highlighted-nb: #0086b3; +$highlighted-nc: #458; +$highlighted-no: teal; +$highlighted-ni: purple; +$highlighted-ne: #900; +$highlighted-nf: #900; +$highlighted-nn: #555; +$highlighted-nt: navy; +$highlighted-nv: teal; +$highlighted-w: #bbb; +$highlighted-mf: #099; +$highlighted-mh: #099; +$highlighted-mi: #099; +$highlighted-mo: #099; +$highlighted-sb: #d14; +$highlighted-sc: #d14; +$highlighted-sd: #d14; +$highlighted-s2: #d14; +$highlighted-se: #d14; +$highlighted-sh: #d14; +$highlighted-si: #d14; +$highlighted-sx: #d14; +$highlighted-sr: #009926; +$highlighted-s1: #d14; +$highlighted-ss: #990073; +$highlighted-bp: #999; +$highlighted-vc: teal; +$highlighted-vg: teal; +$highlighted-vi: teal; +$highlighted-il: #099; +$highlighted-gc: #999; +$highlighted-gc-bg: #eaf2f5; + +.code { + background-color: $white-light; + font-family: monospace; + font-size: $code_font_size; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + + > tr { + line-height: $code_line_height; + } +} + +.diff-line-num { + padding: 0 5px; + text-align: right; + width: 35px; + background-color: $gray-light; + color: $black-transparent; + border-right: 1px solid $white-normal; + + &.old { + background-color: $line-number-old; + border-right-color: $line-removed-dark; + } + + &.new { + background-color: $line-number-new; + border-right-color: $line-added-dark; + } +} + +.line_content { + padding-left: 0.5em; + padding-right: 0.5em; + + &.old { + background-color: $line-removed; + + > .line > span.idiff, + > .line > span > span.idiff { + background-color: $line-removed-dark; + } + } + + &.new { + background-color: $line-added; + + > .line > span.idiff, + > .line > span > span.idiff { + background-color: $line-added-dark; + } + } + + &.match { + color: $black-transparent; + background-color: $gray-light; + } +} + +pre { + margin: 0; +} + +span.highlight_word { + background-color: $highlighted-highlight-word !important; +} + +.hll { background-color: $highlighted-hll-bg; } +.c { color: $highlighted-c; font-style: italic; } +.err { color: $highlighted-err; background-color: $highlighted-err-bg; } +.k { font-weight: bold; } +.o { font-weight: bold; } +.cm { color: $highlighted-cm; font-style: italic; } +.cp { color: $highlighted-cp; font-weight: bold; } +.c1 { color: $highlighted-c1; font-style: italic; } +.cs { color: $highlighted-cs; font-weight: bold; font-style: italic; } +.gd { color: $highlighted-gd; background-color: $highlighted-gd-bg; } +.gd .x { color: $highlighted-gd; background-color: $highlighted-gd-x-bg; } +.ge { font-style: italic; } +.gr { color: $highlighted-gr; } +.gh { color: $highlighted-gh; } +.gi { color: $highlighted-gi; background-color: $highlighted-gi-bg; } +.gi .x { color: $highlighted-gi; background-color: $highlighted-gi-x-bg; } +.go { color: $highlighted-go; } +.gp { color: $highlighted-gp; } +.gs { font-weight: bold; } +.gu { color: $highlighted-gu; font-weight: bold; } +.gt { color: $highlighted-gt; } +.kc { font-weight: bold; } +.kd { font-weight: bold; } +.kn { font-weight: bold; } +.kp { font-weight: bold; } +.kr { font-weight: bold; } +.kt { color: $highlighted-kt; font-weight: bold; } +.m { color: $highlighted-m; } +.s { color: $highlighted-s; } +.n { color: $highlighted-n; } +.na { color: $highlighted-na; } +.nb { color: $highlighted-nb; } +.nc { color: $highlighted-nc; font-weight: bold; } +.no { color: $highlighted-no; } +.ni { color: $highlighted-ni; } +.ne { color: $highlighted-ne; font-weight: bold; } +.nf { color: $highlighted-nf; font-weight: bold; } +.nn { color: $highlighted-nn; } +.nt { color: $highlighted-nt; } +.nv { color: $highlighted-nv; } +.ow { font-weight: bold; } +.w { color: $highlighted-w; } +.mf { color: $highlighted-mf; } +.mh { color: $highlighted-mh; } +.mi { color: $highlighted-mi; } +.mo { color: $highlighted-mo; } +.sb { color: $highlighted-sb; } +.sc { color: $highlighted-sc; } +.sd { color: $highlighted-sd; } +.s2 { color: $highlighted-s2; } +.se { color: $highlighted-se; } +.sh { color: $highlighted-sh; } +.si { color: $highlighted-si; } +.sx { color: $highlighted-sx; } +.sr { color: $highlighted-sr; } +.s1 { color: $highlighted-s1; } +.ss { color: $highlighted-ss; } +.bp { color: $highlighted-bp; } +.vc { color: $highlighted-vc; } +.vg { color: $highlighted-vg; } +.vi { color: $highlighted-vi; } +.il { color: $highlighted-il; } +.gc { color: $highlighted-gc; background-color: $highlighted-gc-bg; } diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss deleted file mode 100644 index 8d1a6020ca4..00000000000 --- a/app/assets/stylesheets/mailers/repository_push_email.scss +++ /dev/null @@ -1,143 +0,0 @@ -@import "framework/variables"; - -// This file is largely copied from `highlight/white.scss`, but modified to -// avoid all descendant selectors (`table td`). This is because the CSS inlining -// we use performs dramatically worse on descendant selectors than the -// alternatives. -// <https://gitlab.com/gitlab-org/gitlab-ee/issues/490#note_12283632> -// -// DO NOT ADD ANY DESCENDANT SELECTORS TO THIS FILE. Instead, use (in order of -// preference): plain class selectors, type (element name) selectors, or -// explicit child selectors. - -.code { - background-color: #fff; - font-family: monospace; - font-size: $code_font_size; - -premailer-cellpadding: 0; - -premailer-cellspacing: 0; - -premailer-width: 100%; - - > tr { - line-height: $code_line_height; - } -} - -.diff-line-num { - padding: 0 5px; - text-align: right; - width: 35px; - background-color: $background-color; - color: $black-transparent; - border-right: 1px solid $table-border-gray; - - &.old { - background-color: $line-number-old; - border-right-color: $line-removed-dark; - } - - &.new { - background-color: $line-number-new; - border-right-color: $line-added-dark; - } -} - -.line_content { - padding-left: 0.5em; - padding-right: 0.5em; - - &.old { - background-color: $line-removed; - - > .line > span.idiff, - > .line > span > span.idiff { - background-color: $line-removed-dark; - } - } - - &.new { - background-color: $line-added; - - > .line > span.idiff, - > .line > span > span.idiff { - background-color: $line-added-dark; - } - } - - &.match { - color: $black-transparent; - background-color: $match-line; - } -} - -pre { - margin: 0; -} - -span.highlight_word { - background-color: #fafe3d !important; -} - -.hll { background-color: #f8f8f8; } -.c { color: #998; font-style: italic; } -.err { color: #a61717; background-color: #e3d2d2; } -.k { font-weight: bold; } -.o { font-weight: bold; } -.cm { color: #998; font-style: italic; } -.cp { color: #999; font-weight: bold; } -.c1 { color: #998; font-style: italic; } -.cs { color: #999; font-weight: bold; font-style: italic; } -.gd { color: #000; background-color: #fdd; } -.gd .x { color: #000; background-color: #faa; } -.ge { font-style: italic; } -.gr { color: #a00; } -.gh { color: #999; } -.gi { color: #000; background-color: #dfd; } -.gi .x { color: #000; background-color: #afa; } -.go { color: #888; } -.gp { color: #555; } -.gs { font-weight: bold; } -.gu { color: #800080; font-weight: bold; } -.gt { color: #a00; } -.kc { font-weight: bold; } -.kd { font-weight: bold; } -.kn { font-weight: bold; } -.kp { font-weight: bold; } -.kr { font-weight: bold; } -.kt { color: #458; font-weight: bold; } -.m { color: #099; } -.s { color: #d14; } -.n { color: #333; } -.na { color: teal; } -.nb { color: #0086b3; } -.nc { color: #458; font-weight: bold; } -.no { color: teal; } -.ni { color: purple; } -.ne { color: #900; font-weight: bold; } -.nf { color: #900; font-weight: bold; } -.nn { color: #555; } -.nt { color: navy; } -.nv { color: teal; } -.ow { font-weight: bold; } -.w { color: #bbb; } -.mf { color: #099; } -.mh { color: #099; } -.mi { color: #099; } -.mo { color: #099; } -.sb { color: #d14; } -.sc { color: #d14; } -.sd { color: #d14; } -.s2 { color: #d14; } -.se { color: #d14; } -.sh { color: #d14; } -.si { color: #d14; } -.sx { color: #d14; } -.sr { color: #009926; } -.s1 { color: #d14; } -.ss { color: #990073; } -.bp { color: #999; } -.vc { color: teal; } -.vg { color: teal; } -.vi { color: teal; } -.il { color: #099; } -.gc { color: #999; background-color: #eaf2f5; } diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss index ced8c4a9907..a81e5eb5ebf 100644 --- a/app/assets/stylesheets/notify.scss +++ b/app/assets/stylesheets/notify.scss @@ -1,3 +1,5 @@ +@import "framework/variables"; + img { max-width: 100%; height: auto; @@ -5,26 +7,14 @@ img { p.details { font-style: italic; - color: #777; + color: $notify-details; } .footer > p { font-size: small; - color: #777; + color: $notify-footer; } pre.commit-message { white-space: pre-wrap; } - -.file-stats > a { - text-decoration: none; - - > .new-file { - color: #090; - } - - > .deleted-file { - color: #b00; - } -} diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss deleted file mode 100644 index 6cefafd8fc7..00000000000 --- a/app/assets/stylesheets/pages/admin.scss +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Admin area - * - */ -.admin-dashboard { - .data { - a { - h1 { - line-height: 48px; - font-size: 48px; - padding: 20px; - text-align: center; - font-weight: normal; - } - } - } - - .str-truncated { - max-width: 60%; - } -} - -.admin-filter form { - .select2-container { - width: 100%; - } - - .controls { - margin-left: 130px; - } - - .form-actions { - padding-left: 130px; - background: #fff; - } - - .visibility-levels { - .controls { - margin-bottom: 9px; - } - - i { - color: inherit; - } - } -} - -.broadcast-messages { - .message { - line-height: 2; - } -} - -.broadcast-message { - @extend .alert-warning; - padding: 10px; - text-align: center; - - > div, - p { - display: inline; - margin: 0; - - a { - color: inherit; - text-decoration: underline; - } - } -} - -.broadcast-message-preview { - @extend .broadcast-message; - margin-bottom: 20px; -} - -// Users List - -.users-list { - .user-row { - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - white-space: nowrap; - } - - .user-details { - flex: 1 1 auto; - overflow: hidden; - padding-right: 8px; - } - - .user-name { - display: inline-block; - font-weight: 600; - } - - .user-name, - .user-email { - overflow: hidden; - text-overflow: ellipsis; - } - - .dropdown { - .btn-block { - margin-bottom: 0; - line-height: inherit; - } - } - - .label-default { - color: $btn-transparent-color; - } -} - -.abuse-reports { - .table { - table-layout: fixed; - } - - .subheading { - padding-bottom: $gl-padding; - } - - .message { - word-wrap: break-word; - } - - .btn { - white-space: normal; - padding: $gl-btn-padding; - } - - th { - width: 15%; - - &.wide { - width: 55%; - } - } - - @media (max-width: $screen-sm-max) { - th { - width: 100%; - } - - td { - width: 100%; - float: left; - } - } - - .no-reports { - .emoji-icon { - margin-left: $btn-side-margin; - margin-top: 3px; - } - - span { - font-size: 19px; - } - } -} diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 47a7e84b5c6..f2d60bff2b5 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -31,7 +31,7 @@ .dropdown-content { max-height: 150px; - } + } } .issue-board-dropdown-content { @@ -74,6 +74,7 @@ height: 475px; // Needed for PhantomJS height: calc(100vh - 220px); min-height: 475px; + transition: width .2s; &.is-compact { width: calc(100% - 290px); @@ -98,7 +99,7 @@ .board-inner { height: 100%; font-size: $issue-boards-font-size; - background: $background-color; + background: $gray-light; border: 1px solid $border-color; border-radius: $border-radius-default; } @@ -109,6 +110,12 @@ &.has-border { border-top: 3px solid; + margin-top: -1px; + margin-right: -1px; + margin-left: -1px; + padding-top: 1px; + padding-right: 1px; + padding-left: 1px; .board-title { padding-top: ($gl-padding - 3px); @@ -145,7 +152,7 @@ .board-blank-state { height: calc(100% - 49px); padding: $gl-padding; - background-color: #fff; + background-color: $white-light; } .board-blank-state-list { @@ -166,8 +173,12 @@ } } -.board-list { +.board-list-component { height: calc(100% - 49px); +} + +.board-list { + height: 100%; margin-bottom: 0; padding: 5px; list-style: none; @@ -175,7 +186,7 @@ overflow-x: hidden; &.is-smaller { - height: calc(100% - 185px); + height: calc(100% - 136px); } } @@ -187,9 +198,9 @@ .card { position: relative; padding: 10px $gl-padding; - background: #fff; + background: $white-light; border-radius: $border-radius-default; - box-shadow: 0 1px 2px rgba(186, 186, 186, 0.5); + box-shadow: 0 1px 2px $issue-boards-card-shadow; list-style: none; &:not(:last-child) { @@ -239,7 +250,7 @@ } .issue-boards-search { - width: 335px; + width: 290px; .form-control { display: inline-block; @@ -249,7 +260,7 @@ .board-list-count { padding: 10px 0; - color: $gl-placeholder-color; + color: $gl-text-color-secondary; font-size: 13px; > .fa { @@ -321,7 +332,6 @@ } .issuable-header-text { - width: 100%; padding-right: 35px; > strong { @@ -329,3 +339,18 @@ } } } + +.right-sidebar.right-sidebar-expanded { + &.boards-sidebar-slide-enter-active, + &.boards-sidebar-slide-leave-active { + transition: width .2s, + padding .2s; + } + + &.boards-sidebar-slide-enter, + &.boards-sidebar-slide-leave-active { + width: 0; + padding-left: 0; + padding-right: 0; + } +} diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/pages/branches.scss new file mode 100644 index 00000000000..3e2fa8ca88d --- /dev/null +++ b/app/assets/stylesheets/pages/branches.scss @@ -0,0 +1,55 @@ +.divergence-graph { + padding: 12px 12px 0 0; + float: right; + + .graph-side { + position: relative; + width: 80px; + height: 22px; + padding: 5px 0 13px; + float: left; + + .bar { + position: absolute; + height: 4px; + background-color: $divergence-graph-bar-bg; + } + + .bar-behind { + right: 0; + border-radius: 3px 0 0 3px; + } + + .bar-ahead { + left: 0; + border-radius: 0 3px 3px 0; + } + + .count { + padding-top: 6px; + padding-bottom: 0; + font-size: 12px; + color: $gl-text-color; + display: block; + } + + .count-behind { + padding-right: 4px; + text-align: right; + } + + .count-ahead { + padding-left: 4px; + text-align: left; + } + } + + .graph-separator { + position: relative; + width: 1px; + height: 18px; + margin: 5px 0 0; + float: left; + background-color: $divergence-graph-separator-bg; + } +} diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index f1d311cabbe..fd101d43b5b 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -1,7 +1,38 @@ +@keyframes fade-out-status { + 0%, 50% { opacity: 1; } + 100% { opacity: 0; } +} + +@keyframes blinking-dots { + 0% { + background-color: rgba($white-light, 1); + box-shadow: 12px 0 0 0 rgba($white-light,0.2), + 24px 0 0 0 rgba($white-light,0.2); + } + + 25% { + background-color: rgba($white-light, 0.4); + box-shadow: 12px 0 0 0 rgba($white-light,2), + 24px 0 0 0 rgba($white-light,0.2); + } + + 75% { + background-color: rgba($white-light, 0.4); + box-shadow: 12px 0 0 0 rgba($white-light,0.2), + 24px 0 0 0 rgba($white-light,1); + } + + 100% { + background-color: rgba($white-light, 1); + box-shadow: 12px 0 0 0 rgba($white-light,0.2), + 24px 0 0 0 rgba($white-light,0.2); + } +} + .build-page { pre.trace { - background: #111; - color: #fff; + background: $builds-trace-bg; + color: $white-light; font-family: $monospace_font; white-space: pre-wrap; overflow: auto; @@ -14,32 +45,99 @@ } } - .scroll-controls { - .scroll-step { - width: 31px; - margin: 0 0 0 auto; + .environment-information { + background-color: $gray-light; + border: 1px solid $border-color; + padding: 12px $gl-padding; + border-radius: $border-radius-default; + + svg { + position: relative; + top: 1px; + margin-right: 5px; } + } +} + +.scroll-controls { + height: 100%; + + .scroll-step { + width: 31px; + margin: 0 0 0 auto; + } + + .scroll-link, + .autoscroll-container { + right: 25px; + z-index: 1; + } - &.affix-bottom { - position: absolute; - right: 25px; + .scroll-link { + position: fixed; + display: block; + margin-bottom: 10px; + + &.scroll-top .gitlab-icon-scroll-up-hover, + &.scroll-top:hover .gitlab-icon-scroll-up, + &.scroll-bottom .gitlab-icon-scroll-down-hover, + &.scroll-bottom:hover .gitlab-icon-scroll-down { + display: none; } - &.affix { - right: 25px; - bottom: 15px; - z-index: 1; + &.scroll-top:hover .gitlab-icon-scroll-up-hover, + &.scroll-bottom:hover .gitlab-icon-scroll-down-hover { + display: inline-block; } - &.sidebar-expanded { - right: #{$gutter_width + ($gl-padding * 2)}; + &.scroll-top { + top: 110px; } - a { - display: block; - margin-bottom: 10px; + &.scroll-bottom { + bottom: -2px; } } + + .autoscroll-container { + position: absolute; + } + + &.sidebar-expanded { + + .scroll-link, + .autoscroll-container { + right: ($gutter_width + ($gl-padding * 2)); + } + } +} + +.status-message { + display: inline-block; + color: $white-light; + + .status-icon { + display: inline-block; + width: 16px; + height: 33px; + } + + .status-text { + float: left; + opacity: 0; + margin-right: 10px; + font-weight: normal; + line-height: 1.8; + transition: opacity 1s ease-out; + + &.animate { + animation: fade-out-status 2s ease; + } + } + + &:hover .status-text { + opacity: 1; + } } .build-header { @@ -49,12 +147,9 @@ min-height: 58px; align-items: center; - .btn-inverted { - @include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light); - } - @media (max-width: $screen-sm-max) { padding-right: 40px; + margin-top: 6px; .btn-inverted { display: none; @@ -63,14 +158,14 @@ .header-content { flex: 1; - } - a { - color: $gl-gray; + a { + color: $gl-text-color; - &:hover { - color: $gl-link-color; - text-decoration: none; + &:hover { + color: $gl-link-color; + text-decoration: none; + } } } @@ -86,8 +181,8 @@ } .build-trace { - background: $ci-output-bg; - color: $ci-text-color; + background: $black; + color: $gray-darkest; white-space: pre; overflow-x: auto; font-size: 12px; @@ -99,6 +194,15 @@ .bash { display: block; } + + .build-loader-animation { + position: relative; + width: 6px; + height: 6px; + margin: auto auto 12px 2px; + border-radius: 50%; + animation: blinking-dots 1s linear infinite; + } } .right-sidebar.build-sidebar { @@ -238,6 +342,12 @@ } } +.build-sidebar { + .container-fluid.container-limited { + max-width: 100%; + } +} + .build-detail-row { margin-bottom: 5px; @@ -247,7 +357,7 @@ } .build-light-text { - color: $gl-placeholder-color; + color: $gl-text-color-secondary; } .build-gutter-toggle { diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss index 87c453a7a27..90643832390 100644 --- a/app/assets/stylesheets/pages/ci_projects.scss +++ b/app/assets/stylesheets/pages/ci_projects.scss @@ -1,7 +1,7 @@ .ci-body { .project-title { margin: 0; - color: #444; + color: $common-gray-dark; font-size: 20px; line-height: 1.5; } @@ -18,7 +18,7 @@ } td { - color: $gl-gray; + color: $gl-text-color; vertical-align: middle !important; a { diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss deleted file mode 100644 index 47d3e72679b..00000000000 --- a/app/assets/stylesheets/pages/commit.scss +++ /dev/null @@ -1,252 +0,0 @@ -.commit-title { - display: block; -} - -.commit-author, -.commit-committer { - display: block; - color: #999; - font-weight: normal; - font-style: italic; -} - -.commit-author strong, -.commit-committer strong { - font-weight: bold; - font-style: normal; -} - -.commit-description { - background: none; - border: none; - margin: 0; - padding: 0; - margin-top: 10px; - word-break: normal; - white-space: pre-wrap; -} - -.commit-info-row { - margin-bottom: 10px; - line-height: 24px; - padding-top: 6px; - - &.commit-info-row-header { - line-height: 34px; - padding: 10px 0; - margin-bottom: 0; - - @media (min-width: $screen-sm-min) { - display: flex; - align-items: center; - - .commit-meta { - flex: 1; - } - } - - .commit-hash-full { - @media (max-width: $screen-sm-max) { - width: 80px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: inline-block; - vertical-align: bottom; - } - } - - .commit-action-buttons { - i { - color: $gl-icon-color; - font-size: 13px; - margin-right: 3px; - } - - @media (max-width: $screen-xs-max) { - .dropdown { - width: 100%; - margin-top: 10px; - } - - .dropdown-toggle { - width: 100%; - } - } - } - } - - .avatar { - @extend .avatar-inline; - margin-left: 0; - - @media (min-width: $screen-sm-min) { - margin-left: 4px; - } - } - - .commit-committer-link, - .commit-author-link { - color: $gl-gray; - font-weight: bold; - } - - .fa-clipboard { - color: $dropdown-title-btn-color; - } - - .commit-info { - &.branches { - margin-left: 8px; - } - } - - .ci-status-link { - - svg { - position: relative; - top: 2px; - margin: 0 2px 0 3px; - } - } -} - -.js-details-expand { - &:hover { - text-decoration: none; - } -} - -.commit-info-widget { - background: $background-color; - color: $gl-gray; - border: 1px solid $border-color; - border-radius: $border-radius-default; - - .widget-row { - padding: $gl-padding; - - &:not(:last-of-type) { - border-bottom: 1px solid $widget-inner-border; - } - - &.branch-info { - .monospace, - .commit-info { - margin-left: 4px; - } - } - } - - .icon-container { - display: inline-block; - margin-right: 8px; - - svg { - position: relative; - top: 2px; - height: 16px; - width: 16px; - } - - &.commit-icon { - svg { - path { - fill: $gl-text-color; - } - } - } - } - - .label.label-gray { - background-color: $widget-expand-item; - } -} - -.ci-status-link { - svg { - overflow: visible; - } -} - -.commit-box { - border-top: 1px solid $border-color; - padding: $gl-padding 0; - - .commit-title { - margin: 0; - font-size: 23px; - color: $gl-gray-dark; - } - - .commit-description { - margin-top: 15px; - } -} - -.file-stats { - ul { - list-style: none; - margin: 0; - padding: 10px 0; - - li { - padding: 3px 0; - line-height: 20px; - } - } - - .new-file { - a { - color: $gl-text-green; - } - } - - .renamed-file { - a { - color: $gl-text-orange; - } - } - - .deleted-file { - a { - color: $gl-text-red; - } - } - - .edit-file { - a { - color: $gl-text-color; - } - } -} - -/* - * Commit message textarea for web editor and - * custom merge request message - */ -.commit-message-container { - background-color: $body-bg; - position: relative; - font-family: $monospace_font; - $left: 12px; - overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987 - .max-width-marker { - width: 72ch; - color: rgba(0, 0, 0, 0.0); - font-family: inherit; - left: $left; - height: 100%; - border-right: 1px solid mix($input-border, white); - position: absolute; - z-index: 1; - } - - > textarea { - background-color: rgba(0, 0, 0, 0.0); - font-family: inherit; - padding-left: $left; - position: relative; - z-index: 2; - } -} diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 98a84351a3d..fef8e8eec27 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -1,15 +1,83 @@ +.commit-description { + background: none; + border: none; + padding: 0; + margin-top: 10px; + word-break: normal; + white-space: pre-wrap; +} + +.js-details-expand { + &:hover { + text-decoration: none; + } +} + +.commit-box { + border-top: 1px solid $border-color; + padding: $gl-padding 0; + + .commit-title { + margin: 0; + color: $gl-text-color; + } + + .commit-description { + margin-top: 15px; + } +} + +.commit-hash-full { + @media (max-width: $screen-sm-max) { + width: 80px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + vertical-align: bottom; + } +} + +/* + * Commit message textarea for web editor and + * custom merge request message + */ +.commit-message-container { + background-color: $body-bg; + position: relative; + font-family: $monospace_font; + $left: 12px; + overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987 + .max-width-marker { + width: 72ch; + color: $commit-max-width-marker-color; + font-family: inherit; + left: $left; + height: 100%; + border-right: 1px solid mix($input-border, $white-light); + position: absolute; + z-index: 1; + } + + textarea { + background-color: $commit-message-text-area-bg; + font-family: inherit; + padding-left: $left; + position: relative; + z-index: 2; + } +} + + .commits-compare-switch { - @include btn-default; - @include btn-white; float: left; margin-right: 9px; } .commit-header { padding: 5px 10px; - background-color: $background-color; - border-top: 1px solid #eee; - border-bottom: 1px solid #eee; + background-color: $gray-light; + border-bottom: 1px solid $gray-darker; font-size: 14px; &:first-child { @@ -18,8 +86,6 @@ } .commit-row-title { - line-height: 1.35; - .notes_count { float: right; margin-right: 10px; @@ -30,15 +96,14 @@ } .commit-row-message { - color: $gl-dark-link-color; + color: $gl-text-color; } - } .text-expander { display: inline-block; background: $gray-light; - color: $gl-placeholder-color; + color: $gl-text-color-secondary; padding: 0 5px; cursor: pointer; border: 1px solid $border-gray-dark; @@ -54,14 +119,15 @@ .commit-actions { @media (min-width: $screen-sm-min) { - float: right; - margin-left: $gl-padding; - margin-top: 2px; + width: 300px; + text-align: right; font-size: 0; } .ci-status-link { display: inline-block; + position: relative; + top: 1px; } .btn-clipboard, @@ -82,40 +148,18 @@ font-weight: 600; } -.commit { - padding: 10px 0; - position: relative; - - @media (min-width: $screen-sm-min) { - padding-left: 46px; - } - - &:not(:last-child) { - border-bottom: 1px solid #eee; - } +.commit, +.generic_commit_status { a, button { - color: $gl-dark-link-color; + color: $gl-text-color; vertical-align: baseline; } - - .avatar { - margin-left: -46px; - } - - .item-title { - display: inline-block; - - @media (min-width: $screen-sm-min) { - max-width: 70%; - } - } - .commit-row-description { font-size: 14px; - border-left: 1px solid $btn-gray-hover; + border-left: 1px solid $white-normal; padding: 10px 15px; margin: 10px 0; background: $gray-light; @@ -132,20 +176,7 @@ } a { - color: $gl-dark-link-color; - } - } - - .commit-row-info { - color: $gl-gray; - line-height: 1.35; - - a { - color: $gl-gray; - } - - .avatar { - margin-right: 8px; + color: $gl-text-color; } } @@ -162,7 +193,7 @@ } .branch-commit { - color: $gl-gray; + color: $gl-text-color; .commit-icon { text-align: center; @@ -172,7 +203,7 @@ height: 14px; width: 14px; vertical-align: middle; - fill: $table-text-gray; + fill: $gl-text-color-secondary; } } @@ -181,62 +212,6 @@ } .commit-row-message { - color: $gl-gray; - } -} - -.divergence-graph { - padding: 12px 12px 0 0; - float: right; - - .graph-side { - position: relative; - width: 80px; - height: 22px; - padding: 5px 0 13px; - float: left; - - .bar { - position: absolute; - height: 4px; - background-color: #ccc; - } - - .bar-behind { - right: 0; - border-radius: 3px 0 0 3px; - } - - .bar-ahead { - left: 0; - border-radius: 0 3px 3px 0; - } - - .count { - padding-top: 6px; - padding-bottom: 0; - font-size: 12px; - color: #333; - display: block; - } - - .count-behind { - padding-right: 4px; - text-align: right; - } - - .count-ahead { - padding-left: 4px; - text-align: left; - } - } - - .graph-separator { - position: relative; - width: 1px; - height: 18px; - margin: 5px 0 0; - float: left; - background-color: #ccc; + color: $gl-text-color; } } diff --git a/app/assets/stylesheets/pages/confirmation.scss b/app/assets/stylesheets/pages/confirmation.scss deleted file mode 100644 index 81e5cee240d..00000000000 --- a/app/assets/stylesheets/pages/confirmation.scss +++ /dev/null @@ -1,32 +0,0 @@ -.well-confirmation { - margin-bottom: 20px; - border-bottom: 1px solid #eee; - - > h1, - h2, - h3, - h4, - h5, - h6 { - font-weight: 400; - } - - .lead { - margin-bottom: 20px; - } - - ul, - ol { - padding-left: 0; - } - - li { - list-style-type: none; - } -} - -.confirmation-content { - a { - color: $md-link-color; - } -} diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 572e1e7d558..cda069e6c0e 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -1,16 +1,63 @@ #cycle-analytics { + max-width: 1000px; margin: 24px auto 0; - max-width: 800px; position: relative; - .panel { + .col-headers { + ul { + margin: 0; + padding: 0; + @include clearfix; + } + + li { + display: inline-block; + float: left; + line-height: 50px; + width: 20%; + } + + + .fa { + color: $cycle-analytics-light-gray; + + &:hover { + color: $gl-text-color; + } + } + + .stage-header { + width: 26%; + padding-left: $gl-padding; + } + + .median-header { + width: 14%; + } + + .event-header { + width: 45%; + padding-left: $gl-padding; + } + + .total-time-header { + width: 15%; + text-align: right; + padding-right: $gl-padding; + } + .stage-name { + font-weight: 600; + } + } + + .panel { .content-block { padding: 24px 0; border-bottom: none; position: relative; - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { padding: 6px 0 24px; } } @@ -18,7 +65,7 @@ .column { text-align: center; - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { padding: 15px 0; } @@ -35,23 +82,20 @@ } &:last-child { - text-align: right; - - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { text-align: center; } } } + } - .dropdown { - top: 13px; - } + .js-ca-dropdown { + top: $gl-padding-top; } .bordered-box { border: 1px solid $border-color; border-radius: $border-radius-default; - } .content-list { @@ -69,16 +113,16 @@ &.title { line-height: 19px; - font-size: 15px; + font-size: 14px; font-weight: 600; - color: $gl-title-color; + color: $gl-text-color; } &.text { color: $layout-link-gray; &.value-col { - color: $gl-title-color; + color: $gl-text-color; } } } @@ -103,7 +147,7 @@ position: absolute; right: $cycle-analytics-box-padding; cursor: pointer; - color: #b2b2b2; + color: $cycle-analytics-dismiss-icon-color; } .svg-container { @@ -116,7 +160,7 @@ } .inner-content { - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { padding: 0 28px; text-align: center; } @@ -141,4 +185,302 @@ margin-top: 36px; } + .stage-panel-body { + display: flex; + flex-wrap: wrap; + } + + .stage-nav, + .stage-entries { + display: flex; + vertical-align: top; + font-size: $gl-font-size; + } + + .stage-nav { + width: 40%; + margin-bottom: 0; + + ul { + padding: 0; + margin: 0; + width: 100%; + } + + li { + list-style-type: none; + @include clearfix; + } + + .stage-nav-item { + display: block; + line-height: 65px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + border-right: 1px solid $border-color; + background-color: $gray-light; + + &.active { + background-color: transparent; + border-right-color: transparent; + border-top-color: $border-color; + border-bottom-color: $border-color; + box-shadow: inset 2px 0 0 0 $active-item-blue; + + .stage-name { + font-weight: 600; + } + } + + &:hover:not(.active) { + background-color: $gray-lightest; + box-shadow: inset 2px 0 0 0 $border-color; + cursor: pointer; + } + + &:first-child { + border-top: none; + } + + &:last-child { + border-bottom: none; + } + + .stage-nav-item-cell { + float: left; + + &.stage-name { + width: 65%; + } + + &.stage-median { + width: 35%; + } + } + + .stage-name { + padding-left: 16px; + } + + .stage-empty, + .not-available { + color: $gl-text-color-secondary; + } + } + } + + .stage-panel-container { + width: 100%; + overflow: auto; + } + + .stage-panel { + min-width: 968px; + + .panel-heading { + padding: 0; + background-color: transparent; + } + + .events-description { + line-height: 65px; + padding-left: $gl-padding; + } + } + + .stage-events { + width: 60%; + overflow: scroll; + height: 467px; + } + + .stage-event-list { + margin: 0; + padding: 0; + } + + .stage-event-item { + list-style-type: none; + padding: 0 0 $gl-padding; + margin: 0 $gl-padding $gl-padding; + border-bottom: 1px solid $gray-darker; + @include clearfix; + + &:last-child { + border-bottom: none; + margin-bottom: 0; + } + + .item-details, + .item-time { + float: left; + } + + .item-details { + width: 75%; + } + + .item-title { + margin: 0 0 2px; + + &.issue-title, + &.commit-title, + &.merge-merquest-title { + max-width: 100%; + display: block; + @include text-overflow(); + + a { + color: $gl-text-color; + } + } + } + + .item-time { + width: 25%; + text-align: right; + } + + .total-time { + font-size: $cycle-analytics-big-font; + color: $cycle-analytics-dark-text; + + span { + color: $gl-text-color; + font-size: $gl-font-size; + } + } + + .issue-date, + .build-date { + color: $gl-text-color; + } + + .issue-link, + .commit-author-link, + .issue-author-link { + color: $gl-text-color; + } + + // Custom CSS for components + .item-conmmit-component { + .commit-icon { + position: relative; + top: 3px; + left: 1px; + display: inline-block; + + svg { + float: left; + } + } + } + + .merge-request-branch { + a { + max-width: 180px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + display: inline-block; + vertical-align: bottom; + } + } + } + + // Custom Styles for stage items + .item-build-component { + + .item-title { + .icon-build-status { + float: left; + margin-right: 5px; + position: relative; + top: 2px; + } + + .item-build-name { + color: $gl-text-color; + } + + .pipeline-id { + color: $gl-text-color; + padding: 0 3px 0 0; + } + + .branch-name { + color: $black; + display: inline-block; + max-width: 180px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + line-height: 1.3; + vertical-align: top; + } + + .short-sha { + color: $gl-link-color; + line-height: 1.3; + vertical-align: top; + font-weight: normal; + } + + .fa { + color: $gl-text-color-secondary; + font-size: $code_font_size; + } + } + } + + .empty-stage, + .no-access-stage { + text-align: center; + width: 75%; + margin: 0 auto; + padding-top: 130px; + color: $gl-text-color-secondary; + + h4 { + color: $gl-text-color; + } + } + + .empty-stage { + .icon-no-data { + height: 36px; + width: 78px; + display: inline-block; + margin-bottom: 20px; + } + } + + .no-access-stage { + .icon-lock { + height: 36px; + width: 78px; + display: inline-block; + margin-bottom: 20px; + } + } +} + +.cycle-analytics-overview { + padding-top: 100px; + + .overview-details { + display: flex; + align-items: center; + } + + .overview-image { + text-align: right; + } + + .overview-icon { + svg { + width: 365px; + height: 227px; + } + } } diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss deleted file mode 100644 index 016bab104eb..00000000000 --- a/app/assets/stylesheets/pages/dashboard.scss +++ /dev/null @@ -1,43 +0,0 @@ -.dashboard { - .side { - .panel { - .panel-heading { - background: $background-color; - border-top-left-radius: 0; - } - - border-top-left-radius: 0; - } - } -} - -.dashboard-search-filter { - padding: 5px; - - .search-text-input { - float: left; - @extend .col-md-2; - } - - .btn { - margin-left: 5px; - float: left; - } -} - -.project-access-icon { - margin-left: 10px; - float: left; - margin-right: 15px; - margin-bottom: 15px; - - i { - color: #888; - } -} - -.dash-project-access-icon { - float: left; - margin-right: 5px; - width: 16px; -} diff --git a/app/assets/stylesheets/pages/deploy_keys.scss b/app/assets/stylesheets/pages/deploy_keys.scss new file mode 100644 index 00000000000..2fafe052106 --- /dev/null +++ b/app/assets/stylesheets/pages/deploy_keys.scss @@ -0,0 +1,13 @@ +.deploy-keys-list { + width: 100%; + overflow: auto; + + table { + border: 1px solid $table-border-color; + } +} + +.deploy-keys-title { + padding-bottom: 2px; + line-height: 2; +} diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 0f0c0abe7ae..46fd19c93f9 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -1,16 +1,15 @@ .detail-page-header { padding: $gl-padding-top 0; border-bottom: 1px solid $border-color; - color: #5c5d5e; - font-size: 16px; + color: $gl-text-color; line-height: 34px; .author { - color: #5c5d5e; + color: $gl-text-color; } .identifier { - color: #5c5d5e; + color: $gl-text-color; } .issue_created_ago, @@ -23,7 +22,7 @@ .title { margin: 0 0 16px; font-size: 2em; - color: $gl-gray-dark; + color: $gl-text-color; padding: 0 0 0.3em; border-bottom: 1px solid $white-dark; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index fde138c874d..96ba7c40634 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -11,10 +11,10 @@ .diff-header { position: relative; - background: $background-color; + background: $gray-light; border-bottom: 1px solid $border-color; padding: 10px 16px; - color: #555; + color: $gl-text-color; z-index: 10; border-radius: 3px 3px 0 0; @@ -24,7 +24,7 @@ display: block; .file-mode { - color: #777; + color: $file-mode-changed; } } @@ -38,7 +38,7 @@ cursor: pointer; &:hover { - background-color: $dark-background-color; + background-color: $gray-normal; } .diff-toggle-caret { @@ -49,8 +49,8 @@ .diff-content { overflow: auto; overflow-y: hidden; - background: #fff; - color: #333; + background: $white-light; + color: $gl-text-color; border-radius: 0 0 3px 3px; .unfold { @@ -59,7 +59,7 @@ .file-mode-changed { padding: 10px; - color: #777; + color: $file-mode-changed; } .suppressed-container { @@ -92,20 +92,6 @@ &.noteable_line { position: relative; - - &.old { - &::before { - content: '-'; - position: absolute; - } - } - - &.new { - &::before { - content: '+'; - position: absolute; - } - } } span { @@ -151,8 +137,9 @@ .line_content { display: block; margin: 0; - padding: 0 0.5em; + padding: 0 1.5em; border: none; + position: relative; &.parallel { display: table-cell; @@ -161,6 +148,22 @@ word-break: break-all; } } + + &.old { + &::before { + content: '-'; + position: absolute; + left: 0.5em; + } + } + + &.new { + &::before { + content: '+'; + position: absolute; + left: 0.5em; + } + } } .text-file.diff-wrap-lines table .line_holder td span { @@ -169,7 +172,7 @@ } .image { - background: #ddd; + background: $diff-image-bg; text-align: center; padding: 30px; @@ -179,13 +182,13 @@ .frame { display: inline-block; - background-color: #fff; + background-color: $white-light; line-height: 0; img { - border: 1px solid #fff; - background-image: linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5 100%), - linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5 100%); + border: 1px solid $white-light; + background-image: linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%), + linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%); background-size: 10px 10px; background-position: 0 0, 5px 5px; max-width: 100%; @@ -203,7 +206,7 @@ .image-info { font-size: 12px; margin: 5px 0 0; - color: grey; + color: $diff-image-info-color; } .view.swipe { @@ -217,7 +220,7 @@ .swipe-wrap { overflow: hidden; - border-left: 1px solid #999; + border-left: 1px solid $diff-swipe-border; position: absolute; display: block; top: 13px; @@ -347,7 +350,7 @@ .view-modes { padding: 10px; text-align: center; - background: #eee; + background: $gray-darker; ul, li { @@ -358,8 +361,8 @@ } li { - color: grey; - border-left: 1px solid #c1c1c1; + color: $diff-view-modes-color; + border-left: 1px solid $diff-view-modes-border; padding: 0 12px 0 16px; cursor: pointer; @@ -377,7 +380,7 @@ } cursor: default; - color: #333; + color: $gl-text-color; } &.disabled { diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 778126bcfb7..4af267403d8 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -10,14 +10,14 @@ } .ace_gutter-cell { - background-color: $background-color; + background-color: $gray-light; } .cancel-btn { - color: #b94a48; + color: $editor-cancel-color; &:hover { - color: #b94a48; + color: $editor-cancel-color; } } @@ -34,7 +34,7 @@ } .editor-ref { - background: $background-color; + background: $gray-light; padding-right: $gl-padding; border-right: 1px solid $border-color; display: block; @@ -51,8 +51,16 @@ .new-file-name { display: inline-block; - width: 450px; + max-width: 450px; float: left; + + @media(max-width: $screen-md-max) { + width: 280px; + } + + @media(max-width: $screen-sm-max) { + width: 180px; + } } .file-buttons { @@ -67,7 +75,8 @@ .soft-wrap-toggle, .license-selector, .gitignore-selector, - .gitlab-ci-yml-selector { + .gitlab-ci-yml-selector, + .dockerfile-selector { display: inline-block; vertical-align: top; font-family: $regular_font; @@ -97,7 +106,8 @@ .gitignore-selector, .license-selector, - .gitlab-ci-yml-selector { + .gitlab-ci-yml-selector, + .dockerfile-selector { .dropdown { line-height: 21px; } @@ -114,3 +124,42 @@ } } } + +@media(max-width: $screen-xs-max){ + .file-editor { + .file-title { + .pull-right { + height: auto; + } + } + + .new-file-name { + max-width: none; + width: 100%; + margin-bottom: 3px; + } + + .file-buttons { + display: block; + width: 100%; + margin-bottom: 10px; + + .soft-wrap-toggle { + width: 100%; + margin: 3px 0; + } + + .encoding-selector, + .license-selector, + .gitignore-selector, + .gitlab-ci-yml-selector { + display: block; + margin: 3px 0; + + button { + width: 100%; + } + } + } + } +} diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index fc49ff780fc..778ef01430e 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -1,10 +1,56 @@ -.environments-container, -.deployments-container { +@media (max-width: $screen-md-max) { + .deployments-container { + width: 100%; + overflow: auto; + } +} + +.environments-list-loading { width: 100%; - overflow: auto; + font-size: 34px; +} + +@media (max-width: $screen-xs-max) { + .environments-container { + width: 100%; + overflow: auto; + } } .environments { + table-layout: fixed; + + .environments-commit, + .environments-actions, + .environments-deploy, + .environments-build, + .environments-date { + position: static; + float: none; + display: table-cell; + } + + .environments-name, + .environments-commit, + .environments-actions { + width: 20%; + } + + .environments-date { + width: 10%; + } + + .environments-deploy, + .environments-build { + width: 15%; + } + + .environment-name, + .environments-build-cell, + .deployment-column { + word-break: break-all; + } + .deployment-column { .avatar { float: none; @@ -15,6 +61,10 @@ margin: 0; } + .avatar-image-container { + text-decoration: none; + } + .icon-play { height: 13px; width: 12px; @@ -22,24 +72,25 @@ .external-url, .dropdown-new { - color: $table-text-gray; + color: $gl-text-color-secondary; } .dropdown-menu { .fa { margin-right: 6px; - color: $table-text-gray; + color: $gl-text-color-secondary; } } .build-link, .branch-name { - color: $gl-dark-link-color; + color: $gl-text-color; } - .stop-env-link { - color: $table-text-gray; + .stop-env-link, + .external-url { + color: $gl-text-color-secondary; .stop-env-icon { font-size: 14px; @@ -50,7 +101,7 @@ .build-column { .build-link { - color: $gl-dark-link-color; + color: $gl-text-color; } .avatar { @@ -58,10 +109,22 @@ } } } + + .children-row .environment-name { + margin-left: 17px; + margin-right: -17px; + } + + .folder-icon { + padding: 0 5px 0 0; + } + + .folder-name { + cursor: pointer; + } } .table.ci-table.environments { - .icon-container { width: 20px; text-align: center; @@ -72,4 +135,4 @@ margin-right: 0; } } -} +}
\ No newline at end of file diff --git a/app/assets/stylesheets/pages/errors.scss b/app/assets/stylesheets/pages/errors.scss deleted file mode 100644 index 11309817d31..00000000000 --- a/app/assets/stylesheets/pages/errors.scss +++ /dev/null @@ -1,16 +0,0 @@ -.error-page { - max-width: 400px; - margin: 0 auto; - - h1, - h2, - h3 { - text-align: center; - } - - h1 { - font-size: 56px; - line-height: 100px; - font-weight: 300; - } -} diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 3004959ff7b..b989d72ce1c 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -5,7 +5,7 @@ .event-item { font-size: $gl-font-size; padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top); - border-bottom: 1px solid $table-border-color; + border-bottom: 1px solid $white-normal; color: $list-text-color; &.event-inline { @@ -21,7 +21,7 @@ } a { - color: $gl-dark-link-color; + color: $gl-text-color; } .avatar { @@ -62,7 +62,7 @@ border: none; background: $gray-light; border-radius: 0; - color: #777; + color: $events-pre-color; margin: 0 20px; overflow: hidden; } @@ -80,7 +80,7 @@ } .event-note-icon { - color: #777; + color: $events-pre-color; float: left; font-size: $gl-font-size; line-height: 16px; @@ -91,7 +91,7 @@ .event_icon { position: relative; float: right; - border: 1px solid #eee; + border: 1px solid $gray-darker; padding: 5px; border-radius: 5px; background: $gray-light; @@ -170,7 +170,7 @@ .event-body { margin: 0; - border-left: 2px solid #ddd; + border-left: 2px solid $events-body-border; padding-left: 10px; } @@ -186,4 +186,3 @@ display: none; } } - diff --git a/app/assets/stylesheets/pages/explore.scss b/app/assets/stylesheets/pages/explore.scss deleted file mode 100644 index 9b92128624c..00000000000 --- a/app/assets/stylesheets/pages/explore.scss +++ /dev/null @@ -1,8 +0,0 @@ -.explore-title { - text-align: center; - - h3 { - font-weight: normal; - font-size: 30px; - } -} diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss index f7f9a9bb770..84da9180f93 100644 --- a/app/assets/stylesheets/pages/graph.scss +++ b/app/assets/stylesheets/pages/graph.scss @@ -2,15 +2,15 @@ border: 1px solid $border-color; .controls { - color: #888; + color: $project-network-controls-color; font-size: 14px; padding: 5px; border-bottom: 1px solid $border-color; - background: #eee; + background: $gray-darker; } .network-graph { - background: #fff; + background: $white-light; height: 500px; overflow-y: scroll; overflow-x: hidden; @@ -20,15 +20,14 @@ .graphs { .graph-author-email { float: right; - color: #777; + color: $graph-author-email-color; } .graph-additions { - color: #4a2; + color: $gl-text-green; } .graph-deletions { - color: #d12f19; + color: $gl-text-red; } } - diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 4375e29c8db..d377526e655 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -10,11 +10,10 @@ } .group-row { - .stats { float: right; line-height: $list-text-height; - color: $gl-gray; + color: $gl-text-color; span { margin-right: 15px; @@ -23,36 +22,12 @@ } .ldap-group-links { - .form-actions { margin-bottom: $gl-padding; } } -.groups-cover-block { - - .container-fluid { - position: relative; - } - - .group-right-buttons { - position: absolute; - right: 16px; - - .btn { - @include btn-gray; - padding: 3px 10px; - background-color: $background-color; - } - } - - .group-avatar { - border: 0; - } -} - .groups-header { - @media (min-width: $screen-sm-min) { .nav-links { width: 35%; @@ -68,14 +43,14 @@ padding: 50px 100px; overflow: hidden; - @media (max-width: $screen-md-min) { + @media (max-width: $screen-sm-max) { padding: 50px 0; } svg { float: right; - @media (max-width: $screen-md-min) { + @media (max-width: $screen-sm-max) { float: none; display: block; width: 250px; @@ -90,7 +65,7 @@ width: 460px; margin-top: 120px; - @media (max-width: $screen-md-min) { + @media (max-width: $screen-sm-max) { float: none; margin-top: 60px; width: auto; diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index a48b4c65db8..dae8ccdef6c 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -9,7 +9,7 @@ li { line-height: 24px; - color: #888; + color: $document-index-color; a { margin-right: 3px; @@ -20,7 +20,7 @@ .shortcut-mappings { font-size: 12px; - color: #555; + color: $help-shortcut-mapping-color; tbody:first-child tr:first-child { padding-top: 0; @@ -29,7 +29,7 @@ th { padding-top: 15px; line-height: 1.5; - color: #333; + color: $help-shortcut-header-color; text-align: left; } @@ -42,7 +42,7 @@ .shortcut { padding-right: 10px; - color: #999; + color: $help-shortcut-color; text-align: right; white-space: nowrap; } @@ -60,7 +60,7 @@ // Border around images in the help pages. img:not(.emoji) { - border: 1px solid $table-border-gray; + border: 1px solid $white-normal; padding: 5px; margin: 5px; max-height: calc(100vh - 100px); diff --git a/app/assets/stylesheets/pages/icons.scss b/app/assets/stylesheets/pages/icons.scss deleted file mode 100644 index 407c8db211d..00000000000 --- a/app/assets/stylesheets/pages/icons.scss +++ /dev/null @@ -1,12 +0,0 @@ -// CI icon colors - -.ci-status-icon { - &-created { - fill: $gray-darkest; - } - - &-skipped, - &-canceled { - fill: $gl-text-color; - } -} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 230b927a17d..93cc5a8cf0a 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -1,3 +1,52 @@ +// Limit MR description for side-by-side diff view +.fixed-width-container { + max-width: $limited-layout-width - ($gl-padding * 2); + margin-left: auto; + margin-right: auto; +} + +.limit-container-width { + .detail-page-header { + @extend .fixed-width-container; + } + + .issuable-details { + .detail-page-description, + .mr-source-target, + .mr-state-widget, + .merge-manually { + @extend .fixed-width-container; + } + + .merge-request-tabs-holder { + &.affix { + border-bottom: 1px solid $border-color; + + .nav-links { + border: 0; + } + } + + .container-fluid { + @extend .fixed-width-container; + } + } + } + + .merge-request-details { + .emoji-list-container { + @extend .fixed-width-container; + } + } + + .diffs { + .mr-version-controls, + .files-changed { + @extend .fixed-width-container; + } + } +} + .issuable-details { section { .issuable-discussion { @@ -5,11 +54,16 @@ } } + .title { + padding: 0; + margin: 0; + border-bottom: none; + } + // Border around images in issue and MR descriptions. .description img:not(.emoji) { - border: 1px solid $table-border-gray; + border: 1px solid $white-normal; padding: 5px; - margin: 5px; max-height: calc(100vh - 100px); } } @@ -30,6 +84,7 @@ .color-label { padding: 6px 10px; + border-radius: $label-border-radius; } } @@ -50,7 +105,7 @@ .block { @include clearfix; padding: $gl-padding 0; - border-bottom: 1px solid $border-gray-light; + border-bottom: 1px solid $border-gray-normal; // This prevents the mess when resizing the sidebar // of elements repositioning themselves.. width: $gutter_inner_width; @@ -97,10 +152,10 @@ } .edit-link { - color: $gl-gray; + color: $gl-text-color; - &:hover { - color: $md-link-color; + &:not([href]):hover { + color: rgba($avatar-border, .2); } } } @@ -132,8 +187,8 @@ display: none; } - .btn-clipboard { - color: $gl-gray; + .btn-clipboard:hover { + color: $gl-text-color; } } @@ -168,7 +223,7 @@ } .no-value { - color: $gl-placeholder-color; + color: $gl-text-color-secondary; } .sidebar-collapsed-icon { @@ -177,7 +232,7 @@ .gutter-toggle { margin-top: 7px; - border-left: 1px solid $border-gray-light; + border-left: 1px solid $border-gray-normal; } .assignee .avatar { @@ -215,7 +270,7 @@ } .participants { - border-bottom: 1px solid $border-gray-light; + border-bottom: 1px solid $border-gray-normal; } .hide-collapsed { @@ -233,7 +288,11 @@ width: 100%; text-align: center; padding-bottom: 10px; - color: #999; + color: $issuable-sidebar-color; + + &:hover { + color: $gl-text-color; + } span { display: block; @@ -244,15 +303,17 @@ display: none; } + .avatar:hover { + border-color: $issuable-sidebar-color; + } + .btn-clipboard { border: none; + color: $issuable-sidebar-color; &:hover { background: transparent; - } - - i { - color: #999; + color: $gl-text-color; } } } @@ -267,24 +328,14 @@ } } - .issuable-header-btn { - background: $gray-normal; - border: 1px solid $border-gray-normal; - - &:hover { - background: $gray-dark; - border: 1px solid $border-gray-dark; - } - - &.btn-primary { - @extend .btn-primary; - } - } - a { &:hover { color: $md-link-color; text-decoration: none; + + .avatar { + border-color: rgba($avatar-border, .2); + } } } @@ -326,6 +377,10 @@ display: inline-block; padding: 5px; + &:nth-of-type(7n) { + padding-right: 0; + } + .author_link { display: block; } @@ -340,7 +395,7 @@ margin-left: 5px; a { - color: $gl-placeholder-color; + color: $gl-text-color-secondary; } } @@ -395,6 +450,7 @@ .issuable-meta { display: inline-block; line-height: 18px; + font-size: 14px; } .js-issuable-selector-wrap { @@ -421,3 +477,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/issues.scss b/app/assets/stylesheets/pages/issues.scss index 3e7fc3fa52c..8734a3b1598 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -55,26 +55,45 @@ ul.related-merge-requests > li { } .merge-request-status { - color: $gl-gray; - font-size: 15px; - font-weight: bold; + font-size: 13px; + padding: 0 5px; + color: $white-light; + height: 20px; + border-radius: 3px; + line-height: 18px; + border: 1px solid; + + &.merged { + border-color: darken($blue-normal, 10%); + background: $blue-normal; + } + + &.closed { + border-color: darken($red-normal, 10%); + background: $red-normal; + } + + &.open { + border: 1px solid darken($green-normal, 10%); + background: $green-normal; + } } .merge-request, .issue { &.today { - background: #f3fff2; - border-color: #e1e8d5; + background: $issues-today-bg; + border-color: $issues-today-border; } &.closed { background: $gray-light; - border-color: #e5e5e5; + border-color: $border-color; } &.merged { background: $gray-light; - border-color: #e5e5e5; + border-color: $border-color; } } @@ -125,7 +144,7 @@ ul.related-merge-requests > li { } .btn { - background-color: $background-color; - border: 1px solid $border-gray-light; + background-color: $gray-light; + border: 1px solid $border-gray-normal; } } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 397f89f501a..21d9b4c54ea 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -90,7 +90,7 @@ @media (min-width: $screen-sm-min) { display: inline-block; - width: 40%; + width: 30%; margin-left: 10px; margin-bottom: 0; vertical-align: middle; @@ -98,13 +98,14 @@ } .label { - padding: 9px; + padding: 8px 9px 9px; font-size: 14px; } } .color-label { - padding: 3px 4px; + padding: 3px 7px; + border-radius: $label-border-radius; } .dropdown-labels-error { @@ -116,7 +117,7 @@ .manage-labels-list { .btn-action { - color: $gl-dark-link-color; + color: $gl-text-color; .fa { font-size: 18px; @@ -198,8 +199,14 @@ } .label-remove { - border-left: 1px solid rgba(0, 0, 0, .1); + border-left: 1px solid $label-remove-border; z-index: 3; + border-radius: $label-border-radius; + padding: 6px 10px 6px 9px; + + &:hover { + box-shadow: inset 0 0 0 80px $label-remove-border; + } } .btn { @@ -222,6 +229,14 @@ width: 100%; } +.label-subscription { + vertical-align: middle; + + .dropdown-group-label a { + cursor: pointer; + } +} + .label-subscribe-button { .label-subscribe-button-icon { &[disabled] { diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss index 8290519dc25..68b6c5ecbd4 100644 --- a/app/assets/stylesheets/pages/lint.scss +++ b/app/assets/stylesheets/pages/lint.scss @@ -1,11 +1,21 @@ .ci-body { .incorrect-syntax { - font-size: 19px; - color: red; + font-size: 18px; + color: $lint-incorrect-color; } .correct-syntax { - font-size: 19px; - color: #47a447; + font-size: 18px; + color: $lint-correct-color; + } +} + +.ci-linter { + .ci-editor { + height: 400px; + } + + .ci-template pre { + white-space: pre-wrap; } } diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 10f67b47998..71ed5b1361a 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -17,14 +17,19 @@ line-height: 1.5; p { - font-size: 18px; - color: #888; + font-size: 16px; + color: $login-brand-holder-color; } h1:first-child { font-weight: normal; - margin-bottom: 30px; + margin-bottom: 0.68em; margin-top: 0; + font-size: 34px; + } + + h3 { + font-size: 22px; } img { @@ -105,19 +110,19 @@ li { flex: 1; text-align: center; + border-left: 1px solid $border-color; &:first-of-type { + border-left: none; border-top-left-radius: $border-radius-default; } &:last-of-type { - border-left: 1px solid $border-color; border-top-right-radius: $border-radius-default; } &:not(.active) { background-color: $gray-light; - border-left: 1px solid $border-color; } a { @@ -174,7 +179,7 @@ .form-control { &:active, &:focus { - background-color: #fff; + background-color: $white-light; } } @@ -195,7 +200,7 @@ h2 { margin-top: 0; font-size: 14px; - color: #a00; + color: $login-devise-error-color; } } } @@ -254,27 +259,3 @@ } } } - -// For sign in pane only, to improve tab order, the following removes the submit button from -// normal document flow and pins it to the bottom of the form. For context, see !6867 & !6928 - -.login-box { - .new_user { - position: relative; - padding-bottom: 35px; - - @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - .forgot-password { - float: none !important; - margin-top: 5px; - } - } - } - - .move-submit-down { - position: absolute; - width: 100%; - bottom: 0; - } -} - diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 756efa9c7fa..be7193bae04 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -25,7 +25,7 @@ } .form-horizontal { - margin-top: 5px; + margin-top: 20px; @media (min-width: $screen-sm-min) { display: -webkit-flex; @@ -54,6 +54,10 @@ @media (min-width: $screen-sm-min) { width: 50%; } + + .dropdown-menu-toggle { + width: 100%; + } } .member-access-text { @@ -74,19 +78,48 @@ float: right; } + .dropdown { + width: 100%; + margin-top: 5px; + + .dropdown-menu-toggle { + vertical-align: middle; + width: 100%; + } + + @media (min-width: $screen-sm-min) { + margin-top: 0; + width: 155px; + } + } + .form-control { width: 100%; padding-right: 35px; @media (min-width: $screen-sm-min) { + width: 250px; + } + + @media (min-width: $screen-md-min) { width: 350px; } + + &.input-short { + @media (min-width: $screen-md-min) { + width: 170px; + } + + @media (min-width: $screen-lg-min) { + width: 210px; + } + } } } .member-search-btn { position: absolute; - right: 0; + right: 4px; top: 0; height: 35px; padding-left: 10px; @@ -95,4 +128,8 @@ background: transparent; border: 0; outline: 0; + + @media (min-width: $screen-sm-min) { + right: 160px; + } } diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 19ab198c2e7..5a9f199fb34 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -1,3 +1,5 @@ +// Disabled to use the color map for creating color schemes +// scss-lint:disable ColorVariable $colors: ( white_header_head_neutral : #e1fad7, white_line_head_neutral : #effdec, @@ -98,6 +100,7 @@ $colors: ( solarized_dark_header_not_chosen : rgba(#839496, .25), solarized_dark_line_not_chosen : rgba(#839496, .15) ); +// scss-lint:enable ColorVariable @mixin color-scheme($color) { @@ -228,14 +231,15 @@ $colors: ( position: absolute; right: 10px; padding: 0; - color: #fff; + outline: none; + color: $white-light; width: 75px; // static width to make 2 buttons have same width height: 19px; } } .btn-success .fa-spinner { - color: #fff; + color: $white-light; } .editor-wrap { @@ -270,7 +274,7 @@ $colors: ( } .discard-changes-alert { - background-color: $background-color; + background-color: $gray-light; text-align: right; padding: $gl-padding-top $gl-padding; color: $gl-text-color; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index f8e31a624ec..45ff9f7ff5f 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -3,8 +3,8 @@ * */ .mr-state-widget { - background: $background-color; - color: $gl-gray; + background: $gray-light; + color: $gl-text-color; border: 1px solid $border-color; border-radius: 2px; @@ -21,10 +21,18 @@ display: inline-block; float: left; + .btn-success.dropdown-toggle .fa { + color: inherit; + } + + .btn-success.dropdown-toggle:disabled { + background-color: $gl-success; + } + .accept_merge_request { &.ci-pending, &.ci-running { - @include btn-orange; + @include btn-blue; } &.ci-skipped, @@ -47,9 +55,10 @@ &.right { float: right; + padding-right: 0; a { - color: $gl-gray; + color: $gl-text-color; } } @@ -60,7 +69,8 @@ } .ci_widget { - border-bottom: 1px solid $widget-inner-border; + border-bottom: 1px solid $well-inner-border; + color: $gl-text-color; svg { margin-right: 4px; @@ -69,48 +79,12 @@ overflow: visible; } - &.ci-success { - color: $gl-success; - - a.environment, - a.pipeline { - color: inherit; - } - } - &.ci-success_with_warnings { - color: $gl-success; i { color: $gl-warning; } } - - &.ci-skipped { - background-color: #eee; - color: #888; - } - - &.ci-pending { - color: $gl-warning; - } - - &.ci-running { - color: $blue-normal; - } - - &.ci-failed, - &.ci-error { - color: $gl-danger; - } - - &.ci-canceled { - color: $gl-gray; - } - - a.monospace { - color: inherit; - } } .mr-widget-body, @@ -120,7 +94,7 @@ } .normal { - color: #5c5d5e; + color: $gl-text-color; } .js-deployment-link { @@ -130,9 +104,9 @@ .mr-widget-body { h4 { font-weight: 600; - font-size: 17px; + font-size: 16px; margin: 5px 0; - color: $gl-gray-dark; + color: $gl-text-color; &.has-conflicts .fa-exclamation-triangle { color: $gl-warning; @@ -150,7 +124,7 @@ @media (max-width: $screen-xs-max) { h4 { - font-size: 15px; + font-size: 14px; } p { @@ -163,6 +137,11 @@ margin-bottom: 4px; } + .btn-grouped { + float: none; + margin-right: 0; + } + .accept-action { width: 100%; text-align: center; @@ -177,7 +156,7 @@ } .mr-widget-footer { - border-top: 1px solid #eee; + border-top: 1px solid $gray-darker; } .ci-coverage { @@ -211,7 +190,7 @@ } .label-branch { - color: $gl-gray-dark; + color: $gl-text-color; font-family: $monospace_font; font-weight: bold; overflow: hidden; @@ -331,10 +310,6 @@ left: 0; top: 2px; } - - .commit-row-info { - line-height: 20px; - } } .btn-clipboard { @@ -388,7 +363,7 @@ th { background-color: $white-light; - color: $gl-placeholder-color; + color: $gl-text-color-secondary; } } } @@ -404,7 +379,7 @@ } .mr-version-controls { - background: $background-color; + background: $gray-light; border-bottom: 1px solid $border-color; color: $gl-text-color; @@ -445,11 +420,19 @@ .merge-request-tabs-holder { background-color: $white-light; + .container-limited { + max-width: $limited-layout-width; + } + &.affix { top: 100px; left: 0; z-index: 10; transition: right .15s; + + @media (max-width: $screen-xs-max) { + right: 0; + } } &:not(.affix) .container-fluid { diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 13402acd8e1..686b64cdd24 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -11,6 +11,7 @@ } .progress { + width: 100%; height: 6px; } } @@ -24,13 +25,6 @@ } .issuable-row { - .color-label { - border-radius: 2px; - padding: 3px !important; - margin-right: 7px; - } - - // Issue title span a { color: $gl-text-color; word-wrap: break-word; @@ -39,15 +33,66 @@ } .milestone-summary { - margin-bottom: 25px; - .milestone-stat { + white-space: nowrap; margin-right: 10px; + + &.with-drilldown { + margin-right: 2px; + } } .remaining-days { color: $orange-light; } + + .milestone-stats-and-buttons { + display: flex; + justify-content: flex-start; + flex-wrap: wrap; + + @media (min-width: $screen-xs-min) { + justify-content: space-between; + flex-wrap: nowrap; + } + } + + .milestone-progress-buttons { + order: 1; + margin-top: 10px; + + @media (min-width: $screen-xs-min) { + order: 2; + margin-top: 0; + flex-shrink: 0; + } + + .btn { + float: left; + margin-right: $btn-side-margin; + + &:last-child { + margin-right: 0; + } + } + } + + .milestone-stats { + order: 2; + width: 100%; + padding: 7px 0; + flex-shrink: 1; + + @media (min-width: $screen-xs-min) { + // when displayed on one line stats go first, buttons second + order: 1; + } + } + + .progress { + width: 100%; + margin: 15px 0; + } } .issues-sortable-list, @@ -57,13 +102,17 @@ margin-top: 7px; .issuable-number { - color: $gl-placeholder-color; + color: $gl-text-color-secondary; margin-right: 5px; } .avatar { float: none; } + + > a:not(:last-of-type) { + margin-right: 5px; + } } } @@ -72,7 +121,7 @@ padding: 20px 0; } -@media (max-width: $screen-sm-min) { +@media (max-width: $screen-xs-max) { .milestone-actions { @include clearfix(); padding-top: $gl-vert-padding; @@ -82,3 +131,50 @@ } } } + +.milestone-page-header { + display: flex; + flex-flow: row; + align-items: center; + flex-wrap: wrap; + + .status-box { + margin-top: 0; + } + + .milestone-buttons { + margin-left: auto; + } + + .status-box { + order: 1; + } + + .milestone-buttons { + order: 2; + } + + .header-text-content { + order: 3; + width: 100%; + } + + .milestone-buttons .verbose { + display: none; + } + + @media (min-width: $screen-xs-min) { + .milestone-buttons .verbose { + display: inline; + } + + .header-text-content { + order: 2; + width: auto; + } + + .milestone-buttons { + order: 3; + } + } +} diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 16ddef481bd..f984b469609 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -27,6 +27,7 @@ .new-note, .note-edit-form { .note-form-actions { + position: relative; margin-top: $gl-padding; } @@ -44,7 +45,7 @@ .note-textarea { display: block; padding: 10px 0; - color: $gl-gray; + color: $gl-text-color; font-family: $regular_font; border: 0; @@ -62,7 +63,7 @@ .common-note-form { .md-area { padding: $gl-padding-top $gl-padding; - border: 1px solid $note-form-border-color; + border: 1px solid $border-color; border-radius: $border-radius-base; transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; @@ -109,9 +110,9 @@ margin: auto; margin-top: 0; text-align: center; - font-size: 13px; + font-size: 12px; - @media (max-width: $screen-md-min) { + @media (max-width: $screen-sm-max) { // On smaller devices the warning becomes the fourth item in the list, // rather than centering, and grows to span the full width of the // comment area. @@ -129,10 +130,10 @@ .note-edit-form { display: none; - font-size: 15px; + font-size: 14px; .md-area { - background-color: #fff; + background-color: $white-light; } } @@ -204,7 +205,7 @@ .comment-toolbar { padding-top: $gl-padding-top; - color: $note-toolbar-color; + color: $gl-text-color-secondary; border-top: 1px solid $border-color; } @@ -265,3 +266,18 @@ } } } + +.note-edit-warning.settings-message { + display: none; + padding: 5px 10px; + position: absolute; + left: 127px; + top: 2px; + + @media (max-width: $screen-xs-max) { + position: relative; + top: 0; + left: 0; + margin-bottom: 10px; + } +} diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 526e9ae5cdd..da0caa30c26 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -3,9 +3,9 @@ */ @-webkit-keyframes targe3-note { - from { background: #fffff0; } - 50% { background: #ffffd3; } - to { background: #fffff0; } + from { background: $note-targe3-outside; } + 50% { background: $note-targe3-inside; } + to { background: $note-targe3-outside; } } ul.notes { @@ -35,11 +35,114 @@ ul.notes { .system-note { font-size: 14px; - padding-top: 10px; - padding-bottom: 10px; - background: #fdfdfd; + padding: 0; + clear: both; + + &.timeline-entry::after { + clear: none; + } + + .system-note-message { + display: inline; + + &::first-letter { + text-transform: lowercase; + } + + a { + color: $gl-link-color; + text-decoration: none; + } + + p { + display: inline; + margin: 0; + + &::first-letter { + text-transform: lowercase; + } + } + } + + .timeline-content { + padding: 14px 10px; + } + + .note-body { + overflow: hidden; + + .system-note-commit-list-toggler { + display: none; + padding: 10px 0 0; + cursor: pointer; + position: relative; + z-index: 2; + + &:hover { + color: $gl-link-color; + text-decoration: underline; + } + } + + .note-text { + & p:first-child { + display: none; + } + + &.system-note-commit-list { + max-height: 70px; + overflow: hidden; + display: block; + + ul { + margin: 3px 0 3px 16px !important; + + .gfm-commit { + font-family: $monospace_font; + font-size: 12px; + } + } + + p:first-child { + display: none; + } + + p:last-child { + a { + color: $gl-text-color; + + &:hover { + color: $gl-link-color; + } + } + } + + &::after { + content: ''; + width: 100%; + height: 67px; + position: absolute; + left: 0; + bottom: 0; + background: linear-gradient(rgba($white-light, 0.1) -100px, $white-light 100%); + } + + &.hide-shade { + max-height: 100%; + overflow: auto; + + &::after { + display: none; + background: transparent; + } + } + } + } + } .timeline-icon { + display: none; + .avatar { visibility: hidden; @@ -63,7 +166,17 @@ ul.notes { .note { display: block; position: relative; - border-bottom: 1px solid $table-border-gray; + border-bottom: 1px solid $white-normal; + + &.note-discussion { + &.timeline-entry { + padding: 14px 10px; + } + + .system-note { + padding: 0; + } + } &.is-editting { .note-header, @@ -82,16 +195,14 @@ ul.notes { } .note-body { - overflow: auto; + overflow-x: auto; + overflow-y: hidden; .note-text { - overflow: auto; word-wrap: break-word; @include md-typography; - // Reset ul style types since we're nested inside a ul already @include bulleted-list; - ul.task-list { ul:not(.task-list) { padding-left: 1.3em; @@ -141,6 +252,22 @@ ul.notes { } } +.page-sidebar-pinned.right-sidebar-expanded { + @media (max-width: $screen-md-max) { + .note-header { + .note-headline-light { + display: block; + } + + .note-actions { + position: absolute; + right: 0; + top: 0; + } + } + } +} + // Diff code in discussion view .discussion-body .diff-file { .file-title { @@ -164,25 +291,25 @@ ul.notes { font-family: $regular_font; td { - border: 1px solid $table-border-gray; + border: 1px solid $white-normal; border-left: none; &.notes_line { vertical-align: middle; text-align: center; padding: 10px 0; - background: $background-color; + background: $gray-light; color: $text-color; } &.notes_line2 { text-align: center; padding: 10px 0; - border-left: 1px solid #ddd !important; + border-left: 1px solid $note-line2-border !important; } &.notes_content { - background-color: $background-color; + background-color: $gray-light; border-width: 1px 0; padding: 0; vertical-align: top; @@ -218,7 +345,19 @@ ul.notes { } .author_link { - color: $gl-gray; + color: $gl-text-color; + } +} + +.discussion-header { + font-size: 14px; +} + +.note-headline-light { + display: inline; + + @media (max-width: $screen-xs-min) { + display: block; } } @@ -241,7 +380,7 @@ ul.notes { .note-actions { float: right; margin-left: 10px; - color: $notes-action-color; + color: $gray-darkest; } .note-actions { @@ -252,10 +391,6 @@ ul.notes { .note-action-button { margin-left: 10px; } - - @media (min-width: $screen-sm-min) { - position: relative; - } } .discussion-actions { @@ -280,9 +415,8 @@ ul.notes { } .fa { - color: $notes-action-color; + color: $gray-darkest; position: relative; - top: 1px; font-size: 17px; } @@ -318,15 +452,10 @@ ul.notes { color: $notes-role-color; font-size: 12px; line-height: 20px; - border: 1px solid $notes-role-border-color; + border: 1px solid $border-color; border-radius: $border-radius-base; } -.diff-file .note .note-actions { - right: 0; - top: 0; -} - /** * Line note button on the side of diffs @@ -340,7 +469,7 @@ ul.notes { .add-diff-note { margin-top: -4px; border-radius: 40px; - background: #fff; + background: $white-light; padding: 4px; font-size: 16px; color: $gl-link-color; @@ -353,7 +482,7 @@ ul.notes { &:hover { background: $gl-info; - color: #fff; + color: $white-light; @include show-add-diff-note; } } @@ -386,7 +515,6 @@ ul.notes { .line-resolve-all-container { .btn-group { - margin-top: -1px; margin-left: -4px; } @@ -397,27 +525,24 @@ ul.notes { } .line-resolve-all { + vertical-align: middle; display: inline-block; - padding: 5px 10px; - background-color: $background-color; + padding: 6px 10px; + background-color: $gray-light; border: 1px solid $border-color; border-radius: $border-radius-default; &.has-next-btn { border-top-right-radius: 0; border-bottom-right-radius: 0; + border-right: 0; } .line-resolve-btn { - vertical-align: middle; margin-right: 5px; } } -.line-resolve-text { - vertical-align: middle; -} - .line-resolve-btn { display: inline-block; position: relative; @@ -436,18 +561,14 @@ ul.notes { &.is-active { color: $gl-text-green; - svg path { + svg { fill: $gl-text-green; } } svg { position: relative; - color: $notes-action-color; - - path { - fill: $notes-action-color; - } + fill: $gray-darkest; } } @@ -460,3 +581,17 @@ ul.notes { } } } + +// Merge request notes in diffs +.diff-file { + // Diff is side by side + .notes_content.parallel .note-header .note-headline-light { + display: block; + position: relative; + } + // Diff is inline + .notes_content .note-header .note-headline-light { + display: inline-block; + position: relative; + } +} diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss index 94fbbef3c77..bdf07a99daf 100644 --- a/app/assets/stylesheets/pages/notifications.scss +++ b/app/assets/stylesheets/pages/notifications.scss @@ -1,24 +1,16 @@ .notification-list-item { line-height: 34px; + + .dropdown-menu { + @extend .dropdown-menu-align-right; + } } .notification { position: relative; top: 1px; - > .fa { + .fa { font-size: 18px; } } - -.ns-part { - color: $gl-text-green; -} - -.ns-watch { - color: $gl-success; -} - -.ns-mute { - color: $gl-danger; -} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index bf3cb6e7ad9..8dff22e32bd 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -1,4 +1,9 @@ .pipelines { + .realtime-loading { + font-size: 40px; + text-align: center; + } + .stage { max-width: 90px; width: 90px; @@ -22,30 +27,44 @@ .table.ci-table { min-width: 1200px; + table-layout: fixed; + + .label { + margin-bottom: 3px; + } .pipeline-id { color: $black; } - .branch-commit { - width: 30%; + .pipeline-date, + .pipeline-status { + width: 10%; + } - .branch-name { - max-width: 195px; - } + .pipeline-info, + .pipeline-commit, + .pipeline-stages, + .pipeline-actions { + width: 20%; } } } -.content-list { - - &.pipelines, - &.builds-content-list { - width: 100%; - overflow: auto; +@media (max-width: $screen-md-max) { + .content-list { + &.pipelines, + &.builds-content-list { + width: 100%; + overflow: auto; + } } } +.content-list.pipelines .table-holder { + min-height: 300px; +} + .pipeline-holder { width: 100%; overflow: auto; @@ -74,6 +93,10 @@ td { padding: 10px 8px; } + + .commit-link { + padding: 9px 8px 10px; + } } tbody { @@ -91,14 +114,6 @@ } } - .ci-status { - - svg { - top: 1px; - margin-right: 0; - } - } - a:hover { text-decoration: none; } @@ -109,15 +124,11 @@ float: none; } - .api { - color: $code-color; - } - .branch-commit { .branch-name { font-weight: bold; - max-width: 150px; + max-width: 120px; overflow: hidden; display: inline-block; white-space: nowrap; @@ -129,7 +140,7 @@ height: 14px; width: 14px; vertical-align: middle; - fill: $table-text-gray; + fill: $gl-text-color-secondary; } .fa { @@ -143,7 +154,7 @@ .commit-title { margin-top: 4px; - max-width: 300px; + max-width: 225px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; @@ -174,13 +185,16 @@ .stage-cell { font-size: 0; + padding: 10px 4px; - svg { - height: 18px; - width: 18px; - position: relative; + > .stage-container > div > button > span > svg, + > .stage-container > button > svg { + height: 22px; + width: 22px; + position: absolute; + top: -1px; + left: -1px; z-index: 2; - vertical-align: middle; overflow: visible; } @@ -189,8 +203,8 @@ position: relative; margin-right: 6px; - .tooltip { - white-space: nowrap; + .tooltip-inner { + padding: 3px 4px; } &:not(:last-child) { @@ -198,21 +212,17 @@ content: ''; width: 8px; position: absolute; - right: -7px; - bottom: 8px; + right: -8px; + top: 10px; border-bottom: 2px solid $border-color; } } - - a { - display: block; - } } } .duration, .finished-at { - color: $table-text-gray; + color: $gl-text-color-secondary; margin: 4px 0; .fa { @@ -233,7 +243,7 @@ .btn { margin: 0; - color: $table-text-gray; + color: $gl-text-color-secondary; } .cancel-retry-btns { @@ -246,10 +256,10 @@ .dropdown-toggle, .dropdown-menu { - color: $table-text-gray; + color: $gl-text-color-secondary; .fa { - color: $table-text-gray; + color: $gl-text-color-secondary; font-size: 14px; } @@ -283,7 +293,7 @@ .build-link { a { - color: $gl-dark-link-color; + color: $gl-text-color; } } @@ -292,68 +302,170 @@ } } +.admin-builds-table { + .ci-table td:last-child { + min-width: 120px; + } +} + // Pipeline visualization +.pipeline-actions { + border-bottom: none; +} -.toggle-pipeline-btn { - background-color: $gray-dark; +.tab-pane { + &.pipelines { + .ci-table { + min-width: 900px; + } - &.graph-collapsed { - background-color: $white-light; + .content-list.pipelines { + overflow: auto; + } + + .stage { + max-width: 100px; + width: 100px; + } + + .pipeline-actions { + min-width: initial; + } + } + + &.builds { + .ci-table { + tr { + height: 71px; + } + } } } +// Pipeline graph .pipeline-graph { width: 100%; - overflow: auto; + background-color: $gray-light; + padding: $gl-padding; white-space: nowrap; transition: max-height 0.3s, padding 0.3s; + overflow: auto; - &.graph-collapsed { - max-height: 0; - padding: 0 16px; + .stage-column-list, + .builds-container > ul { + padding: 0; } -} -.pipeline-visualization { - position: relative; + a { + text-decoration: none; + color: $gl-text-color-secondary; + } - ul { - padding: 0; + svg { + vertical-align: middle; + margin-right: 3px; } -} -.stage-column { - display: inline-block; - vertical-align: top; + .stage-column { + display: inline-block; + vertical-align: top; - &:not(:last-child) { - margin-right: 44px; - } + &:not(:last-child) { + margin-right: 44px; + } - &.left-margin { - &:not(:first-child) { - margin-left: 44px; + &.left-margin { + &:not(:first-child) { + margin-left: 44px; - .left-connector { - &::before { - content: ''; - position: absolute; - top: 48%; - left: -48px; - border-top: 2px solid $border-color; - width: 48px; - height: 1px; + .left-connector { + &::before { + content: ''; + position: absolute; + top: 48%; + left: -48px; + border-top: 2px solid $border-color; + width: 48px; + height: 1px; + } } } } - } - &.no-margin { - margin: 0; - } + &.no-margin { + margin: 0; + } - li { - list-style: none; + li { + list-style: none; + } + + &:last-child { + .build { + // Remove right connecting horizontal line from first build in last stage + &:first-child { + &::after { + border: none; + } + } + // Remove right curved connectors from all builds in last stage + &:not(:first-child) { + &::after { + border: none; + } + } + // Remove opposite curve + .curve { + &::before { + display: none; + } + } + } + } + + &:first-child { + .build { + // Remove left curved connectors from all builds in first stage + &:not(:first-child) { + &::before { + border: none; + } + } + // Remove opposite curve + .curve { + &::after { + display: none; + } + } + } + } + + // Curve first child connecting lines in opposite direction + .curve { + display: none; + + &::before, + &::after { + content: ''; + width: 21px; + height: 25px; + position: absolute; + top: -31px; + border-top: 2px solid $border-color; + } + + &::after { + left: -44px; + border-right: 2px solid $border-color; + border-radius: 0 20px; + } + + &::before { + right: -44px; + border-left: 2px solid $border-color; + border-radius: 20px 0 0; + } + } } .stage-name { @@ -366,166 +478,114 @@ } .build { - border: 1px solid $border-color; position: relative; - padding: 7px 10px 8px; - border-radius: 30px; width: 186px; margin-bottom: 10px; + white-space: normal; + color: $gl-text-color-secondary; + + // Action Icons in big pipeline-graph nodes + > .ci-action-icon-container .ci-action-icon-wrapper { + i { + color: $border-color; + border-radius: 100%; + border: 1px solid $border-color; + padding: 5px 6px; + font-size: 13px; + background: $white-light; + height: 30px; + width: 30px; - &:hover { - background-color: $gray-lighter; - } - - &.playable { - - svg { - height: 13px; - width: 20px; - position: relative; - top: 1px; - - path { - fill: $layout-link-gray; - } - } - } - - .build-content { - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - width: 164px; - - .ci-status-icon { - svg { - height: 20px; - width: 20px; + &::before { + position: relative; + top: 3px; + left: 3px; } - } - - .tooltip { - white-space: nowrap; - .tooltip-inner { - overflow: hidden; - text-overflow: ellipsis; + &:hover { + color: $gl-text-color; + background-color: $stage-hover-bg; + border: 1px solid $stage-hover-bg; } } - .ci-status-text { - width: 135px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - vertical-align: middle; - display: inline-block; - position: relative; - top: -1px; - } - - a { - color: $gl-text-color-light; - text-decoration: none; + .ci-play-icon { + padding: 5px 5px 5px 7px; } + } - .dropdown-menu-toggle { - background-color: transparent; - border: none; - width: auto; - padding: 0; - color: $gl-text-color-light; - flex-grow: 1; - - .ci-status-text { - max-width: 112px; - width: auto; - } - } + > .ci-action-icon-container { + position: absolute; + right: 5px; + top: 5px; + } - .grouped-pipeline-dropdown { - padding: 0; - width: 186px; - left: auto; - right: -197px; - top: -9px; + .ci-status-icon svg { + height: 20px; + width: 20px; + } - ul { - max-height: 245px; - overflow: auto; + .dropdown-menu-toggle { + background-color: transparent; + border: none; + padding: 0; + color: $gl-text-color-secondary; - li:first-child { - padding-top: 8px; - } + &:focus { + outline: none; + } - li:last-child { - padding-bottom: 8px; - } - } + &:hover { + color: $gl-text-color; - a { + .dropdown-counter-badge { color: $gl-text-color; - padding: 7px 8px 8px; - - &:hover { - background-color: $blue-light-transparent; - border-radius: 3px; - - .ci-status-text { - text-decoration: none; - } - } - } - - svg { - width: 14px; - height: 14px; } + } + } - .ci-status-text { - width: 112px; - } + > .build-content { + display: inline-block; + padding: 8px 10px 9px; + width: 100%; + border: 1px solid $border-color; + border-radius: 30px; + background-color: $white-light; - .arrow { - &::before, - &::after { - content: ''; - display: inline-block; - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - top: 18px; - } + &:hover { + background-color: $stage-hover-bg; + border: 1px solid $stage-hover-border; + color: $gl-text-color; + } + } - &::before { - left: -5px; - margin-top: -6px; - border-width: 7px 5px 7px 0; - border-right-color: $border-color; - } - &::after { - left: -4px; - margin-top: -9px; - border-width: 10px 7px 10px 0; - border-right-color: $white-light; - } - } + .arrow { + &::before, + &::after { + content: ''; + display: inline-block; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + top: 18px; } - .badge { - background-color: $gray-darker; - color: $gl-text-color-light; - font-weight: normal; - margin-left: $btn-xs-side-margin; + &::before { + left: -5px; + margin-top: -6px; + border-width: 7px 5px 7px 0; + border-right-color: $border-color; } - } - svg { - vertical-align: middle; - margin-right: 5px; + &::after { + left: -4px; + margin-top: -9px; + border-width: 10px 7px 10px 0; + border-right-color: $white-light; + } } // Connect first build in each stage with right horizontal line @@ -581,121 +641,302 @@ } } } +} - &:last-child { - .build { - // Remove right connecting horizontal line from first build in last stage - &:first-child { - &::after { - border: none; - } - } - // Remove right curved connectors from all builds in last stage - &:not(:first-child) { - &::after { - border: none; - } - } - // Remove opposite curve - .curve { - &::before { - display: none; - } - } +// Triggers the dropdown in the big pipeline graph +.dropdown-counter-badge { + color: $border-color; + font-weight: 100; + font-size: 15px; + position: absolute; + right: 5px; + top: 8px; +} + +.ci-status-text { + max-width: 110px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: bottom; + display: inline-block; + position: relative; + font-weight: 200; +} + +// Dropdown button in mini pipeline graph +.mini-pipeline-graph-dropdown-toggle { + border-radius: 100px; + background-color: $white-light; + border-width: 1px; + border-style: solid; + width: 22px; + height: 22px; + margin: 0; + padding: 0; + transition: all 0.2s linear; + position: relative; + + > .fa.fa-caret-down { + position: absolute; + left: 20px; + top: 5px; + display: inline-block; + visibility: hidden; + opacity: 0; + color: inherit; + font-size: 12px; + transition: visibility 0.1s, opacity 0.1s linear; + } + + &:active, + &:focus, + &:hover { + outline: none; + width: 35px; + + .fa.fa-caret-down { + visibility: visible; + opacity: 1; } } - &:first-child { - .build { - // Remove left curved connectors from all builds in first stage - &:not(:first-child) { - &::before { - border: none; - } - } - // Remove opposite curve - .curve { - &::after { - display: none; - } - } + // Dropdown button animation in mini pipeline graph + &.ci-status-icon-success { + border-color: $gl-success; + color: $gl-success; + + &:hover, + &:focus, + &:active { + background-color: rgba($gl-success, 0.1); + border-color: $gl-success; } } - // Curve first child connecting lines in opposite direction - .curve { - display: none; + &.ci-status-icon-failed { + border-color: $gl-danger; + color: $gl-danger; - &::before, - &::after { - content: ''; - width: 21px; - height: 25px; - position: absolute; - top: -32px; - border-top: 2px solid $border-color; + &:hover, + &:focus, + &:active { + background-color: rgba($gl-danger, 0.1); + border-color: $gl-danger; } + } - &::after { - left: -44px; - border-right: 2px solid $border-color; - border-radius: 0 20px; + &.ci-status-icon-pending, + &.ci-status-icon-success_with_warnings { + border-color: $gl-warning; + color: $gl-warning; + + &:hover, + &:focus, + &:active { + background-color: rgba($gl-warning, 0.1); + border-color: $gl-warning; } + } - &::before { - right: -44px; - border-left: 2px solid $border-color; - border-radius: 20px 0 0; + &.ci-status-icon-running { + border-color: $blue-normal; + color: $blue-normal; + + &:hover, + &:focus, + &:active { + background-color: rgba($blue-normal, 0.1); + border-color: $blue-normal; } } -} -.pipeline-actions { - border-bottom: none; -} + &.ci-status-icon-canceled, + &.ci-status-icon-disabled, + &.ci-status-icon-not-found, + &.ci-status-icon-manual { + border-color: $gl-text-color; + color: $gl-text-color; -.toggle-pipeline-btn { + &:hover, + &:focus, + &:active { + background-color: rgba($gl-text-color, 0.1); + border-color: $gl-text-color; + } + } - .fa { - color: $dropdown-header-color; + &.ci-status-icon-created, + &.ci-status-icon-skipped { + border-color: $gray-darkest; + color: $gray-darkest; + + &:hover, + &:focus, + &:active { + background-color: rgba($gray-darkest, 0.1); + border-color: $gray-darkest; + } } } -.tab-pane { +// dropdown content for big and mini pipeline +.big-pipeline-graph-dropdown-menu, +.mini-pipeline-graph-dropdown-menu { + width: 195px; + max-width: 195px; - &.pipelines { + li { + padding: 2px 3px; + } - .ci-table { - min-width: 900px; + .scrollable-menu { + max-height: 245px; + overflow: auto; + } + + // Loading icon + .builds-dropdown-loading { + margin: 0 auto; + width: 20px; + } + + // Action icon on the right + a.ci-action-icon-wrapper { + color: $action-icon-color; + border: 1px solid $action-icon-color; + border-radius: 20px; + width: 22px; + height: 22px; + padding: 2px 0 0 5px; + cursor: pointer; + float: right; + margin: -26px 9px 0 0; + font-size: 12px; + background-color: $white-light; + + &:hover, + &:focus { + text-decoration: none; + color: $gl-text-color; + background-color: $stage-hover-bg; + border: 1px solid transparent; } + } - .content-list.pipelines { - overflow: auto; + // link to the build + .mini-pipeline-graph-dropdown-item { + padding: 3px 7px 4px; + clear: both; + font-weight: normal; + line-height: 1.428571429; + white-space: nowrap; + margin: 0 5px; + border-radius: 3px; + + // build name + .ci-build-text { + font-weight: 200; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + width: 90px; + color: $gl-text-color-secondary; + margin-left: 2px; + display: inline-block; + top: 1px; + vertical-align: text-bottom; + position: relative; } - .stage { - max-width: 100px; - width: 100px; + // status icon on the left + .ci-status-icon { + top: 3px; + position: relative; + + > svg { + overflow: visible; + width: 18px; + height: 18px; + } } - .pipeline-actions { - min-width: initial; + &:hover, + &:focus { + outline: none; + text-decoration: none; + color: $gl-text-color; + background-color: $stage-hover-bg; } } +} - &.builds { +// Dropdown in the big pipeline graph +.big-pipeline-graph-dropdown-menu { + width: 195px; + min-width: 195px; + left: auto; + right: -195px; + top: -4px; + box-shadow: 0 1px 5px $black-transparent; - .ci-table { - tr { - height: 71px; - } + .mini-pipeline-graph-dropdown-item { + .ci-status-icon { + top: -1px; } } } -.ci-status-icon-created { +/** + * Top arrow in the dropdown in the mini pipeline graph + */ +.mini-pipeline-graph-dropdown-menu { + .arrow-up { + &::before, + &::after { + content: ''; + display: inline-block; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + top: -6px; + left: 2px; + border-width: 0 5px 6px; + } - svg { - fill: $gray-darkest; + &::before { + border-width: 0 5px 5px; + border-bottom-color: $border-color; + } + + &::after { + margin-top: 1px; + border-bottom-color: $white-light; + } + } +} + +/** + * Terminal + */ +.terminal-icon { + margin-left: 3px; +} + +.terminal-container { + .content-block { + border-bottom: none; + } + + #terminal { + margin-top: 10px; + min-height: 450px; + box-sizing: border-box; + + > div { + min-height: 450px; + } } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 6fab97a71aa..722b3006f7c 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -60,8 +60,8 @@ .account-well { padding: 10px; - background-color: $help-well-bg; - border: 1px solid $help-well-border; + background-color: $gray-light; + border: 1px solid $border-color; border-radius: $border-radius-base; ul { @@ -136,7 +136,7 @@ .provider-btn-group { display: inline-block; margin-right: 10px; - border: 1px solid $provider-btn-group-border; + border: 1px solid $border-color; border-radius: 3px; &:last-child { @@ -147,7 +147,7 @@ .provider-btn-image { display: inline-block; padding: 5px 10px; - border-right: 1px solid $provider-btn-group-border; + border-right: 1px solid $border-color; > img { width: 20px; @@ -180,7 +180,7 @@ .modal-dialog { width: 380px; - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { width: auto; } @@ -198,7 +198,7 @@ } .personal-access-tokens-never-expires-label { - color: $personal-access-tokens-disabled-label-color; + color: $note-disabled-comment-color; } .datepicker.personal-access-tokens-expires-at .ui-state-disabled span { @@ -216,8 +216,8 @@ } } -.user-profile { +.user-profile { .cover-controls a { margin-left: 5px; } @@ -231,8 +231,11 @@ } } - @media (max-width: $screen-xs-max) { + .user-profile-nav { + font-size: 0; + } + @media (max-width: $screen-xs-max) { .cover-block { padding-top: 20px; } @@ -253,6 +256,12 @@ } } } + + .user-profile-nav { + a { + margin-right: 0; + } + } } } @@ -261,4 +270,14 @@ table.u2f-registrations { td:not(:last-child) { border-right: solid 1px transparent; } -}
\ No newline at end of file +} + +.oauth-application-show { + .scope-name { + font-weight: 600; + } + + .scopes-list { + padding-left: 18px; + } +} diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss index f8da0983b77..100ace41f2a 100644 --- a/app/assets/stylesheets/pages/profiles/preferences.scss +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -22,8 +22,8 @@ background: $theme-graphite; } - &.ui_gray { - background: $theme-gray; + &.ui_black { + background: $theme-black; } &.ui_green { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index f7d54564530..cd0839e58ea 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -6,33 +6,27 @@ } } -.no-ssh-key-message, -.project-limit-message { - background-color: #f28d35; - margin-bottom: 0; -} - .new_project, .edit-project { - fieldset { - - &.features { + .sharing-and-permissions { + .header { + padding-top: $gl-vert-padding; + } - .label-light { - margin-bottom: 0; - } + .label-light { + margin-bottom: 0; + } - .help-block { - margin-top: 0; - } + .help-block { + margin-top: 0; } .form-group { margin-bottom: 5px; } - &> .form-group { + & > .form-group { padding-left: 0; } } @@ -76,17 +70,18 @@ &.static-namespace { height: 35px; border-radius: 3px; - border: 1px solid #e5e5e5; + border: 1px solid $border-color; } - &+ .select2 a { + & + .select2 a { border-top-left-radius: 0; border-bottom-left-radius: 0; } } } -.project-home-panel { +.project-home-panel, +.group-home-panel { padding-top: 24px; padding-bottom: 24px; @@ -94,17 +89,18 @@ border-bottom: 1px solid $border-color; } - .project-avatar { + .project-avatar, + .group-avatar { float: none; margin: 0 auto; - border: none; &.identicon { border-radius: 50%; } } - .project-title { + .project-title, + .group-title { margin-top: 10px; margin-bottom: 10px; font-size: 24px; @@ -118,10 +114,11 @@ } } - .project-home-desc { + .project-home-desc, + .group-home-desc { margin-left: auto; margin-right: auto; - margin-bottom: 15px; + margin-bottom: 0; max-width: 700px; > p { @@ -141,13 +138,20 @@ } } -.project-repo-buttons { - font-size: 0; +.nav > .project-repo-buttons { + margin-top: 0; +} +.project-repo-buttons, +.group-buttons { .btn { @include btn-gray; padding: 3px 10px; + &:last-child { + margin-left: 0; + } + .fa { color: $layout-link-gray; } @@ -168,22 +172,33 @@ } } - .project-repo-btn-group, - .notification-dropdown, - .project-dropdown { - margin-left: 10px; + .project-action-button { + margin: 15px 5px 0; + vertical-align: top; + } + + .notification-dropdown .dropdown-menu { + @extend .dropdown-menu-align-right; + } + + .download-button { + @media (max-width: $screen-md-max) { + margin-left: 0; + } } .count-buttons { display: inline-block; vertical-align: top; + margin-top: 15px; } .project-clone-holder { display: inline-block; + margin: 15px 5px 0 0; input { - height: 29px; + height: 27px; } } @@ -205,7 +220,7 @@ left: 0; margin-top: -6px; border-width: 7px 5px 7px 0; - border-right-color: #dce0e5; + border-right-color: $count-arrow-border; pointer-events: none; } @@ -220,7 +235,7 @@ left: 1px; margin-top: -9px; border-width: 10px 7px 10px 0; - border-right-color: #fff; + border-right-color: $white-light; pointer-events: none; } } @@ -228,7 +243,7 @@ .count { @include btn-gray; display: inline-block; - background: white; + background: $white-light; border-radius: 2px; border-width: 1px; border-style: solid; @@ -237,7 +252,7 @@ line-height: 13px; padding: $gl-vert-padding $gl-padding; letter-spacing: .4px; - padding: 7px 14px; + padding: 6px 14px; text-align: center; vertical-align: middle; touch-action: manipulation; @@ -250,7 +265,7 @@ } &:hover { - background: #fff; + background: $white-light; } } } @@ -277,12 +292,12 @@ .option-title { font-weight: normal; display: inline-block; - color: $gl-gray-dark; + color: $gl-text-color; } .option-descr { margin-left: 29px; - color: #54565b; + color: $project-option-descr-color; } } } @@ -290,7 +305,7 @@ .save-project-loader { margin-top: 50px; margin-bottom: 50px; - color: #555; + color: $save-project-loader-color; } .transfer-project .select2-container { @@ -316,7 +331,7 @@ a.deploy-project-label { padding: 5px; margin-right: 5px; - color: $gl-gray; + color: $gl-text-color; background-color: $row-hover; &:hover { @@ -353,11 +368,11 @@ a.deploy-project-label { > li + li::before { padding: 0 3px; - color: #999; + color: $project-breadcrumb-color; } a { - color: $gl-dark-link-color; + color: $gl-text-color; } .dropdown-menu { @@ -396,13 +411,13 @@ a.deploy-project-label { width: 100px; height: 100px; background-color: $gray-light; - border: 1px solid $gray-dark; + border: 1px solid $white-normal; margin: 0 auto; border-radius: 50%; i { font-size: 100px; - color: $gray-dark; + color: $white-normal; } } @@ -411,7 +426,7 @@ a.deploy-project-label { width: 100%; height: 100%; padding-top: $gl-padding; - color: $gl-gray; + color: $gl-text-color; .caption { min-height: 30px; @@ -458,8 +473,23 @@ a.deploy-project-label { } } +.page-sidebar-pinned { + .project-stats .nav > li.right { + @media (min-width: $screen-lg-min) { + float: none; + } + } + + .download-button { + @media (min-width: $screen-lg-min) { + margin-left: 0; + } + } +} + .project-stats { font-size: 0; + text-align: center; border-bottom: 1px solid $border-color; .nav { @@ -474,12 +504,12 @@ a.deploy-project-label { margin-right: $gl-padding; } - &.project-repo-buttons-right { - margin-top: 10px; + &.right { + vertical-align: top; + margin-top: 0; - @media (min-width: $screen-md-min) { + @media (min-width: $screen-lg-min) { float: right; - margin-top: 0; } } } @@ -487,7 +517,7 @@ a.deploy-project-label { .nav > li > a { padding: 0; background-color: transparent; - font-size: 15px; + font-size: 14px; line-height: 29px; color: $notes-light-color; @@ -498,7 +528,7 @@ a.deploy-project-label { } li.missing { - border: 1px dashed $border-gray-light; + border: 1px dashed $border-gray-normal; border-radius: $border-radius-default; a { @@ -515,20 +545,20 @@ a.deploy-project-label { } pre.light-well { - border-color: #f1f1f1; + border-color: $well-light-border; } .git-empty { margin: 0 7px 7px; h5 { - color: #5c5d5e; + color: $gl-text-color; } .light-well { border-radius: 2px; - color: #5b6169; + color: $well-light-text-color; font-size: 13px; line-height: 1.6em; } @@ -553,15 +583,25 @@ pre.light-well { @include basic-list; .project-row { - border-color: $table-border-color; + border-color: $white-normal; .project-full-name { @include str-truncated; + + @media (max-width: $screen-xs-max) { + max-width: 50%; + } } .controls { line-height: $list-text-height; + .badge { + @media (max-width: $screen-xs-max) { + display: none; + } + } + a:hover { text-decoration: none; } @@ -575,6 +615,12 @@ pre.light-well { top: 2px; } } + + .description p { + @media (max-width: $screen-xs-max) { + max-width: 50%; + } + } } .bottom { @@ -588,7 +634,6 @@ pre.light-well { margin: 0; } - .activity-filter-block { .controls { padding-bottom: 7px; @@ -597,6 +642,12 @@ pre.light-well { } } +.commits-search-form { + .input-short { + min-width: 200px; + } +} + .project-last-commit { @media (min-width: $screen-sm-min) { margin-top: $gl-padding; @@ -605,7 +656,7 @@ pre.light-well { &.container-fluid { padding-top: 12px; padding-bottom: 12px; - background-color: $background-color; + background-color: $gray-light; border: 1px solid $border-color; border-right-width: 0; border-left-width: 0; @@ -627,7 +678,7 @@ pre.light-well { } .commit-row-message { - color: $gl-gray; + color: $gl-text-color; } .commit_short_id { @@ -682,7 +733,7 @@ pre.light-well { .form-control { @extend .monospace; - background: #fff; + background: $white-light; font-size: 14px; margin-left: -1px; cursor: auto; @@ -692,17 +743,17 @@ pre.light-well { .cannot-be-merged, .cannot-be-merged:hover { - color: #e62958; + color: $error-exclamation-point; margin-top: 2px; } .private-forks-notice .private-fork-icon { i:nth-child(1) { - color: #2aa056; + color: $project-private-forks-notice-odd; } i:nth-child(2) { - color: #fff; + color: $white-light; } } @@ -715,7 +766,7 @@ pre.light-well { .protected-branches-list { a { - color: $gl-gray; + color: $gl-text-color; &:hover { color: $gl-link-color; @@ -775,7 +826,31 @@ pre.light-well { .compare-form-group { .dropdown-menu { - width: 300px; + width: 100%; + + @media (min-width: $screen-sm-min) { + width: 300px; + } + } + + + .compare-ellipsis { + width: 100%; + vertical-align: middle; + text-align: center; + margin-top: -20px; + + @media (min-width: $screen-sm-min) { + margin-top: 0; + width: auto; + } + } + + .inline-input-group { + width: 100%; + + @media (min-width: $screen-sm-min) { + width: 250px; + } } } @@ -830,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 { @@ -842,3 +925,31 @@ pre.light-well { pointer-events: none; } } + +.variables-table { + table-layout: fixed; + + .variable-key { + width: 30%; + } +} + +.services-installation-info .row { + margin-bottom: 10px; +} + +.service-installation { + padding: 32px; + margin: 32px; + border-radius: 3px; + background-color: $white-light; + + h3 { + margin-top: 0; + } + + hr { + margin: 32px 0; + border-color: $border-color; + } +} diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss index 7b3878c91df..9b6ff237557 100644 --- a/app/assets/stylesheets/pages/runners.scss +++ b/app/assets/stylesheets/pages/runners.scss @@ -1,27 +1,27 @@ .runner-state { padding: 6px 12px; margin-right: 10px; - color: #fff; + color: $white-light; &.runner-state-shared { - background: #32b186; + background: $runner-state-shared-bg; } &.runner-state-specific { - background: #3498db; + background: $runner-state-specific-bg; } } .runner-status-online { - color: green; + color: $runner-status-online-color; } .runner-status-offline { - color: gray; + color: $runner-status-offline-color; } .runner-status-paused { - color: red; + color: $runner-status-paused-color; } .runner { diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index b4761df3f23..88ea92c5afb 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -8,6 +8,23 @@ border-bottom: none; } } + + .blob-result { + margin: 5px 0; + } +} + +.search form:hover, +.file-finder-input:hover, +.issuable-search-form:hover, +.search-text-input:hover, +.form-control:hover { + border-color: lighten($dropdown-input-focus-border, 20%); + box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%); +} + +input[type="checkbox"]:hover { + box-shadow: 0 0 2px 2px lighten($search-input-focus-shadow-color, 20%), 0 0 0 1px lighten($search-input-focus-shadow-color, 20%); } .search { @@ -21,6 +38,11 @@ padding: 4px; width: $search-input-width; line-height: 24px; + + &:hover { + border-color: lighten($dropdown-input-focus-border, 20%); + box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%); + } } .location-text { @@ -28,10 +50,9 @@ } .search-input { - padding-right: 20px; border: none; font-size: 14px; - padding: 0; + padding: 0 20px 0 0; margin-left: 5px; line-height: 25px; width: 98%; @@ -43,9 +64,9 @@ border-radius: $border-radius-default; font-size: 14px; font-style: normal; - color: $location-badge-color; + color: $note-disabled-comment-color; display: inline-block; - background-color: $location-badge-bg; + background-color: $gray-normal; vertical-align: top; cursor: default; } @@ -132,7 +153,7 @@ .search-input-wrap { i { - color: $location-icon-active-color; + color: $layout-link-gray; } } } diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 2e8f356298d..a28a87ed4f8 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -1,5 +1,5 @@ .settings-list-icon { - color: $gl-placeholder-color; + color: $gl-text-color-secondary; font-size: $settings-icon-size; line-height: 42px; } @@ -20,3 +20,7 @@ .danger-title { color: $gl-danger; } + +.service-settings .control-label { + padding-top: 0; +} diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss index 69288b31cc4..dfa4d033fb8 100644 --- a/app/assets/stylesheets/pages/stat_graph.scss +++ b/app/assets/stylesheets/pages/stat_graph.scss @@ -1,16 +1,16 @@ .tint-box { - background: #f3f3f3; + background: $stat-graph-common-bg; position: relative; margin-bottom: 10px; } .area { - fill: #1db34f; + fill: $stat-graph-area-fill; fill-opacity: 0.5; } .axis { - fill: #aaa; + fill: $stat-graph-axis-fill; font-size: 10px; } @@ -37,26 +37,26 @@ @include make-md-column(6); margin-top: 10px; - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { width: 100%; } } .person .spark { display: block; - background: #f3f3f3; + background: $stat-graph-common-bg; width: 100%; } .person .area-contributor { - fill: #f17f49; + fill: $stat-graph-orange-fill; } } .selection rect { - fill: #333; + fill: $stat-graph-selection-fill; fill-opacity: 0.1; - stroke: #333; + stroke: $stat-graph-selection-stroke; stroke-width: 1px; stroke-opacity: 0.4; shape-rendering: crispedges; diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 92997eae8b9..6f31d4ed789 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -1,8 +1,8 @@ .container-fluid { .ci-status { - padding: 2px 7px; + padding: 2px 7px 4px; margin-right: 10px; - border: 1px solid #eee; + border: 1px solid $gray-darker; white-space: nowrap; border-radius: 4px; @@ -11,80 +11,121 @@ text-decoration: none; } - &.ci-failed { + svg { + height: 13px; + width: 13px; + position: relative; + top: 2px; + overflow: visible; + } + + &.ci-failed, + &.ci-failed_with_warnings { color: $gl-danger; border-color: $gl-danger; + + &:not(span):hover { + background-color: rgba($gl-danger, .07); + } + + svg { + fill: $gl-danger; + } } &.ci-success, &.ci-success_with_warnings { color: $gl-success; border-color: $gl-success; + + &:not(span):hover { + background-color: rgba($gl-success, .07); + } + + svg { + fill: $gl-success; + } } &.ci-info { color: $gl-info; border-color: $gl-info; + + &:not(span):hover { + background-color: rgba($gl-info, .07); + } + + svg { + fill: $gl-info; + } } &.ci-canceled, - &.ci-skipped, &.ci-disabled { - color: $gl-gray; - border-color: $gl-gray; + color: $gl-text-color; + border-color: $gl-text-color; + + &:not(span):hover { + background-color: rgba($gl-text-color, .07); + } + + svg { + fill: $gl-text-color; + } } &.ci-pending { color: $gl-warning; border-color: $gl-warning; + + &:not(span):hover { + background-color: rgba($gl-warning, .07); + } + + svg { + fill: $gl-warning; + } } &.ci-running { color: $blue-normal; border-color: $blue-normal; - } - &.ci-created { - color: $table-text-gray; - border-color: $table-text-gray; + &:not(span):hover { + background-color: rgba($blue-normal, .07); + } svg { - fill: $table-text-gray; + fill: $blue-normal; } } - svg { - height: 13px; - width: 13px; - position: relative; - top: 1px; - margin: 0 3px; - overflow: visible; - } - } + &.ci-created, + &.ci-skipped { + color: $gl-text-color-secondary; + border-color: $gl-text-color-secondary; - .ci-status-icon-success { - color: $gl-success; - } + &:not(span):hover { + background-color: rgba($gl-text-color-secondary, .07); + } - .ci-status-icon-failed { - color: $gl-danger; - } + svg { + fill: $gl-text-color-secondary; + } + } - .ci-status-icon-pending, - .ci-status-icon-success_with_warning { - color: $gl-warning; - } + &.ci-manual { + color: $gl-text-color; + border-color: $gl-text-color; - .ci-status-icon-running { - color: $blue-normal; - } + &:not(span):hover { + background-color: rgba($gl-text-color, .07); + } - .ci-status-icon-canceled, - .ci-status-icon-disabled, - .ci-status-icon-not-found, - .ci-status-icon-skipped { - color: $gl-gray; + svg { + fill: $gl-text-color; + } + } } } @@ -95,3 +136,9 @@ left: 5px; } } + +.ci-status-link { + svg { + overflow: visible; + } +} diff --git a/app/assets/stylesheets/pages/tags.scss b/app/assets/stylesheets/pages/tags.scss deleted file mode 100644 index 24ebd3e7cfa..00000000000 --- a/app/assets/stylesheets/pages/tags.scss +++ /dev/null @@ -1,7 +0,0 @@ -.tag-buttons { - line-height: 40px; - - .btn:not(.dropdown-toggle) { - margin-left: 10px; - } -} diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index b3aef2fdd32..01675acc62e 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -11,7 +11,7 @@ background: $todo-alert-blue; margin-left: -17px; font-size: 11px; - color: white; + color: $white-light; padding: 3px; padding-top: 1px; padding-bottom: 1px; @@ -81,7 +81,7 @@ word-wrap: break-word; .md { - color: #7f8fa4; + color: $gl-grayish-blue; font-size: $gl-font-size; .label { @@ -90,7 +90,7 @@ } p { - color: #5c5d5e; + color: $gl-text-color; } } @@ -102,7 +102,7 @@ border: none; background: $gray-light; border-radius: 0; - color: #777; + color: $todo-body-pre-color; margin: 0 20px; overflow: hidden; } @@ -146,7 +146,7 @@ .todo-body { margin: 0; - border-left: 2px solid #ddd; + border-left: 2px solid $todo-body-border; padding-left: 10px; } } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 2b836fa1f4a..4cce1c363eb 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -14,14 +14,15 @@ .add-to-tree { vertical-align: top; + padding: 6px 10px; } .tree-table { margin-bottom: 0; tr { - border-bottom: 1px solid $table-border-gray; - border-top: 1px solid $table-border-gray; + border-bottom: 1px solid $white-normal; + border-top: 1px solid $white-normal; td, th { @@ -31,7 +32,7 @@ .last-commit { @include str-truncated(506px); - @media (min-width: $screen-sm-max) and (max-width: $screen-md-max) { + @media (min-width: $screen-md-min) and (max-width: $screen-md-max) { @include str-truncated(450px); } @@ -39,7 +40,7 @@ .commit-history-link-spacer { margin: 0 10px; - color: $table-border-color; + color: $white-normal; } &:hover { @@ -53,7 +54,7 @@ &.selected { td { - background: $gray-dark; + background: $white-normal; border-top: 1px solid $border-gray-dark; border-bottom: 1px solid $border-gray-dark; } @@ -77,7 +78,7 @@ i, a { - color: $gl-dark-link-color; + color: $gl-text-color; } img { @@ -103,21 +104,21 @@ padding-right: 8px; .commit-author-name { - color: $gl-gray; + color: $gl-text-color; } } .tree-time-ago { min-width: 135px; - color: $gl-gray-light; + color: $gl-text-color-secondary; } .tree-commit { max-width: 320px; - color: $gl-gray-light; + color: $gl-text-color-secondary; .tree-commit-link { - color: $gl-gray-light; + color: $gl-text-color-secondary; &:hover { text-decoration: underline; @@ -133,21 +134,18 @@ .blob-commit-info { list-style: none; - padding: $gl-padding; - background: $background-color; + background: $gray-light; + padding: 6px 0; border: 1px solid $border-color; border-bottom: none; margin: 0; - .commit { - padding-top: 0; - padding-bottom: 0; + .table-list-cell { + border-bottom: none; + } - .commit-row-title { - .commit-row-message { - font-weight: normal; - } - } + .commit-actions { + width: 200px; } } @@ -172,7 +170,7 @@ position: relative; z-index: 2; - .download-button { + .project-action-button { margin-left: $btn-side-margin; } } diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss index e73cecc92be..8c87bc3cafd 100644 --- a/app/assets/stylesheets/pages/ui_dev_kit.scss +++ b/app/assets/stylesheets/pages/ui_dev_kit.scss @@ -7,11 +7,11 @@ .example { &::before { content: "Example"; - color: #bbb; + color: $ui-dev-kit-example-color; } padding: 15px; - border: 1px dashed #ddd; + border: 1px dashed $ui-dev-kit-example-border; margin-bottom: 15px; } } diff --git a/app/assets/stylesheets/pages/votes.scss b/app/assets/stylesheets/pages/votes.scss deleted file mode 100644 index dc9a7d71e8b..00000000000 --- a/app/assets/stylesheets/pages/votes.scss +++ /dev/null @@ -1,4 +0,0 @@ -.votes-inline { - display: inline-block; - margin: 0 8px; -} diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index dfaeba41cf6..d5783e14b21 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -4,3 +4,128 @@ margin-right: auto; padding-right: 7px; } + +.wiki-page-header { + @extend .top-area; + position: relative; + + .wiki-page-title { + margin: 0; + font-size: 22px; + } + + .wiki-last-edit-by { + color: $gl-text-color-secondary; + + strong { + color: $gl-text-color; + } + } + + .light { + font-weight: normal; + color: $gl-text-color-secondary; + } + + .git-access-header { + padding: 16px 40px 11px 0; + line-height: 28px; + font-size: 18px; + } + + .git-clone-holder { + width: 100%; + padding-bottom: 40px; + } + + button.sidebar-toggle { + position: absolute; + right: 0; + top: 11px; + display: block; + } + + @media (min-width: $screen-sm-min) { + &.has-sidebar-toggle { + padding-right: 40px; + } + + .git-clone-holder { + width: 480px; + } + + .nav-controls { + width: auto; + min-width: 50%; + white-space: nowrap; + } + } + + @media (min-width: $screen-md-min) { + &.has-sidebar-toggle { + padding-right: 0; + } + + button.sidebar-toggle { + display: none; + } + } +} + +.wiki-git-access { + margin: $gl-padding 0; + + h3 { + font-size: 22px; + font-weight: normal; + margin-top: 1.4em; + } +} + +.right-sidebar.wiki-sidebar { + padding: $gl-padding 0; + + &.right-sidebar-collapsed { + display: none; + } + + .blocks-container { + padding: 0 $gl-padding; + } + + .block { + width: 100%; + } + + a { + color: $layout-link-gray; + + &:hover, + &.active { + color: $black; + } + } + + .active > a { + color: $black; + } + + ul.wiki-pages, + ul.wiki-pages li { + list-style: none; + padding: 0; + margin: 0; + } + + ul.wiki-pages li { + margin: 5px 0 10px; + } + + .wiki-sidebar-header { + padding: 0 $gl-padding $gl-padding; + + .gutter-toggle { + margin-top: 0; + } + } +} diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss index 3fa7fa3d7e3..b085c56390d 100644 --- a/app/assets/stylesheets/pages/xterm.scss +++ b/app/assets/stylesheets/pages/xterm.scss @@ -18,7 +18,267 @@ $l-blue: #81a2be; $l-magenta: #b294bb; $l-cyan: #8abeb7; - $l-white: $ci-text-color; + $l-white: $gray-darkest; + + /* + * xterm colors + */ + $xterm-fg-0: $black; + $xterm-fg-1: #800000; + $xterm-fg-2: #008000; + $xterm-fg-3: #808000; + $xterm-fg-4: #000080; + $xterm-fg-5: #800080; + $xterm-fg-6: #008080; + $xterm-fg-7: #c0c0c0; + $xterm-fg-8: #808080; + $xterm-fg-9: #f00; + $xterm-fg-10: #0f0; + $xterm-fg-11: #ff0; + $xterm-fg-12: #00f; + $xterm-fg-13: #f0f; + $xterm-fg-14: #0ff; + $xterm-fg-15: $white-light; + $xterm-fg-16: $black; + $xterm-fg-17: #00005f; + $xterm-fg-18: #000087; + $xterm-fg-19: #0000af; + $xterm-fg-20: #0000d7; + $xterm-fg-21: #00f; + $xterm-fg-22: #005f00; + $xterm-fg-23: #005f5f; + $xterm-fg-24: #005f87; + $xterm-fg-25: #005faf; + $xterm-fg-26: #005fd7; + $xterm-fg-27: #005fff; + $xterm-fg-28: #008700; + $xterm-fg-29: #00875f; + $xterm-fg-30: #008787; + $xterm-fg-31: #0087af; + $xterm-fg-32: #0087d7; + $xterm-fg-33: #0087ff; + $xterm-fg-34: #00af00; + $xterm-fg-35: #00af5f; + $xterm-fg-36: #00af87; + $xterm-fg-37: #00afaf; + $xterm-fg-38: #00afd7; + $xterm-fg-39: #00afff; + $xterm-fg-40: #00d700; + $xterm-fg-41: #00d75f; + $xterm-fg-42: #00d787; + $xterm-fg-43: #00d7af; + $xterm-fg-44: #00d7d7; + $xterm-fg-45: #00d7ff; + $xterm-fg-46: #0f0; + $xterm-fg-47: #00ff5f; + $xterm-fg-48: #00ff87; + $xterm-fg-49: #00ffaf; + $xterm-fg-50: #00ffd7; + $xterm-fg-51: #0ff; + $xterm-fg-52: #5f0000; + $xterm-fg-53: #5f005f; + $xterm-fg-54: #5f0087; + $xterm-fg-55: #5f00af; + $xterm-fg-56: #5f00d7; + $xterm-fg-57: #5f00ff; + $xterm-fg-58: #5f5f00; + $xterm-fg-59: #5f5f5f; + $xterm-fg-60: #5f5f87; + $xterm-fg-61: #5f5faf; + $xterm-fg-62: #5f5fd7; + $xterm-fg-63: #5f5fff; + $xterm-fg-64: #5f8700; + $xterm-fg-65: #5f875f; + $xterm-fg-66: #5f8787; + $xterm-fg-67: #5f87af; + $xterm-fg-68: #5f87d7; + $xterm-fg-69: #5f87ff; + $xterm-fg-70: #5faf00; + $xterm-fg-71: #5faf5f; + $xterm-fg-72: #5faf87; + $xterm-fg-73: #5fafaf; + $xterm-fg-74: #5fafd7; + $xterm-fg-75: #5fafff; + $xterm-fg-76: #5fd700; + $xterm-fg-77: #5fd75f; + $xterm-fg-78: #5fd787; + $xterm-fg-79: #5fd7af; + $xterm-fg-80: #5fd7d7; + $xterm-fg-81: #5fd7ff; + $xterm-fg-82: #5fff00; + $xterm-fg-83: #5fff5f; + $xterm-fg-84: #5fff87; + $xterm-fg-85: #5fffaf; + $xterm-fg-86: #5fffd7; + $xterm-fg-87: #5fffff; + $xterm-fg-88: #870000; + $xterm-fg-89: #87005f; + $xterm-fg-90: #870087; + $xterm-fg-91: #8700af; + $xterm-fg-92: #8700d7; + $xterm-fg-93: #8700ff; + $xterm-fg-94: #875f00; + $xterm-fg-95: #875f5f; + $xterm-fg-96: #875f87; + $xterm-fg-97: #875faf; + $xterm-fg-98: #875fd7; + $xterm-fg-99: #875fff; + $xterm-fg-100: #878700; + $xterm-fg-101: #87875f; + $xterm-fg-102: #878787; + $xterm-fg-103: #8787af; + $xterm-fg-104: #8787d7; + $xterm-fg-105: #8787ff; + $xterm-fg-106: #87af00; + $xterm-fg-107: #87af5f; + $xterm-fg-108: #87af87; + $xterm-fg-109: #87afaf; + $xterm-fg-110: #87afd7; + $xterm-fg-111: #87afff; + $xterm-fg-112: #87d700; + $xterm-fg-113: #87d75f; + $xterm-fg-114: #87d787; + $xterm-fg-115: #87d7af; + $xterm-fg-116: #87d7d7; + $xterm-fg-117: #87d7ff; + $xterm-fg-118: #87ff00; + $xterm-fg-119: #87ff5f; + $xterm-fg-120: #87ff87; + $xterm-fg-121: #87ffaf; + $xterm-fg-122: #87ffd7; + $xterm-fg-123: #87ffff; + $xterm-fg-124: #af0000; + $xterm-fg-125: #af005f; + $xterm-fg-126: #af0087; + $xterm-fg-127: #af00af; + $xterm-fg-128: #af00d7; + $xterm-fg-129: #af00ff; + $xterm-fg-130: #af5f00; + $xterm-fg-131: #af5f5f; + $xterm-fg-132: #af5f87; + $xterm-fg-133: #af5faf; + $xterm-fg-134: #af5fd7; + $xterm-fg-135: #af5fff; + $xterm-fg-136: #af8700; + $xterm-fg-137: #af875f; + $xterm-fg-138: #af8787; + $xterm-fg-139: #af87af; + $xterm-fg-140: #af87d7; + $xterm-fg-141: #af87ff; + $xterm-fg-142: #afaf00; + $xterm-fg-143: #afaf5f; + $xterm-fg-144: #afaf87; + $xterm-fg-145: #afafaf; + $xterm-fg-146: #afafd7; + $xterm-fg-147: #afafff; + $xterm-fg-148: #afd700; + $xterm-fg-149: #afd75f; + $xterm-fg-150: #afd787; + $xterm-fg-151: #afd7af; + $xterm-fg-152: #afd7d7; + $xterm-fg-153: #afd7ff; + $xterm-fg-154: #afff00; + $xterm-fg-155: #afff5f; + $xterm-fg-156: #afff87; + $xterm-fg-157: #afffaf; + $xterm-fg-158: #afffd7; + $xterm-fg-159: #afffff; + $xterm-fg-160: #d70000; + $xterm-fg-161: #d7005f; + $xterm-fg-162: #d70087; + $xterm-fg-163: #d700af; + $xterm-fg-164: #d700d7; + $xterm-fg-165: #d700ff; + $xterm-fg-166: #d75f00; + $xterm-fg-167: #d75f5f; + $xterm-fg-168: #d75f87; + $xterm-fg-169: #d75faf; + $xterm-fg-170: #d75fd7; + $xterm-fg-171: #d75fff; + $xterm-fg-172: #d78700; + $xterm-fg-173: #d7875f; + $xterm-fg-174: #d78787; + $xterm-fg-175: #d787af; + $xterm-fg-176: #d787d7; + $xterm-fg-177: #d787ff; + $xterm-fg-178: #d7af00; + $xterm-fg-179: #d7af5f; + $xterm-fg-180: #d7af87; + $xterm-fg-181: #d7afaf; + $xterm-fg-182: #d7afd7; + $xterm-fg-183: #d7afff; + $xterm-fg-184: #d7d700; + $xterm-fg-185: #d7d75f; + $xterm-fg-186: #d7d787; + $xterm-fg-187: #d7d7af; + $xterm-fg-188: #d7d7d7; + $xterm-fg-189: #d7d7ff; + $xterm-fg-190: #d7ff00; + $xterm-fg-191: #d7ff5f; + $xterm-fg-192: #d7ff87; + $xterm-fg-193: #d7ffaf; + $xterm-fg-194: #d7ffd7; + $xterm-fg-195: #d7ffff; + $xterm-fg-196: #f00; + $xterm-fg-197: #ff005f; + $xterm-fg-198: #ff0087; + $xterm-fg-199: #ff00af; + $xterm-fg-200: #ff00d7; + $xterm-fg-201: #f0f; + $xterm-fg-202: #ff5f00; + $xterm-fg-203: #ff5f5f; + $xterm-fg-204: #ff5f87; + $xterm-fg-205: #ff5faf; + $xterm-fg-206: #ff5fd7; + $xterm-fg-207: #ff5fff; + $xterm-fg-208: #ff8700; + $xterm-fg-209: #ff875f; + $xterm-fg-210: #ff8787; + $xterm-fg-211: #ff87af; + $xterm-fg-212: #ff87d7; + $xterm-fg-213: #ff87ff; + $xterm-fg-214: #ffaf00; + $xterm-fg-215: #ffaf5f; + $xterm-fg-216: #ffaf87; + $xterm-fg-217: #ffafaf; + $xterm-fg-218: #ffafd7; + $xterm-fg-219: #ffafff; + $xterm-fg-220: #ffd700; + $xterm-fg-221: #ffd75f; + $xterm-fg-222: #ffd787; + $xterm-fg-223: #ffd7af; + $xterm-fg-224: #ffd7d7; + $xterm-fg-225: #ffd7ff; + $xterm-fg-226: #ff0; + $xterm-fg-227: #ffff5f; + $xterm-fg-228: #ffff87; + $xterm-fg-229: #ffffaf; + $xterm-fg-230: #ffffd7; + $xterm-fg-231: $white-light; + $xterm-fg-232: #080808; + $xterm-fg-233: #121212; + $xterm-fg-234: #1c1c1c; + $xterm-fg-235: #262626; + $xterm-fg-236: #303030; + $xterm-fg-237: #3a3a3a; + $xterm-fg-238: #444; + $xterm-fg-239: #4e4e4e; + $xterm-fg-240: #585858; + $xterm-fg-241: #626262; + $xterm-fg-242: #6c6c6c; + $xterm-fg-243: #767676; + $xterm-fg-244: #808080; + $xterm-fg-245: #8a8a8a; + $xterm-fg-246: #949494; + $xterm-fg-247: #9e9e9e; + $xterm-fg-248: #a8a8a8; + $xterm-fg-249: #b2b2b2; + $xterm-fg-250: #bcbcbc; + $xterm-fg-251: #c6c6c6; + $xterm-fg-252: #d0d0d0; + $xterm-fg-253: #dadada; + $xterm-fg-254: #e4e4e4; + $xterm-fg-255: #eee; .term-bold { font-weight: bold; @@ -169,1026 +429,1026 @@ } .xterm-fg-0 { - color: #000; + color: $xterm-fg-0; } .xterm-fg-1 { - color: #800000; + color: $xterm-fg-1; } .xterm-fg-2 { - color: #008000; + color: $xterm-fg-2; } .xterm-fg-3 { - color: #808000; + color: $xterm-fg-3; } .xterm-fg-4 { - color: #000080; + color: $xterm-fg-4; } .xterm-fg-5 { - color: #800080; + color: $xterm-fg-5; } .xterm-fg-6 { - color: #008080; + color: $xterm-fg-6; } .xterm-fg-7 { - color: #c0c0c0; + color: $xterm-fg-7; } .xterm-fg-8 { - color: #808080; + color: $xterm-fg-8; } .xterm-fg-9 { - color: #f00; + color: $xterm-fg-9; } .xterm-fg-10 { - color: #0f0; + color: $xterm-fg-10; } .xterm-fg-11 { - color: #ff0; + color: $xterm-fg-11; } .xterm-fg-12 { - color: #00f; + color: $xterm-fg-12; } .xterm-fg-13 { - color: #f0f; + color: $xterm-fg-13; } .xterm-fg-14 { - color: #0ff; + color: $xterm-fg-14; } .xterm-fg-15 { - color: #fff; + color: $white-light; } .xterm-fg-16 { - color: #000; + color: $black; } .xterm-fg-17 { - color: #00005f; + color: $xterm-fg-17; } .xterm-fg-18 { - color: #000087; + color: $xterm-fg-18; } .xterm-fg-19 { - color: #0000af; + color: $xterm-fg-19; } .xterm-fg-20 { - color: #0000d7; + color: $xterm-fg-20; } .xterm-fg-21 { - color: #00f; + color: $xterm-fg-21; } .xterm-fg-22 { - color: #005f00; + color: $xterm-fg-22; } .xterm-fg-23 { - color: #005f5f; + color: $xterm-fg-23; } .xterm-fg-24 { - color: #005f87; + color: $xterm-fg-24; } .xterm-fg-25 { - color: #005faf; + color: $xterm-fg-25; } .xterm-fg-26 { - color: #005fd7; + color: $xterm-fg-26; } .xterm-fg-27 { - color: #005fff; + color: $xterm-fg-27; } .xterm-fg-28 { - color: #008700; + color: $xterm-fg-28; } .xterm-fg-29 { - color: #00875f; + color: $xterm-fg-29; } .xterm-fg-30 { - color: #008787; + color: $xterm-fg-30; } .xterm-fg-31 { - color: #0087af; + color: $xterm-fg-31; } .xterm-fg-32 { - color: #0087d7; + color: $xterm-fg-32; } .xterm-fg-33 { - color: #0087ff; + color: $xterm-fg-33; } .xterm-fg-34 { - color: #00af00; + color: $xterm-fg-34; } .xterm-fg-35 { - color: #00af5f; + color: $xterm-fg-35; } .xterm-fg-36 { - color: #00af87; + color: $xterm-fg-36; } .xterm-fg-37 { - color: #00afaf; + color: $xterm-fg-37; } .xterm-fg-38 { - color: #00afd7; + color: $xterm-fg-38; } .xterm-fg-39 { - color: #00afff; + color: $xterm-fg-39; } .xterm-fg-40 { - color: #00d700; + color: $xterm-fg-40; } .xterm-fg-41 { - color: #00d75f; + color: $xterm-fg-41; } .xterm-fg-42 { - color: #00d787; + color: $xterm-fg-42; } .xterm-fg-43 { - color: #00d7af; + color: $xterm-fg-43; } .xterm-fg-44 { - color: #00d7d7; + color: $xterm-fg-44; } .xterm-fg-45 { - color: #00d7ff; + color: $xterm-fg-45; } .xterm-fg-46 { - color: #0f0; + color: $xterm-fg-46; } .xterm-fg-47 { - color: #00ff5f; + color: $xterm-fg-47; } .xterm-fg-48 { - color: #00ff87; + color: $xterm-fg-48; } .xterm-fg-49 { - color: #00ffaf; + color: $xterm-fg-49; } .xterm-fg-50 { - color: #00ffd7; + color: $xterm-fg-50; } .xterm-fg-51 { - color: #0ff; + color: $xterm-fg-51; } .xterm-fg-52 { - color: #5f0000; + color: $xterm-fg-52; } .xterm-fg-53 { - color: #5f005f; + color: $xterm-fg-53; } .xterm-fg-54 { - color: #5f0087; + color: $xterm-fg-54; } .xterm-fg-55 { - color: #5f00af; + color: $xterm-fg-55; } .xterm-fg-56 { - color: #5f00d7; + color: $xterm-fg-56; } .xterm-fg-57 { - color: #5f00ff; + color: $xterm-fg-57; } .xterm-fg-58 { - color: #5f5f00; + color: $xterm-fg-58; } .xterm-fg-59 { - color: #5f5f5f; + color: $xterm-fg-59; } .xterm-fg-60 { - color: #5f5f87; + color: $xterm-fg-60; } .xterm-fg-61 { - color: #5f5faf; + color: $xterm-fg-61; } .xterm-fg-62 { - color: #5f5fd7; + color: $xterm-fg-62; } .xterm-fg-63 { - color: #5f5fff; + color: $xterm-fg-63; } .xterm-fg-64 { - color: #5f8700; + color: $xterm-fg-64; } .xterm-fg-65 { - color: #5f875f; + color: $xterm-fg-65; } .xterm-fg-66 { - color: #5f8787; + color: $xterm-fg-66; } .xterm-fg-67 { - color: #5f87af; + color: $xterm-fg-67; } .xterm-fg-68 { - color: #5f87d7; + color: $xterm-fg-68; } .xterm-fg-69 { - color: #5f87ff; + color: $xterm-fg-69; } .xterm-fg-70 { - color: #5faf00; + color: $xterm-fg-70; } .xterm-fg-71 { - color: #5faf5f; + color: $xterm-fg-71; } .xterm-fg-72 { - color: #5faf87; + color: $xterm-fg-72; } .xterm-fg-73 { - color: #5fafaf; + color: $xterm-fg-73; } .xterm-fg-74 { - color: #5fafd7; + color: $xterm-fg-74; } .xterm-fg-75 { - color: #5fafff; + color: $xterm-fg-75; } .xterm-fg-76 { - color: #5fd700; + color: $xterm-fg-76; } .xterm-fg-77 { - color: #5fd75f; + color: $xterm-fg-77; } .xterm-fg-78 { - color: #5fd787; + color: $xterm-fg-78; } .xterm-fg-79 { - color: #5fd7af; + color: $xterm-fg-79; } .xterm-fg-80 { - color: #5fd7d7; + color: $xterm-fg-80; } .xterm-fg-81 { - color: #5fd7ff; + color: $xterm-fg-81; } .xterm-fg-82 { - color: #5fff00; + color: $xterm-fg-82; } .xterm-fg-83 { - color: #5fff5f; + color: $xterm-fg-83; } .xterm-fg-84 { - color: #5fff87; + color: $xterm-fg-84; } .xterm-fg-85 { - color: #5fffaf; + color: $xterm-fg-85; } .xterm-fg-86 { - color: #5fffd7; + color: $xterm-fg-86; } .xterm-fg-87 { - color: #5fffff; + color: $xterm-fg-87; } .xterm-fg-88 { - color: #870000; + color: $xterm-fg-88; } .xterm-fg-89 { - color: #87005f; + color: $xterm-fg-89; } .xterm-fg-90 { - color: #870087; + color: $xterm-fg-90; } .xterm-fg-91 { - color: #8700af; + color: $xterm-fg-91; } .xterm-fg-92 { - color: #8700d7; + color: $xterm-fg-92; } .xterm-fg-93 { - color: #8700ff; + color: $xterm-fg-93; } .xterm-fg-94 { - color: #875f00; + color: $xterm-fg-94; } .xterm-fg-95 { - color: #875f5f; + color: $xterm-fg-95; } .xterm-fg-96 { - color: #875f87; + color: $xterm-fg-96; } .xterm-fg-97 { - color: #875faf; + color: $xterm-fg-97; } .xterm-fg-98 { - color: #875fd7; + color: $xterm-fg-98; } .xterm-fg-99 { - color: #875fff; + color: $xterm-fg-99; } .xterm-fg-100 { - color: #878700; + color: $xterm-fg-100; } .xterm-fg-101 { - color: #87875f; + color: $xterm-fg-101; } .xterm-fg-102 { - color: #878787; + color: $xterm-fg-102; } .xterm-fg-103 { - color: #8787af; + color: $xterm-fg-103; } .xterm-fg-104 { - color: #8787d7; + color: $xterm-fg-104; } .xterm-fg-105 { - color: #8787ff; + color: $xterm-fg-105; } .xterm-fg-106 { - color: #87af00; + color: $xterm-fg-106; } .xterm-fg-107 { - color: #87af5f; + color: $xterm-fg-107; } .xterm-fg-108 { - color: #87af87; + color: $xterm-fg-108; } .xterm-fg-109 { - color: #87afaf; + color: $xterm-fg-109; } .xterm-fg-110 { - color: #87afd7; + color: $xterm-fg-110; } .xterm-fg-111 { - color: #87afff; + color: $xterm-fg-111; } .xterm-fg-112 { - color: #87d700; + color: $xterm-fg-112; } .xterm-fg-113 { - color: #87d75f; + color: $xterm-fg-113; } .xterm-fg-114 { - color: #87d787; + color: $xterm-fg-114; } .xterm-fg-115 { - color: #87d7af; + color: $xterm-fg-115; } .xterm-fg-116 { - color: #87d7d7; + color: $xterm-fg-116; } .xterm-fg-117 { - color: #87d7ff; + color: $xterm-fg-117; } .xterm-fg-118 { - color: #87ff00; + color: $xterm-fg-118; } .xterm-fg-119 { - color: #87ff5f; + color: $xterm-fg-119; } .xterm-fg-120 { - color: #87ff87; + color: $xterm-fg-120; } .xterm-fg-121 { - color: #87ffaf; + color: $xterm-fg-121; } .xterm-fg-122 { - color: #87ffd7; + color: $xterm-fg-122; } .xterm-fg-123 { - color: #87ffff; + color: $xterm-fg-123; } .xterm-fg-124 { - color: #af0000; + color: $xterm-fg-124; } .xterm-fg-125 { - color: #af005f; + color: $xterm-fg-125; } .xterm-fg-126 { - color: #af0087; + color: $xterm-fg-126; } .xterm-fg-127 { - color: #af00af; + color: $xterm-fg-127; } .xterm-fg-128 { - color: #af00d7; + color: $xterm-fg-128; } .xterm-fg-129 { - color: #af00ff; + color: $xterm-fg-129; } .xterm-fg-130 { - color: #af5f00; + color: $xterm-fg-130; } .xterm-fg-131 { - color: #af5f5f; + color: $xterm-fg-131; } .xterm-fg-132 { - color: #af5f87; + color: $xterm-fg-132; } .xterm-fg-133 { - color: #af5faf; + color: $xterm-fg-133; } .xterm-fg-134 { - color: #af5fd7; + color: $xterm-fg-134; } .xterm-fg-135 { - color: #af5fff; + color: $xterm-fg-135; } .xterm-fg-136 { - color: #af8700; + color: $xterm-fg-136; } .xterm-fg-137 { - color: #af875f; + color: $xterm-fg-137; } .xterm-fg-138 { - color: #af8787; + color: $xterm-fg-138; } .xterm-fg-139 { - color: #af87af; + color: $xterm-fg-139; } .xterm-fg-140 { - color: #af87d7; + color: $xterm-fg-140; } .xterm-fg-141 { - color: #af87ff; + color: $xterm-fg-141; } .xterm-fg-142 { - color: #afaf00; + color: $xterm-fg-142; } .xterm-fg-143 { - color: #afaf5f; + color: $xterm-fg-143; } .xterm-fg-144 { - color: #afaf87; + color: $xterm-fg-144; } .xterm-fg-145 { - color: #afafaf; + color: $xterm-fg-145; } .xterm-fg-146 { - color: #afafd7; + color: $xterm-fg-146; } .xterm-fg-147 { - color: #afafff; + color: $xterm-fg-147; } .xterm-fg-148 { - color: #afd700; + color: $xterm-fg-148; } .xterm-fg-149 { - color: #afd75f; + color: $xterm-fg-149; } .xterm-fg-150 { - color: #afd787; + color: $xterm-fg-150; } .xterm-fg-151 { - color: #afd7af; + color: $xterm-fg-151; } .xterm-fg-152 { - color: #afd7d7; + color: $xterm-fg-152; } .xterm-fg-153 { - color: #afd7ff; + color: $xterm-fg-153; } .xterm-fg-154 { - color: #afff00; + color: $xterm-fg-154; } .xterm-fg-155 { - color: #afff5f; + color: $xterm-fg-155; } .xterm-fg-156 { - color: #afff87; + color: $xterm-fg-156; } .xterm-fg-157 { - color: #afffaf; + color: $xterm-fg-157; } .xterm-fg-158 { - color: #afffd7; + color: $xterm-fg-158; } .xterm-fg-159 { - color: #afffff; + color: $xterm-fg-159; } .xterm-fg-160 { - color: #d70000; + color: $xterm-fg-160; } .xterm-fg-161 { - color: #d7005f; + color: $xterm-fg-161; } .xterm-fg-162 { - color: #d70087; + color: $xterm-fg-162; } .xterm-fg-163 { - color: #d700af; + color: $xterm-fg-163; } .xterm-fg-164 { - color: #d700d7; + color: $xterm-fg-164; } .xterm-fg-165 { - color: #d700ff; + color: $xterm-fg-165; } .xterm-fg-166 { - color: #d75f00; + color: $xterm-fg-166; } .xterm-fg-167 { - color: #d75f5f; + color: $xterm-fg-167; } .xterm-fg-168 { - color: #d75f87; + color: $xterm-fg-168; } .xterm-fg-169 { - color: #d75faf; + color: $xterm-fg-169; } .xterm-fg-170 { - color: #d75fd7; + color: $xterm-fg-170; } .xterm-fg-171 { - color: #d75fff; + color: $xterm-fg-171; } .xterm-fg-172 { - color: #d78700; + color: $xterm-fg-172; } .xterm-fg-173 { - color: #d7875f; + color: $xterm-fg-173; } .xterm-fg-174 { - color: #d78787; + color: $xterm-fg-174; } .xterm-fg-175 { - color: #d787af; + color: $xterm-fg-175; } .xterm-fg-176 { - color: #d787d7; + color: $xterm-fg-176; } .xterm-fg-177 { - color: #d787ff; + color: $xterm-fg-177; } .xterm-fg-178 { - color: #d7af00; + color: $xterm-fg-178; } .xterm-fg-179 { - color: #d7af5f; + color: $xterm-fg-179; } .xterm-fg-180 { - color: #d7af87; + color: $xterm-fg-180; } .xterm-fg-181 { - color: #d7afaf; + color: $xterm-fg-181; } .xterm-fg-182 { - color: #d7afd7; + color: $xterm-fg-182; } .xterm-fg-183 { - color: #d7afff; + color: $xterm-fg-183; } .xterm-fg-184 { - color: #d7d700; + color: $xterm-fg-184; } .xterm-fg-185 { - color: #d7d75f; + color: $xterm-fg-185; } .xterm-fg-186 { - color: #d7d787; + color: $xterm-fg-186; } .xterm-fg-187 { - color: #d7d7af; + color: $xterm-fg-187; } .xterm-fg-188 { - color: #d7d7d7; + color: $xterm-fg-188; } .xterm-fg-189 { - color: #d7d7ff; + color: $xterm-fg-189; } .xterm-fg-190 { - color: #d7ff00; + color: $xterm-fg-190; } .xterm-fg-191 { - color: #d7ff5f; + color: $xterm-fg-191; } .xterm-fg-192 { - color: #d7ff87; + color: $xterm-fg-192; } .xterm-fg-193 { - color: #d7ffaf; + color: $xterm-fg-193; } .xterm-fg-194 { - color: #d7ffd7; + color: $xterm-fg-194; } .xterm-fg-195 { - color: #d7ffff; + color: $xterm-fg-195; } .xterm-fg-196 { - color: #f00; + color: $xterm-fg-196; } .xterm-fg-197 { - color: #ff005f; + color: $xterm-fg-197; } .xterm-fg-198 { - color: #ff0087; + color: $xterm-fg-198; } .xterm-fg-199 { - color: #ff00af; + color: $xterm-fg-199; } .xterm-fg-200 { - color: #ff00d7; + color: $xterm-fg-200; } .xterm-fg-201 { - color: #f0f; + color: $xterm-fg-201; } .xterm-fg-202 { - color: #ff5f00; + color: $xterm-fg-202; } .xterm-fg-203 { - color: #ff5f5f; + color: $xterm-fg-203; } .xterm-fg-204 { - color: #ff5f87; + color: $xterm-fg-204; } .xterm-fg-205 { - color: #ff5faf; + color: $xterm-fg-205; } .xterm-fg-206 { - color: #ff5fd7; + color: $xterm-fg-206; } .xterm-fg-207 { - color: #ff5fff; + color: $xterm-fg-207; } .xterm-fg-208 { - color: #ff8700; + color: $xterm-fg-208; } .xterm-fg-209 { - color: #ff875f; + color: $xterm-fg-209; } .xterm-fg-210 { - color: #ff8787; + color: $xterm-fg-210; } .xterm-fg-211 { - color: #ff87af; + color: $xterm-fg-211; } .xterm-fg-212 { - color: #ff87d7; + color: $xterm-fg-212; } .xterm-fg-213 { - color: #ff87ff; + color: $xterm-fg-213; } .xterm-fg-214 { - color: #ffaf00; + color: $xterm-fg-214; } .xterm-fg-215 { - color: #ffaf5f; + color: $xterm-fg-215; } .xterm-fg-216 { - color: #ffaf87; + color: $xterm-fg-216; } .xterm-fg-217 { - color: #ffafaf; + color: $xterm-fg-217; } .xterm-fg-218 { - color: #ffafd7; + color: $xterm-fg-218; } .xterm-fg-219 { - color: #ffafff; + color: $xterm-fg-219; } .xterm-fg-220 { - color: #ffd700; + color: $xterm-fg-220; } .xterm-fg-221 { - color: #ffd75f; + color: $xterm-fg-221; } .xterm-fg-222 { - color: #ffd787; + color: $xterm-fg-222; } .xterm-fg-223 { - color: #ffd7af; + color: $xterm-fg-223; } .xterm-fg-224 { - color: #ffd7d7; + color: $xterm-fg-224; } .xterm-fg-225 { - color: #ffd7ff; + color: $xterm-fg-225; } .xterm-fg-226 { - color: #ff0; + color: $xterm-fg-226; } .xterm-fg-227 { - color: #ffff5f; + color: $xterm-fg-227; } .xterm-fg-228 { - color: #ffff87; + color: $xterm-fg-228; } .xterm-fg-229 { - color: #ffffaf; + color: $xterm-fg-229; } .xterm-fg-230 { - color: #ffffd7; + color: $xterm-fg-230; } .xterm-fg-231 { - color: #fff; + color: $xterm-fg-231; } .xterm-fg-232 { - color: #080808; + color: $xterm-fg-232; } .xterm-fg-233 { - color: #121212; + color: $xterm-fg-233; } .xterm-fg-234 { - color: #1c1c1c; + color: $xterm-fg-234; } .xterm-fg-235 { - color: #262626; + color: $xterm-fg-235; } .xterm-fg-236 { - color: #303030; + color: $xterm-fg-236; } .xterm-fg-237 { - color: #3a3a3a; + color: $xterm-fg-237; } .xterm-fg-238 { - color: #444; + color: $xterm-fg-238; } .xterm-fg-239 { - color: #4e4e4e; + color: $xterm-fg-239; } .xterm-fg-240 { - color: #585858; + color: $xterm-fg-240; } .xterm-fg-241 { - color: #626262; + color: $xterm-fg-241; } .xterm-fg-242 { - color: #6c6c6c; + color: $xterm-fg-242; } .xterm-fg-243 { - color: #767676; + color: $xterm-fg-243; } .xterm-fg-244 { - color: #808080; + color: $xterm-fg-244; } .xterm-fg-245 { - color: #8a8a8a; + color: $xterm-fg-245; } .xterm-fg-246 { - color: #949494; + color: $xterm-fg-246; } .xterm-fg-247 { - color: #9e9e9e; + color: $xterm-fg-247; } .xterm-fg-248 { - color: #a8a8a8; + color: $xterm-fg-248; } .xterm-fg-249 { - color: #b2b2b2; + color: $xterm-fg-249; } .xterm-fg-250 { - color: #bcbcbc; + color: $xterm-fg-250; } .xterm-fg-251 { - color: #c6c6c6; + color: $xterm-fg-251; } .xterm-fg-252 { - color: #d0d0d0; + color: $xterm-fg-252; } .xterm-fg-253 { - color: #dadada; + color: $xterm-fg-253; } .xterm-fg-254 { - color: #e4e4e4; + color: $xterm-fg-254; } .xterm-fg-255 { - color: #eee; + color: $xterm-fg-255; } } diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 52e0256943a..1b4987dd738 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -67,65 +67,78 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file] params.require(:application_setting).permit( - :default_projects_limit, - :default_branch_protection, - :signup_enabled, - :signin_enabled, - :require_two_factor_authentication, - :two_factor_grace_period, - :gravatar_enabled, - :sign_in_text, - :after_sign_up_text, - :help_page_text, - :home_page_url, + application_setting_params_ce + ) + end + + def application_setting_params_ce + [ + :admin_notification_email, :after_sign_out_path, - :max_attachment_size, - :session_expire_delay, + :after_sign_up_text, + :akismet_api_key, + :akismet_enabled, + :container_registry_token_expire_delay, + :default_branch_protection, + :default_group_visibility, :default_project_visibility, + :default_projects_limit, :default_snippet_visibility, - :default_group_visibility, - :domain_whitelist_raw, :domain_blacklist_enabled, - :domain_blacklist_raw, :domain_blacklist_file, - :version_check_enabled, - :admin_notification_email, - :user_oauth_applications, - :user_default_external, - :shared_runners_enabled, - :shared_runners_text, + :domain_blacklist_raw, + :domain_whitelist_raw, + :email_author_in_body, + :enabled_git_access_protocol, + :gravatar_enabled, + :help_page_text, + :home_page_url, + :housekeeping_bitmaps_enabled, + :housekeeping_enabled, + :housekeeping_full_repack_period, + :housekeeping_gc_period, + :housekeeping_incremental_repack_period, + :html_emails_enabled, + :koding_enabled, + :koding_url, + :plantuml_enabled, + :plantuml_url, :max_artifacts_size, + :max_attachment_size, :metrics_enabled, :metrics_host, - :metrics_port, - :metrics_pool_size, - :metrics_timeout, :metrics_method_call_threshold, + :metrics_packet_size, + :metrics_pool_size, + :metrics_port, :metrics_sample_interval, + :metrics_timeout, :recaptcha_enabled, - :recaptcha_site_key, :recaptcha_private_key, - :sentry_enabled, - :sentry_dsn, - :akismet_enabled, - :akismet_api_key, - :koding_enabled, - :koding_url, - :email_author_in_body, + :recaptcha_site_key, :repository_checks_enabled, - :metrics_packet_size, + :require_two_factor_authentication, + :session_expire_delay, + :sign_in_text, + :signin_enabled, + :signup_enabled, + :sentry_dsn, + :sentry_enabled, :send_user_confirmation_email, - :container_registry_token_expire_delay, - :enabled_git_access_protocol, - :housekeeping_enabled, - :housekeeping_bitmaps_enabled, - :housekeeping_incremental_repack_period, - :housekeeping_full_repack_period, - :housekeeping_gc_period, + :shared_runners_enabled, + :shared_runners_text, + :sidekiq_throttling_enabled, + :sidekiq_throttling_factor, + :two_factor_grace_period, + :user_default_external, + :user_oauth_applications, + :version_check_enabled, + + disabled_oauth_sign_in_sources: [], + import_sources: [], repository_storages: [], restricted_visibility_levels: [], - import_sources: [], - disabled_oauth_sign_in_sources: [] - ) + sidekiq_throttling_queues: [] + ] end end diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb index 471d24934a0..62f62e99a97 100644 --- a/app/controllers/admin/applications_controller.rb +++ b/app/controllers/admin/applications_controller.rb @@ -1,5 +1,8 @@ class Admin::ApplicationsController < Admin::ApplicationController + include OauthApplications + before_action :set_application, only: [:show, :edit, :update, :destroy] + before_action :load_scopes, only: [:new, :edit] def index @applications = Doorkeeper::Application.where("owner_id IS NULL") @@ -47,6 +50,6 @@ class Admin::ApplicationsController < Admin::ApplicationController # Only allow a trusted parameter "white list" through. def application_params - params[:doorkeeper_application].permit(:name, :redirect_uri) + params[:doorkeeper_application].permit(:name, :redirect_uri, :scopes) end end diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb index 285e8495342..4f6a7e9e2cb 100644 --- a/app/controllers/admin/deploy_keys_controller.rb +++ b/app/controllers/admin/deploy_keys_controller.rb @@ -10,7 +10,7 @@ class Admin::DeployKeysController < Admin::ApplicationController end def create - @deploy_key = deploy_keys.new(deploy_key_params) + @deploy_key = deploy_keys.new(deploy_key_params.merge(user: current_user)) if @deploy_key.save redirect_to admin_deploy_keys_path @@ -39,6 +39,6 @@ class Admin::DeployKeysController < Admin::ApplicationController end def deploy_key_params - params.require(:deploy_key).permit(:key, :title) + params.require(:deploy_key).permit(:key, :title, :can_push) end end diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index aa7570cd896..b7722a1d15d 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -1,17 +1,18 @@ class Admin::GroupsController < Admin::ApplicationController - before_action :group, only: [:edit, :show, :update, :destroy, :project_update, :members_update] + before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update] def index - @groups = Group.all + @groups = Group.with_statistics @groups = @groups.sort(@sort = params[:sort]) @groups = @groups.search(params[:name]) if params[:name].present? @groups = @groups.page(params[:page]) end def show + @group = Group.with_statistics.joins(:route).group('routes.path').find_by_full_path(params[:id]) @members = @group.members.order("access_level DESC").page(params[:members_page]) @requesters = AccessRequestsFinder.new(@group).execute(current_user) - @projects = @group.projects.page(params[:projects_page]) + @projects = @group.projects.with_statistics.page(params[:projects_page]) end def new @@ -56,11 +57,15 @@ class Admin::GroupsController < Admin::ApplicationController private def group - @group ||= Group.find_by(path: params[:id]) + @group ||= Group.find_by_full_path(params[:id]) end def group_params - params.require(:group).permit( + params.require(:group).permit(group_params_ce) + end + + def group_params_ce + [ :avatar, :description, :lfs_enabled, @@ -68,6 +73,6 @@ class Admin::GroupsController < Admin::ApplicationController :path, :request_access_enabled, :visibility_level - ) + ] end end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 1d963bdf7d5..b09ae423096 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -3,7 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController before_action :group, only: [:show, :transfer] def index - @projects = Project.all + @projects = Project.with_statistics @projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present? @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = @projects.with_push if params[:with_push].present? diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index bb912ed10cc..aa0f8d434dc 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -16,9 +16,6 @@ class Admin::UsersController < Admin::ApplicationController @joined_projects = user.projects.joined(@user) end - def groups - end - def keys @keys = user.keys end @@ -164,15 +161,6 @@ class Admin::UsersController < Admin::ApplicationController @user ||= User.find_by!(username: params[:id]) end - def user_params - params.require(:user).permit( - :email, :remember_me, :bio, :name, :username, - :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password, - :extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password, - :projects_limit, :can_create_group, :admin, :key_id, :external - ) - end - def redirect_back_or_admin_user(options = {}) redirect_back_or_default(default: default_route, options: options) end @@ -180,4 +168,36 @@ class Admin::UsersController < Admin::ApplicationController def default_route [:admin, @user] end + + def user_params + params.require(:user).permit(user_params_ce) + end + + def user_params_ce + [ + :admin, + :avatar, + :bio, + :can_create_group, + :color_scheme_id, + :email, + :extern_uid, + :external, + :force_random_password, + :hide_no_password, + :hide_no_ssh_key, + :key_id, + :linkedin, + :name, + :password_expires_at, + :projects_limit, + :provider, + :remember_me, + :skype, + :theme_id, + :twitter, + :username, + :website_url + ] + end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 517ad4f03f3..bb47e2a8bf7 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -25,7 +25,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception helper_method :can?, :current_application_settings - helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? + helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) @@ -49,6 +49,14 @@ class ApplicationController < ActionController::Base render_404 end + def route_not_found + if current_user + not_found + else + redirect_to new_user_session_path + end + end + protected # This filter handles both private tokens and personal access tokens @@ -224,7 +232,7 @@ class ApplicationController < ActionController::Base end def require_email - if current_user && current_user.temp_oauth_email? + if current_user && current_user.temp_oauth_email? && session[:impersonator_id].nil? redirect_to profile_path, notice: 'Please complete your profile with email address' and return end end @@ -237,6 +245,10 @@ class ApplicationController < ActionController::Base current_application_settings.import_sources.include?('github') end + def gitea_import_enabled? + current_application_settings.import_sources.include?('gitea') + end + def github_import_configured? Gitlab::OAuth::Provider.enabled?(:github) end @@ -254,7 +266,7 @@ class ApplicationController < ActionController::Base end def bitbucket_import_configured? - Gitlab::OAuth::Provider.enabled?(:bitbucket) && Gitlab::BitbucketImport.public_key.present? + Gitlab::OAuth::Provider.enabled?(:bitbucket) end def google_code_import_enabled? diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index daa82336208..6db4e1dc1bc 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -11,14 +11,14 @@ class AutocompleteController < ApplicationController @users = @users.reorder(:name) @users = @users.page(params[:page]) - if params[:todo_filter].present? + if params[:todo_filter].present? && current_user @users = @users.todo_authors(current_user.id, params[:todo_state_filter]) end 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? @@ -55,7 +55,13 @@ class AutocompleteController < ApplicationController def find_users @users = if @project - @project.team.users + user_ids = @project.team.users.pluck(:id) + + if params[:author_id].present? + user_ids << params[:author_id] + end + + User.where(id: user_ids) elsif params[:group_id].present? group = Group.find(params[:group_id]) return render_404 unless can?(current_user, :read_group, group) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index dacb5679dd3..6f43ce5226d 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -81,10 +81,8 @@ module CreatesCommit def merge_request_exists? return @merge_request if defined?(@merge_request) - @merge_request = @mr_target_project.merge_requests.opened.find_by( - source_branch: @mr_source_branch, - target_branch: @mr_target_branch - ) + @merge_request = MergeRequestsFinder.new(current_user, project_id: @mr_target_project.id).execute.opened. + find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch, source_project_id: @mr_source_project) end def different_project? diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb new file mode 100644 index 00000000000..52e06f4945a --- /dev/null +++ b/app/controllers/concerns/cycle_analytics_params.rb @@ -0,0 +1,11 @@ +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 +end diff --git a/app/controllers/concerns/diff_for_path.rb b/app/controllers/concerns/diff_for_path.rb index aeec3009f15..1efa9fe060f 100644 --- a/app/controllers/concerns/diff_for_path.rb +++ b/app/controllers/concerns/diff_for_path.rb @@ -3,7 +3,7 @@ module DiffForPath def render_diff_for_path(diffs) diff_file = diffs.diff_files.find do |diff| - diff.old_path == params[:old_path] && diff.new_path == params[:new_path] + diff.file_identifier == params[:file_identifier] end return render_404 unless diff_file diff --git a/app/controllers/concerns/global_milestones.rb b/app/controllers/concerns/global_milestones.rb deleted file mode 100644 index 5c503c5b698..00000000000 --- a/app/controllers/concerns/global_milestones.rb +++ /dev/null @@ -1,20 +0,0 @@ -module GlobalMilestones - extend ActiveSupport::Concern - - def milestones - epoch = DateTime.parse('1970-01-01') - @milestones = MilestonesFinder.new.execute(@projects, params) - @milestones = GlobalMilestone.build_collection(@milestones) - @milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date } - end - - def milestone - milestones = Milestone.of_projects(@projects).where(title: params[:title]) - - if milestones.present? - @milestone = GlobalMilestone.new(params[:title], milestones) - else - render_404 - end - end -end diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index be86fa106f8..0821974aa93 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -12,7 +12,7 @@ module IssuableActions destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym TodoService.new.public_send(destroy_method, issuable, current_user) - name = issuable.class.name.titleize.downcase + name = issuable.human_class_name flash[:notice] = "The #{name} was successfully deleted." redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]) end diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index b5e79099e39..6247934f81e 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -10,11 +10,11 @@ module IssuableCollections private def issues_collection - issues_finder.execute + issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace) end def merge_requests_collection - merge_requests_finder.execute + merge_requests_finder.execute.preload(:source_project, :target_project, :author, :assignee, :labels, :milestone, :merge_request_diff, target_project: :namespace) end def issues_finder diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index b89fb94be6e..b46adcceb60 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -7,7 +7,6 @@ module IssuesAction @issues = issues_collection .non_archived - .preload(:author, :project) .page(params[:page]) respond_to do |format| diff --git a/app/helpers/lfs_helper.rb b/app/controllers/concerns/lfs_request.rb index d3966ba1f10..ed22b1e5470 100644 --- a/app/helpers/lfs_helper.rb +++ b/app/controllers/concerns/lfs_request.rb @@ -1,5 +1,21 @@ -module LfsHelper - include Gitlab::Routing.url_helpers +# This concern assumes: +# - a `#project` accessor +# - a `#user` accessor +# - a `#authentication_result` accessor +# - a `#can?(object, action, subject)` method +# - a `#ci?` method +# - a `#download_request?` method +# - a `#upload_request?` method +# - a `#has_authentication_ability?(ability)` method +module LfsRequest + extend ActiveSupport::Concern + + included do + before_action :require_lfs_enabled! + before_action :lfs_check_access! + end + + private def require_lfs_enabled! return if Gitlab.config.lfs.enabled @@ -17,31 +33,15 @@ module LfsHelper return if download_request? && lfs_download_access? return if upload_request? && lfs_upload_access? - if project.public? || (user && user.can?(:read_project, project)) - render_lfs_forbidden + if project.public? || can?(user, :read_project, project) + lfs_forbidden! else render_lfs_not_found end end - def lfs_download_access? - return false unless project.lfs_enabled? - - ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code? - end - - def user_can_download_code? - has_authentication_ability?(:download_code) && can?(user, :download_code, project) - end - - def build_can_download_code? - has_authentication_ability?(:build_download_code) && can?(user, :build_download_code, project) - end - - def lfs_upload_access? - return false unless project.lfs_enabled? - - has_authentication_ability?(:push_code) && can?(user, :push_code, project) + def lfs_forbidden! + render_lfs_forbidden end def render_lfs_forbidden @@ -66,6 +66,30 @@ module LfsHelper ) end + def lfs_download_access? + return false unless project.lfs_enabled? + + ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code? + end + + def lfs_upload_access? + return false unless project.lfs_enabled? + + has_authentication_ability?(:push_code) && can?(user, :push_code, project) + end + + def lfs_deploy_token? + authentication_result.lfs_deploy_token?(project) + end + + def user_can_download_code? + has_authentication_ability?(:download_code) && can?(user, :download_code, project) + end + + def build_can_download_code? + has_authentication_ability?(:build_download_code) && can?(user, :build_download_code, project) + end + def storage_project @storage_project ||= begin result = project @@ -78,4 +102,8 @@ module LfsHelper result end end + + def objects + @objects ||= (params[:objects] || []).to_a + end end diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb index a1b0eee37f9..fdb05bb3228 100644 --- a/app/controllers/concerns/merge_requests_action.rb +++ b/app/controllers/concerns/merge_requests_action.rb @@ -6,8 +6,12 @@ module MergeRequestsAction @label = merge_requests_finder.labels.first @merge_requests = merge_requests_collection - .non_archived - .preload(:author, :target_project) .page(params[:page]) end + + private + + def filter_params + super.merge(non_archived: true) + end end diff --git a/app/controllers/concerns/oauth_applications.rb b/app/controllers/concerns/oauth_applications.rb new file mode 100644 index 00000000000..9849aa93fa6 --- /dev/null +++ b/app/controllers/concerns/oauth_applications.rb @@ -0,0 +1,19 @@ +module OauthApplications + extend ActiveSupport::Concern + + included do + before_action :prepare_scopes, only: [:create, :update] + end + + def prepare_scopes + scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil) + + if scopes + params[:doorkeeper_application][:scopes] = scopes.join(' ') + end + end + + def load_scopes + @scopes = Doorkeeper.configuration.scopes + end +end diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index c33d7eecb9f..d7f5a4e4682 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -1,31 +1,72 @@ module ServiceParams extend ActiveSupport::Concern - ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_url, :api_version, :subdomain, - :room, :recipients, :project_url, :webhook, - :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, - :build_key, :server, :teamcity_url, :drone_url, :build_type, - :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, - :colorize_messages, :channels, - # We're using `issues_events` and `merge_requests_events` - # in the view so we still need to explicitly state them - # here. `Service#event_names` would only give - # `issue_events` and `merge_request_events` (singular!) - # See app/helpers/services_helper.rb for how we - # make those event names plural as special case. - :issues_events, :confidential_issues_events, :merge_requests_events, - :notify_only_broken_builds, :notify_only_broken_pipelines, - :add_pusher, :send_from_committer_email, :disable_diffs, - :external_wiki_url, :notify, :color, - :server_host, :server_port, :default_irc_uri, :enable_ssl_verification, - :jira_issue_transition_id, :url, :project_key] + ALLOWED_PARAMS_CE = [ + :active, + :add_pusher, + :api_key, + :api_url, + :api_version, + :bamboo_url, + :build_key, + :build_type, + :ca_pem, + :channel, + :channels, + :color, + :colorize_messages, + :confidential_issues_events, + :default_irc_uri, + :description, + :device, + :disable_diffs, + :drone_url, + :enable_ssl_verification, + :external_wiki_url, + # We're using `issues_events` and `merge_requests_events` + # in the view so we still need to explicitly state them + # here. `Service#event_names` would only give + # `issue_events` and `merge_request_events` (singular!) + # See app/helpers/services_helper.rb for how we + # make those event names plural as special case. + :issues_events, + :issues_url, + :jira_issue_transition_id, + :merge_requests_events, + :namespace, + :new_issue_url, + :notify, + :notify_only_broken_builds, + :notify_only_broken_pipelines, + :password, + :priority, + :project_key, + :project_url, + :recipients, + :restrict_to_branch, + :room, + :send_from_committer_email, + :server, + :server_host, + :server_port, + :sound, + :subdomain, + :teamcity_url, + :title, + :token, + :type, + :url, + :user_key, + :username, + :webhook + ] # Parameters to ignore if no value is specified FILTER_BLANK_PARAMS = [:password] def service_params dynamic_params = @service.event_channel_names + @service.event_names - service_params = params.permit(:id, service: ALLOWED_PARAMS + dynamic_params) + service_params = params.permit(:id, service: ALLOWED_PARAMS_CE + dynamic_params) if service_params[:service].is_a?(Hash) FILTER_BLANK_PARAMS.each do |param| diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb index 3717c49f272..fbf9a026b10 100644 --- a/app/controllers/concerns/toggle_award_emoji.rb +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -1,11 +1,8 @@ module ToggleAwardEmoji extend ActiveSupport::Concern - included do - before_action :authenticate_user!, only: [:toggle_award_emoji] - end - def toggle_award_emoji + authenticate_user! name = params.require(:name) if awardable.user_can_award?(current_user, name) diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb index 9e3b9be2ff4..92cb534343e 100644 --- a/app/controllers/concerns/toggle_subscription_action.rb +++ b/app/controllers/concerns/toggle_subscription_action.rb @@ -4,13 +4,17 @@ module ToggleSubscriptionAction def toggle_subscription return unless current_user - subscribable_resource.toggle_subscription(current_user) + subscribable_resource.toggle_subscription(current_user, subscribable_project) head :ok end private + def subscribable_project + @project || raise(NotImplementedError) + end + def subscribable_resource raise NotImplementedError end diff --git a/app/controllers/concerns/workhorse_request.rb b/app/controllers/concerns/workhorse_request.rb new file mode 100644 index 00000000000..43c0f1b173c --- /dev/null +++ b/app/controllers/concerns/workhorse_request.rb @@ -0,0 +1,13 @@ +module WorkhorseRequest + extend ActiveSupport::Concern + + included do + before_action :verify_workhorse_api! + end + + private + + def verify_workhorse_api! + Gitlab::Workhorse.verify_api_request!(request.headers) + end +end diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 3da44b9b888..306afb65f10 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -14,12 +14,8 @@ class ConfirmationsController < Devise::ConfirmationsController if signed_in?(resource_name) after_sign_in_path_for(resource) else - sign_in(resource) - if signed_in?(resource_name) - after_sign_in_path_for(resource) - else - new_session_path(resource_name) - end + flash[:notice] += " Please sign in." + new_session_path(resource_name) end end end diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index fa9c6c054f0..7f506db583f 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -1,6 +1,4 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController - include GlobalMilestones - before_action :projects before_action :milestone, only: [:show] @@ -17,4 +15,15 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController def show end + + private + + def milestones + @milestones = DashboardMilestone.build_collection(@projects, params) + end + + def milestone + @milestone = DashboardMilestone.build(@projects, params[:title]) + render_404 unless @milestone + end end diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index d425d0f9014..e3933e3d7b1 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -4,6 +4,9 @@ class Dashboard::TodosController < Dashboard::ApplicationController def index @sort = params[:sort] @todos = @todos.page(params[:page]) + if @todos.out_of_range? && @todos.total_pages != 0 + redirect_to url_for(params.merge(page: @todos.total_pages)) + end end def destroy diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index 949b4a6c25a..c411c21bb80 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -9,7 +9,7 @@ class Groups::ApplicationController < ApplicationController def group unless @group id = params[:group_id] || params[:id] - @group = Group.find_by(path: id) + @group = Group.find_by_full_path(id) unless @group && can?(current_user, :read_group, @group) @group = nil diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 940a3ad20ba..4f273a8d4f0 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -1,20 +1,20 @@ class Groups::GroupMembersController < Groups::ApplicationController include MembershipActions + include SortingHelper # Authorize before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access] def index + @sort = params[:sort].presence || sort_value_name @project = @group.projects.find(params[:project_id]) if params[:project_id] + @members = @group.group_members @members = @members.non_invite unless can?(current_user, :admin_group, @group) + @members = @members.search(params[:search]) if params[:search].present? + @members = @members.sort(@sort) + @members = @members.page(params[:page]).per(50) - if params[:search].present? - users = @group.users.search(params[:search]).to_a - @members = @members.where(user_id: users) - end - - @members = @members.order('access_level DESC').page(params[:page]).per(50) @requesters = AccessRequestsFinder.new(@group).execute(current_user) @group_member = @group.group_members.new diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb index 29528b2cfaa..587898a8634 100644 --- a/app/controllers/groups/labels_controller.rb +++ b/app/controllers/groups/labels_controller.rb @@ -1,4 +1,6 @@ class Groups::LabelsController < Groups::ApplicationController + include ToggleSubscriptionAction + before_action :label, only: [:edit, :update, :destroy] before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy] before_action :save_previous_label_path, only: [:edit] @@ -69,6 +71,11 @@ class Groups::LabelsController < Groups::ApplicationController def label @label ||= @group.labels.find(params[:id]) end + alias_method :subscribable_resource, :label + + def subscribable_project + nil + end def label_params params.require(:label).permit(:title, :description, :color) diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 9d5a28e8d4d..0d872c86c8a 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -1,6 +1,4 @@ class Groups::MilestonesController < Groups::ApplicationController - include GlobalMilestones - before_action :group_projects before_action :milestone, only: [:show, :update] before_action :authorize_admin_milestones!, only: [:new, :create, :update] @@ -58,7 +56,7 @@ class Groups::MilestonesController < Groups::ApplicationController def render_new_with_error(empty_project_ids) @milestone = Milestone.new(milestone_params) - @milestone.errors.add(:project_id, "Please select at least one project.") if empty_project_ids + @milestone.errors.add(:base, "Please select at least one project.") if empty_project_ids render :new end @@ -67,10 +65,19 @@ class Groups::MilestonesController < Groups::ApplicationController end def milestone_params - params.require(:milestone).permit(:title, :description, :due_date, :state_event) + params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event) end def milestone_path(title) group_milestone_path(@group, title.to_slug.to_s, title: title) end + + def milestones + @milestones = GroupMilestone.build_collection(@group, @projects, params) + end + + def milestone + @milestone = GroupMilestone.build(@group, @projects, params[:title]) + render_404 unless @milestone + end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index b83c3a872cf..f81237db991 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -42,6 +42,8 @@ class GroupsController < Groups::ApplicationController @notification_setting = current_user.notification_settings_for(group) end + @nested_groups = group.children + setup_projects respond_to do |format| @@ -75,13 +77,15 @@ class GroupsController < Groups::ApplicationController end def projects - @projects = @group.projects.page(params[:page]) + @projects = @group.projects.with_statistics.page(params[:page]) end def update if Groups::UpdateService.new(@group, current_user, group_params).execute redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." else + @group.reset_path! + render action: "edit" end end @@ -121,7 +125,11 @@ class GroupsController < Groups::ApplicationController end def group_params - params.require(:group).permit( + params.require(:group).permit(group_params_ce) + end + + def group_params_ce + [ :avatar, :description, :lfs_enabled, @@ -131,7 +139,7 @@ class GroupsController < Groups::ApplicationController :request_access_enabled, :share_with_group_lock, :visibility_level - ) + ] end def load_events diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 4eca278599f..37feff79999 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -6,9 +6,11 @@ class HelpController < ApplicationController def index @help_index = File.read(Rails.root.join('doc', 'README.md')) - # Prefix Markdown links with `help/` unless they already have been - # See http://rubular.com/r/nwwhzH6Z8X - @help_index.gsub!(/(\]\()(?!help\/)([^\)\(]+)(\))/, '\1help/\2\3') + # Prefix Markdown links with `help/` unless they are external links + # See http://rubular.com/r/X3baHTbPO2 + @help_index.gsub!(%r{(?<delim>\]\()(?!.+://)(?!/)(?<link>[^\)\(]+\))}) do + "#{$~[:delim]}#{Gitlab.config.gitlab.relative_url_root}/help/#{$~[:link]}" + end end def show diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 6ea54744da8..8e42cdf415f 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -2,50 +2,57 @@ class Import::BitbucketController < Import::BaseController before_action :verify_bitbucket_import_enabled before_action :bitbucket_auth, except: :callback - rescue_from OAuth::Error, with: :bitbucket_unauthorized - rescue_from Gitlab::BitbucketImport::Client::Unauthorized, with: :bitbucket_unauthorized + rescue_from OAuth2::Error, with: :bitbucket_unauthorized + rescue_from Bitbucket::Error::Unauthorized, with: :bitbucket_unauthorized def callback - request_token = session.delete(:oauth_request_token) - raise "Session expired!" if request_token.nil? + response = client.auth_code.get_token(params[:code], redirect_uri: callback_import_bitbucket_url) - request_token.symbolize_keys! - - access_token = client.get_token(request_token, params[:oauth_verifier], callback_import_bitbucket_url) - - session[:bitbucket_access_token] = access_token.token - session[:bitbucket_access_token_secret] = access_token.secret + session[:bitbucket_token] = response.token + session[:bitbucket_expires_at] = response.expires_at + session[:bitbucket_expires_in] = response.expires_in + session[:bitbucket_refresh_token] = response.refresh_token redirect_to status_import_bitbucket_url end def status - @repos = client.projects - @incompatible_repos = client.incompatible_projects + bitbucket_client = Bitbucket::Client.new(credentials) + repos = bitbucket_client.repos + + @repos, @incompatible_repos = repos.partition { |repo| repo.valid? } - @already_added_projects = current_user.created_projects.where(import_type: "bitbucket") + @already_added_projects = current_user.created_projects.where(import_type: 'bitbucket') already_added_projects_names = @already_added_projects.pluck(:import_source) - @repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" } + @repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) } end def jobs - jobs = current_user.created_projects.where(import_type: "bitbucket").to_json(only: [:id, :import_status]) - render json: jobs + render json: current_user.created_projects + .where(import_type: 'bitbucket') + .to_json(only: [:id, :import_status]) end def create + bitbucket_client = Bitbucket::Client.new(credentials) + @repo_id = params[:repo_id].to_s - repo = client.project(@repo_id.gsub('___', '/')) - @project_name = repo['slug'] - @target_namespace = find_or_create_namespace(repo['owner'], client.user['user']['username']) + name = @repo_id.gsub('___', '/') + repo = bitbucket_client.repo(name) + @project_name = params[:new_name].presence || repo.name - unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user, access_params).execute - render 'deploy_key' and return - end + repo_owner = repo.owner + repo_owner = current_user.username if repo_owner == bitbucket_client.user.username + @target_namespace = params[:new_namespace].presence || repo_owner + + namespace = find_or_create_namespace(@target_namespace, current_user) - if current_user.can?(:create_projects, @target_namespace) - @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute + if current_user.can?(:create_projects, namespace) + # The token in a session can be expired, we need to get most recent one because + # Bitbucket::Connection class refreshes it. + session[:bitbucket_token] = bitbucket_client.connection.token + @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @project_name, namespace, current_user, credentials).execute else render 'unauthorized' end @@ -54,8 +61,15 @@ class Import::BitbucketController < Import::BaseController private def client - @client ||= Gitlab::BitbucketImport::Client.new(session[:bitbucket_access_token], - session[:bitbucket_access_token_secret]) + @client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options) + end + + def provider + Gitlab::OAuth::Provider.config_for('bitbucket') + end + + def options + OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys end def verify_bitbucket_import_enabled @@ -63,26 +77,23 @@ class Import::BitbucketController < Import::BaseController end def bitbucket_auth - if session[:bitbucket_access_token].blank? - go_to_bitbucket_for_permissions - end + go_to_bitbucket_for_permissions if session[:bitbucket_token].blank? end def go_to_bitbucket_for_permissions - request_token = client.request_token(callback_import_bitbucket_url) - session[:oauth_request_token] = request_token - - redirect_to client.authorize_url(request_token, callback_import_bitbucket_url) + redirect_to client.auth_code.authorize_url(redirect_uri: callback_import_bitbucket_url) end def bitbucket_unauthorized go_to_bitbucket_for_permissions end - def access_params + def credentials { - bitbucket_access_token: session[:bitbucket_access_token], - bitbucket_access_token_secret: session[:bitbucket_access_token_secret] + token: session[:bitbucket_token], + expires_at: session[:bitbucket_expires_at], + expires_in: session[:bitbucket_expires_in], + refresh_token: session[:bitbucket_refresh_token] } end end diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb new file mode 100644 index 00000000000..fbd851c64a7 --- /dev/null +++ b/app/controllers/import/gitea_controller.rb @@ -0,0 +1,45 @@ +class Import::GiteaController < Import::GithubController + def new + if session[access_token_key].present? && session[host_key].present? + redirect_to status_import_url + end + end + + def personal_access_token + session[host_key] = params[host_key] + super + end + + def status + @gitea_host_url = session[host_key] + super + end + + private + + def host_key + :"#{provider}_host_url" + end + + # Overriden methods + def provider + :gitea + end + + # Gitea is not yet an OAuth provider + # See https://github.com/go-gitea/gitea/issues/27 + def logged_in_with_provider? + false + end + + def provider_auth + if session[access_token_key].blank? || session[host_key].blank? + redirect_to new_import_gitea_url, + alert: 'You need to specify both an Access Token and a Host URL.' + end + end + + def client_options + { host: session[host_key], api_version: 'v1' } + end +end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index ee7d498c59c..53a5981e564 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -1,39 +1,37 @@ class Import::GithubController < Import::BaseController - before_action :verify_github_import_enabled - before_action :github_auth, only: [:status, :jobs, :create] + before_action :verify_import_enabled + before_action :provider_auth, only: [:status, :jobs, :create] - rescue_from Octokit::Unauthorized, with: :github_unauthorized - - helper_method :logged_in_with_github? + rescue_from Octokit::Unauthorized, with: :provider_unauthorized def new - if logged_in_with_github? - go_to_github_for_permissions - elsif session[:github_access_token] - redirect_to status_import_github_url + if logged_in_with_provider? + go_to_provider_for_permissions + elsif session[access_token_key] + redirect_to status_import_url end end def callback - session[:github_access_token] = client.get_token(params[:code]) - redirect_to status_import_github_url + session[access_token_key] = client.get_token(params[:code]) + redirect_to status_import_url end def personal_access_token - session[:github_access_token] = params[:personal_access_token] - redirect_to status_import_github_url + session[access_token_key] = params[:personal_access_token] + redirect_to status_import_url end def status @repos = client.repos - @already_added_projects = current_user.created_projects.where(import_type: "github") + @already_added_projects = current_user.created_projects.where(import_type: provider) already_added_projects_names = @already_added_projects.pluck(:import_source) - @repos.reject!{ |repo| already_added_projects_names.include? repo.full_name } + @repos.reject! { |repo| already_added_projects_names.include? repo.full_name } end def jobs - jobs = current_user.created_projects.where(import_type: "github").to_json(only: [:id, :import_status]) + jobs = current_user.created_projects.where(import_type: provider).to_json(only: [:id, :import_status]) render json: jobs end @@ -44,8 +42,8 @@ class Import::GithubController < Import::BaseController namespace_path = params[:target_namespace].presence || current_user.namespace_path @target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path) - if current_user.can?(:create_projects, @target_namespace) - @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params).execute + if can?(current_user, :create_projects, @target_namespace) + @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params, type: provider).execute else render 'unauthorized' end @@ -54,34 +52,63 @@ class Import::GithubController < Import::BaseController private def client - @client ||= Gitlab::GithubImport::Client.new(session[:github_access_token]) + @client ||= Gitlab::GithubImport::Client.new(session[access_token_key], client_options) end - def verify_github_import_enabled - render_404 unless github_import_enabled? + def verify_import_enabled + render_404 unless import_enabled? end - def github_auth - if session[:github_access_token].blank? - go_to_github_for_permissions - end + def go_to_provider_for_permissions + redirect_to client.authorize_url(callback_import_url) end - def go_to_github_for_permissions - redirect_to client.authorize_url(callback_import_github_url) + def import_enabled? + __send__("#{provider}_import_enabled?") end - def github_unauthorized - session[:github_access_token] = nil - redirect_to new_import_github_url, - alert: 'Access denied to your GitHub account.' + def new_import_url + public_send("new_import_#{provider}_url") end - def logged_in_with_github? - current_user.identities.exists?(provider: 'github') + def status_import_url + public_send("status_import_#{provider}_url") + end + + def callback_import_url + public_send("callback_import_#{provider}_url") + end + + def provider_unauthorized + session[access_token_key] = nil + redirect_to new_import_url, + alert: "Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account." + end + + def access_token_key + :"#{provider}_access_token" end def access_params - { github_access_token: session[:github_access_token] } + { github_access_token: session[access_token_key] } + end + + # The following methods are overriden in subclasses + def provider + :github + end + + def logged_in_with_provider? + current_user.identities.exists?(provider: provider) + end + + def provider_auth + if session[access_token_key].blank? + go_to_provider_for_permissions + end + end + + def client_options + {} end end diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index c736200a104..c2e4d62b50b 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -26,7 +26,7 @@ class JwtController < ApplicationController @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip) render_unauthorized unless @authentication_result.success? && - (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User)) + (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User)) end rescue Gitlab::Auth::MissingPersonalTokenError render_missing_personal_token diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index 0f54dfa4efc..2ae4785b12c 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -2,10 +2,12 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController include Gitlab::CurrentSettings include Gitlab::GonHelper include PageLayoutHelper + include OauthApplications before_action :verify_user_oauth_applications_enabled before_action :authenticate_user! before_action :add_gon_variables + before_action :load_scopes, only: [:index, :create, :edit] layout 'profile' diff --git a/app/controllers/profiles/avatars_controller.rb b/app/controllers/profiles/avatars_controller.rb index f193adb46b4..daa51ae41df 100644 --- a/app/controllers/profiles/avatars_controller.rb +++ b/app/controllers/profiles/avatars_controller.rb @@ -4,7 +4,6 @@ class Profiles::AvatarsController < Profiles::ApplicationController @user.remove_avatar! @user.save - @user.reset_events_cache redirect_to profile_path end diff --git a/app/controllers/profiles/chat_names_controller.rb b/app/controllers/profiles/chat_names_controller.rb new file mode 100644 index 00000000000..6a1f468ba5a --- /dev/null +++ b/app/controllers/profiles/chat_names_controller.rb @@ -0,0 +1,64 @@ +class Profiles::ChatNamesController < Profiles::ApplicationController + before_action :chat_name_token, only: [:new] + before_action :chat_name_params, only: [:new, :create, :deny] + + def index + @chat_names = current_user.chat_names + end + + def new + end + + def create + new_chat_name = current_user.chat_names.new(chat_name_params) + + if new_chat_name.save + flash[:notice] = "Authorized #{new_chat_name.chat_name}" + else + flash[:alert] = "Could not authorize chat nickname. Try again!" + end + + delete_chat_name_token + redirect_to profile_chat_names_path + end + + def deny + delete_chat_name_token + + flash[:notice] = "Denied authorization of chat nickname #{chat_name_params[:user_name]}." + + redirect_to profile_chat_names_path + end + + def destroy + @chat_name = chat_names.find(params[:id]) + + if @chat_name.destroy + flash[:notice] = "Deleted chat nickname: #{@chat_name.chat_name}!" + else + flash[:alert] = "Could not delete chat nickname #{@chat_name.chat_name}." + end + + redirect_to profile_chat_names_path + end + + private + + def delete_chat_name_token + chat_name_token.delete + end + + def chat_name_params + @chat_name_params ||= chat_name_token.get || render_404 + end + + def chat_name_token + return render_404 unless params[:token] || render_404 + + @chat_name_token ||= Gitlab::ChatNameToken.new(params[:token]) + end + + def chat_names + @chat_names ||= current_user.chat_names + end +end diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index 508b82a9a6c..6e007f17913 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -1,8 +1,6 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController - before_action :load_personal_access_tokens, only: :index - def index - @personal_access_token = current_user.personal_access_tokens.build + set_index_vars end def create @@ -12,7 +10,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController flash[:personal_access_token] = @personal_access_token.token redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created." else - load_personal_access_tokens + set_index_vars render :index end end @@ -32,10 +30,12 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController private def personal_access_token_params - params.require(:personal_access_token).permit(:name, :expires_at) + params.require(:personal_access_token).permit(:name, :expires_at, scopes: []) end - def load_personal_access_tokens + def set_index_vars + @personal_access_token ||= current_user.personal_access_tokens.build + @scopes = Gitlab::Auth::SCOPES @active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at) @inactive_personal_access_tokens = current_user.personal_access_tokens.inactive end diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 9eb75bb3891..18044ca78e2 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -22,6 +22,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController end @qr_code = build_qr_code + @account_string = account_string setup_u2f_registration end @@ -78,11 +79,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController private def build_qr_code - issuer = "#{issuer_host} | #{current_user.email}" - uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer) + uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host) RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3) end + def account_string + "#{issuer_host}:#{current_user.email}" + end + def issuer_host Gitlab.config.gitlab.host end diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb new file mode 100644 index 00000000000..d9dfa534669 --- /dev/null +++ b/app/controllers/projects/autocomplete_sources_controller.rb @@ -0,0 +1,48 @@ +class Projects::AutocompleteSourcesController < Projects::ApplicationController + before_action :load_autocomplete_service, except: [:emojis, :members] + + def emojis + render json: Gitlab::AwardEmoji.urls + end + + def members + render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable) + end + + def issues + render json: @autocomplete_service.issues + end + + def merge_requests + render json: @autocomplete_service.merge_requests + end + + def labels + render json: @autocomplete_service.labels + end + + def milestones + render json: @autocomplete_service.milestones + end + + def commands + render json: @autocomplete_service.commands(noteable, params[:type]) + end + + private + + def load_autocomplete_service + @autocomplete_service = ::Projects::AutocompleteService.new(@project, current_user) + end + + def noteable + case params[:type] + when 'Issue' + IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id]) + when 'MergeRequest' + MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id]) + when 'Commit' + @project.commit(params[:type_id]) + end + end +end diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index ada7db3c552..53788687076 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -20,7 +20,6 @@ class Projects::AvatarsController < Projects::ApplicationController @project.remove_avatar! @project.save - @project.reset_events_cache redirect_to edit_project_path(@project) end diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb index f576d0be1fc..863a766a255 100644 --- a/app/controllers/projects/blame_controller.rb +++ b/app/controllers/projects/blame_controller.rb @@ -8,6 +8,9 @@ class Projects::BlameController < Projects::ApplicationController def show @blob = @repository.blob_at(@commit.id, @path) + + return render_404 unless @blob + @blame_groups = Gitlab::Blame.new(@blob, @commit).groups end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index b78cc6585ba..9940263ae24 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -13,7 +13,6 @@ class Projects::BlobController < Projects::ApplicationController before_action :assign_blob_vars before_action :commit, except: [:new, :create] before_action :blob, except: [:new, :create] - before_action :from_merge_request, only: [:edit, :update] before_action :require_branch_head, only: [:edit, :update] before_action :editor_variables, except: [:show, :preview, :diff] before_action :validate_diff_params, only: :diff @@ -39,14 +38,6 @@ class Projects::BlobController < Projects::ApplicationController def update @path = params[:file_path] if params[:file_path].present? - after_edit_path = - if from_merge_request && @target_branch == @ref - diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + - "#file-path-#{hexdigest(@path)}" - else - namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) - end - create_commit(Files::UpdateService, success_path: after_edit_path, failure_view: :edit, failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) @@ -124,9 +115,14 @@ class Projects::BlobController < Projects::ApplicationController render_404 end - def from_merge_request - # If blob edit was initiated from merge request page - @from_merge_request ||= MergeRequest.find_by(id: params[:from_merge_request_id]) + def after_edit_path + from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid]) + if from_merge_request && @target_branch == @ref + diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + + "##{hexdigest(@path)}" + else + namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) + end end def editor_variables diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 2de8ada3e29..89d84809e3a 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -4,7 +4,7 @@ class Projects::BranchesController < Projects::ApplicationController # Authorize before_action :require_non_empty_project before_action :authorize_download_code! - before_action :authorize_push_code!, only: [:new, :create, :destroy] + before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged] def index @sort = params[:sort].presence || sort_value_name @@ -36,7 +36,7 @@ class Projects::BranchesController < Projects::ApplicationController execute(branch_name, ref) if params[:issue_iid] - issue = @project.issues.find_by(iid: params[:issue_iid]) + issue = IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:issue_iid]) SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue end @@ -62,6 +62,13 @@ class Projects::BranchesController < Projects::ApplicationController end end + def destroy_all_merged + DeleteMergedBranchesService.new(@project, current_user).async_execute + + redirect_to namespace_project_branches_path(@project.namespace, @project), + notice: 'Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes.' + end + private def ref 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 cdfc1ba7b92..bfc59bcc862 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -8,13 +8,10 @@ class Projects::CommitController < Projects::ApplicationController # Authorize before_action :require_non_empty_project - before_action :authorize_download_code!, except: [:cancel_builds, :retry_builds] - before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds] + before_action :authorize_download_code! before_action :authorize_read_pipeline!, only: [:pipelines] - before_action :authorize_read_commit_status!, only: [:builds] before_action :commit - before_action :define_commit_vars, only: [:show, :diff_for_path, :builds, :pipelines] - before_action :define_status_vars, only: [:show, :builds, :pipelines] + before_action :define_commit_vars, only: [:show, :diff_for_path, :pipelines] before_action :define_note_vars, only: [:show, :diff_for_path] before_action :authorize_edit_tree!, only: [:revert, :cherry_pick] @@ -35,25 +32,6 @@ class Projects::CommitController < Projects::ApplicationController def pipelines end - def builds - end - - def cancel_builds - ci_builds.running_or_pending.each(&:cancel) - - redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha) - end - - def retry_builds - ci_builds.latest.failed.each do |build| - if build.retryable? - Ci::Build.retry(build, current_user) - end - end - - redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha) - end - def branches @branches = @project.repository.branch_names_contains(commit.id) @tags = @project.repository.tag_names_contains(commit.id) @@ -65,7 +43,7 @@ class Projects::CommitController < Projects::ApplicationController return render_404 if @target_branch.blank? - create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.", + create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.", success_path: successful_change_path, failure_path: failed_change_path) end @@ -74,36 +52,30 @@ class Projects::CommitController < Projects::ApplicationController return render_404 if @target_branch.blank? - create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.", + create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.", success_path: successful_change_path, failure_path: failed_change_path) end private def successful_change_path - return referenced_merge_request_url if @commit.merged_merge_request - - namespace_project_commits_url(@project.namespace, @project, @target_branch) + referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @target_branch) end def failed_change_path - return referenced_merge_request_url if @commit.merged_merge_request - - namespace_project_commit_url(@project.namespace, @project, params[:id]) + referenced_merge_request_url || namespace_project_commit_url(@project.namespace, @project, params[:id]) end def referenced_merge_request_url - namespace_project_merge_request_url(@project.namespace, @project, @commit.merged_merge_request) + if merge_request = @commit.merged_merge_request(current_user) + namespace_project_merge_request_url(@project.namespace, @project, merge_request) + end end def commit @noteable = @commit ||= @project.commit(params[:id]) end - def ci_builds - @ci_builds ||= Ci::Build.where(pipeline: pipelines) - end - def define_commit_vars return git_not_found! unless commit @@ -133,12 +105,6 @@ class Projects::CommitController < Projects::ApplicationController } end - def define_status_vars - @ci_pipelines = project.pipelines.where(sha: commit.sha) - @statuses = CommitStatus.where(pipeline: @ci_pipelines).relevant - @builds = Ci::Build.where(pipeline: @ci_pipelines).relevant - 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/commits_controller.rb b/app/controllers/projects/commits_controller.rb index aba87b6144b..ad92f05a42d 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -21,7 +21,7 @@ class Projects::CommitsController < Projects::ApplicationController @note_counts = project.notes.where(commit_id: @commits.map(&:id)). group(:commit_id).count - @merge_request = @project.merge_requests.opened. + @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened. find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) respond_to do |format| diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index bee3d56076c..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 @@ -53,7 +62,7 @@ class Projects::CompareController < Projects::ApplicationController end def merge_request - @merge_request ||= @project.merge_requests.opened. + @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened. find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) end end diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb new file mode 100644 index 00000000000..b69d46f2c41 --- /dev/null +++ b/app/controllers/projects/cycle_analytics/events_controller.rb @@ -0,0 +1,61 @@ +module Projects + module CycleAnalytics + class EventsController < Projects::ApplicationController + include CycleAnalyticsParams + + before_action :authorize_read_cycle_analytics! + before_action :authorize_read_build!, only: [:test, :staging] + before_action :authorize_read_issue!, only: [:issue, :production] + before_action :authorize_read_merge_request!, only: [:code, :review] + + def issue + render_events(cycle_analytics[:issue].events) + end + + def plan + render_events(cycle_analytics[:plan].events) + end + + def code + render_events(cycle_analytics[:code].events) + end + + def test + options(events_params)[:branch] = events_params[:branch_name] + + render_events(cycle_analytics[:test].events) + end + + def review + render_events(cycle_analytics[:review].events) + end + + def staging + render_events(cycle_analytics[:staging].events) + end + + def production + render_events(cycle_analytics[:production].events) + end + + private + + def render_events(events) + respond_to do |format| + format.html + format.json { render json: { events: events } } + end + end + + def cycle_analytics + @cycle_analytics ||= ::CycleAnalytics.new(project, options(events_params)) + end + + def events_params + return {} unless params[:events].present? + + params[:events].permit(:start_date, :branch_name) + end + end + end +end diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index 16a7b1fc6e2..88ac3ad046b 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -1,11 +1,14 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController include ActionView::Helpers::DateHelper include ActionView::Helpers::TextHelper + include CycleAnalyticsParams before_action :authorize_read_cycle_analytics! def show - @cycle_analytics = CycleAnalytics.new(@project, from: parse_start_date) + @cycle_analytics = ::CycleAnalytics.new(@project, options(cycle_analytics_params)) + + @cycle_analytics_no_data = @cycle_analytics.no_stats? respond_to do |format| format.html @@ -15,53 +18,17 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController private - def parse_start_date - case cycle_analytics_params[:start_date] - when '30' then 30.days.ago - when '90' then 90.days.ago - else 90.days.ago - end - end - 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 cycle_analytics_json - cycle_analytics_view_data = [[:issue, "Issue", "Time before an issue gets scheduled"], - [:plan, "Plan", "Time before an issue starts implementation"], - [:code, "Code", "Time until first merge request"], - [:test, "Test", "Total test time for all commits/merges"], - [:review, "Review", "Time between merge request creation and merge/close"], - [:staging, "Staging", "From merge request merge until deploy to production"], - [:production, "Production", "From issue creation until deploy to production"]] - - stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)| - value = @cycle_analytics.send(stage_method).presence - - stats << { - title: stage_text, - description: stage_description, - 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 } - ] - { - summary: summary, - stats: stats + summary: @cycle_analytics.summary, + stats: @cycle_analytics.stats, + permissions: @cycle_analytics.permissions(user: current_user) } end end diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 529e0aa2d33..b094491e006 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -16,7 +16,7 @@ class Projects::DeployKeysController < Projects::ApplicationController end def create - @key = DeployKey.new(deploy_key_params) + @key = DeployKey.new(deploy_key_params.merge(user: current_user)) set_index_vars if @key.valid? && @project.deploy_keys << @key @@ -53,6 +53,6 @@ class Projects::DeployKeysController < Projects::ApplicationController end def deploy_key_params - params.require(:deploy_key).permit(:key, :title) + params.require(:deploy_key).permit(:key, :title, :can_push) end end diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb index d174e1145a7..1349b015a63 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -5,9 +5,7 @@ class Projects::DiscussionsController < Projects::ApplicationController before_action :authorize_resolve_discussion! def resolve - discussion.resolve!(current_user) - - MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request) + Discussions::ResolveService.new(project, current_user, merge_request: merge_request).execute(discussion) render json: { resolved_by: discussion.resolved_by.try(:name), @@ -26,7 +24,7 @@ class Projects::DiscussionsController < Projects::ApplicationController private def merge_request - @merge_request ||= @project.merge_requests.find_by!(iid: params[:merge_request_id]) + @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).find_by!(iid: params[:merge_request_id]) end def discussion diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index bc66823dfc4..a203efc62b8 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -4,17 +4,22 @@ class Projects::EnvironmentsController < Projects::ApplicationController before_action :authorize_create_environment!, only: [:new, :create] before_action :authorize_create_deployment!, only: [:stop] before_action :authorize_update_environment!, only: [:edit, :update] - before_action :environment, only: [:show, :edit, :update, :stop] + before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize] + before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize] + before_action :verify_api_request!, only: :terminal_websocket_authorize def index @scope = params[:scope] - @all_environments = project.environments - @environments = - if @scope == 'stopped' - @all_environments.stopped - else - @all_environments.available + @environments = project.environments + + respond_to do |format| + format.html + format.json do + render json: EnvironmentSerializer + .new(project: @project, user: current_user) + .represent(@environments) end + end end def show @@ -57,8 +62,33 @@ class Projects::EnvironmentsController < Projects::ApplicationController end end + def terminal + # Currently, this acts as a hint to load the terminal details into the cache + # if they aren't there already. In the future, users will need these details + # to choose between terminals to connect to. + @terminals = environment.terminals + end + + # GET .../terminal.ws : implemented in gitlab-workhorse + def terminal_websocket_authorize + # Just return the first terminal for now. If the list is in the process of + # being looked up, this may result in a 404 response, so the frontend + # should retry those errors + terminal = environment.terminals.try(:first) + if terminal + set_workhorse_internal_api_content_type + render json: Gitlab::Workhorse.terminal_websocket(terminal) + else + render text: 'Not found', status: 404 + end + end + private + def verify_api_request! + Gitlab::Workhorse.verify_api_request!(request.headers) + end + def environment_params params.require(:environment).permit(:name, :external_url) end diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index ade01c706a7..ba46e2528e6 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -4,6 +4,7 @@ class Projects::ForksController < Projects::ApplicationController # Authorize before_action :require_non_empty_project before_action :authorize_download_code! + before_action :authenticate_user!, only: [:new, :create] def index base_query = project.forks.includes(:creator) diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 3f41916e6d3..8714349e27f 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -18,6 +18,14 @@ class Projects::GitHttpClientController < Projects::ApplicationController private + def download_request? + raise NotImplementedError + end + + def upload_request? + raise NotImplementedError + end + def authenticate_user @authentication_result = Gitlab::Auth::Result.new @@ -130,10 +138,6 @@ class Projects::GitHttpClientController < Projects::ApplicationController authentication_result.ci?(project) end - def lfs_deploy_token? - authentication_result.lfs_deploy_token?(project) - end - def authentication_has_download_access? has_authentication_ability?(:download_code) || has_authentication_ability?(:build_download_code) end @@ -149,8 +153,4 @@ class Projects::GitHttpClientController < Projects::ApplicationController def authentication_project authentication_result.project end - - def verify_workhorse_api! - Gitlab::Workhorse.verify_api_request!(request.headers) - end end diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 13caeb42d40..9184dcccac5 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -1,7 +1,5 @@ -# This file should be identical in GitLab Community Edition and Enterprise Edition - class Projects::GitHttpController < Projects::GitHttpClientController - before_action :verify_workhorse_api! + include WorkhorseRequest # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull) # GET /foo/bar.git/info/refs?service=git-receive-pack (git push) @@ -67,14 +65,18 @@ class Projects::GitHttpController < Projects::GitHttpClientController end def render_denied - if user && user.can?(:read_project, project) - render plain: 'Access denied', status: :forbidden + if user && can?(user, :read_project, project) + render plain: access_denied_message, status: :forbidden else # Do not leak information about project existence render_not_found end end + def access_denied_message + 'Access denied' + end + def upload_pack_allowed? return false unless Gitlab.config.gitlab_shell.upload_pack diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index 9eaf26a0dbf..66b7bdbd988 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -4,10 +4,7 @@ class Projects::GroupLinksController < Projects::ApplicationController before_action :authorize_admin_project_member!, only: [:update] def index - @group_links = project.project_group_links.all - - @skip_groups = @group_links.pluck(:group_id) - @skip_groups << project.namespace_id unless project.personal? + redirect_to namespace_project_settings_members_path end def create @@ -25,7 +22,7 @@ class Projects::GroupLinksController < Projects::ApplicationController flash[:alert] = 'Please select a group.' end - redirect_to namespace_project_group_links_path(project.namespace, project) + redirect_to namespace_project_settings_members_path(project.namespace, project) end def update @@ -39,7 +36,7 @@ class Projects::GroupLinksController < Projects::ApplicationController respond_to do |format| format.html do - redirect_to namespace_project_group_links_path(project.namespace, project) + redirect_to namespace_project_settings_members_path(project.namespace, project) end format.js { head :ok } end diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 0ae8ff98009..b668a9331e7 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -6,21 +6,15 @@ class Projects::HooksController < Projects::ApplicationController layout "project_settings" - def index - @hooks = @project.hooks - @hook = ProjectHook.new - end - def create @hook = @project.hooks.new(hook_params) @hook.save - if @hook.valid? - redirect_to namespace_project_hooks_path(@project.namespace, @project) - else + unless @hook.valid? @hooks = @project.hooks.select(&:persisted?) - render :index + flash[:alert] = @hook.errors.full_messages.join.html_safe end + redirect_to namespace_project_settings_integrations_path(@project.namespace, @project) end def test @@ -44,7 +38,7 @@ class Projects::HooksController < Projects::ApplicationController def destroy hook.destroy - redirect_to namespace_project_hooks_path(@project.namespace, @project) + redirect_to namespace_project_settings_integrations_path(@project.namespace, @project) end private diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 3f1a1d1c511..8472ceca329 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -25,11 +25,26 @@ class Projects::IssuesController < Projects::ApplicationController def index @issues = issues_collection @issues = @issues.page(params[:page]) + if @issues.out_of_range? && @issues.total_pages != 0 + return redirect_to url_for(params.merge(page: @issues.total_pages)) + end if params[:label_name].present? @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute end + @users = [] + + if params[:assignee_id].present? + assignee = User.find_by_id(params[:assignee_id]) + @users.push(assignee) if assignee + end + + if params[:author_id].present? + author = User.find_by_id(params[:author_id]) + @users.push(author) if author + end + respond_to do |format| format.html format.atom { render layout: false } @@ -46,8 +61,9 @@ class Projects::IssuesController < Projects::ApplicationController params[:issue] ||= ActionController::Parameters.new( assignee_id: "" ) + build_params = issue_params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions) + @issue = @noteable = Issues::BuildService.new(project, current_user, build_params).execute - @issue = @noteable = @project.issues.new(issue_params) respond_with(@issue) end @@ -69,13 +85,15 @@ class Projects::IssuesController < Projects::ApplicationController respond_to do |format| format.html format.json do - render json: @issue.to_json(include: [:milestone, :labels]) + render json: IssueSerializer.new.represent(@issue) end end end def create - @issue = Issues::CreateService.new(project, current_user, issue_params.merge(request: request)).execute + extra_params = { request: request, + merge_request_for_resolving_discussions: merge_request_for_resolving_discussions } + @issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute respond_to do |format| format.html do @@ -169,6 +187,14 @@ class Projects::IssuesController < Projects::ApplicationController alias_method :awardable, :issue alias_method :spammable, :issue + def merge_request_for_resolving_discussions + return unless merge_request_iid = params[:merge_request_for_resolving_discussions] + + @merge_request_for_resolving_discussions ||= MergeRequestsFinder.new(current_user, project_id: project.id). + execute. + find_by(iid: merge_request_iid) + end + def authorize_read_issue! return render_404 unless can?(current_user, :read_issue, @issue) end diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 42fd09e9b7e..824ed7be73e 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -3,7 +3,7 @@ class Projects::LabelsController < Projects::ApplicationController before_action :module_enabled before_action :label, only: [:edit, :update, :destroy] - before_action :find_labels, only: [:index, :set_priorities, :remove_priority] + before_action :find_labels, only: [:index, :set_priorities, :remove_priority, :toggle_subscription] before_action :authorize_read_label! before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :generate, :destroy, :remove_priority, @@ -123,7 +123,10 @@ class Projects::LabelsController < Projects::ApplicationController def label @label ||= @project.labels.find(params[:id]) end - alias_method :subscribable_resource, :label + + def subscribable_resource + @available_labels.find(params[:id]) + end def find_labels @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb index ece49dcd922..440259b643c 100644 --- a/app/controllers/projects/lfs_api_controller.rb +++ b/app/controllers/projects/lfs_api_controller.rb @@ -1,8 +1,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController - include LfsHelper + include LfsRequest - before_action :require_lfs_enabled! - before_action :lfs_check_access!, except: [:deprecated] + skip_before_action :lfs_check_access!, only: [:deprecated] def batch unless objects.present? @@ -31,8 +30,12 @@ class Projects::LfsApiController < Projects::GitHttpClientController private - def objects - @objects ||= (params[:objects] || []).to_a + def download_request? + params[:operation] == 'download' + end + + def upload_request? + params[:operation] == 'upload' end def existing_oids @@ -83,12 +86,4 @@ class Projects::LfsApiController < Projects::GitHttpClientController } } end - - def download_request? - params[:operation] == 'download' - end - - def upload_request? - params[:operation] == 'upload' - end end diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb index 9005b104e90..32759672b6c 100644 --- a/app/controllers/projects/lfs_storage_controller.rb +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -1,9 +1,8 @@ class Projects::LfsStorageController < Projects::GitHttpClientController - include LfsHelper + include LfsRequest + include WorkhorseRequest - before_action :require_lfs_enabled! - before_action :lfs_check_access! - before_action :verify_workhorse_api!, only: [:upload_authorize] + skip_before_action :verify_workhorse_api!, only: [:download, :upload_finalize] def download lfs_object = LfsObject.find_by_oid(oid) diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb new file mode 100644 index 00000000000..01d99c7df35 --- /dev/null +++ b/app/controllers/projects/mattermosts_controller.rb @@ -0,0 +1,43 @@ +class Projects::MattermostsController < Projects::ApplicationController + include TriggersHelper + include ActionView::Helpers::AssetUrlHelper + + layout 'project_settings' + + before_action :authorize_admin_project! + before_action :service + before_action :teams, only: [:new] + + def new + end + + def create + result, message = @service.configure(current_user, configure_params) + + if result + flash[:notice] = 'This service is now configured' + redirect_to edit_namespace_project_service_path( + @project.namespace, @project, service) + else + flash[:alert] = message || 'Failed to configure service' + redirect_to new_namespace_project_mattermost_path( + @project.namespace, @project) + end + end + + private + + def configure_params + params.require(:mattermost).permit(:trigger, :team_id).merge( + url: service_trigger_url(@service), + icon_url: asset_url('slash-command-logo.png')) + end + + def teams + @teams ||= @service.list_teams(current_user) + end + + def service + @service ||= @project.find_or_initialize_service('mattermost_slash_commands') + end +end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index ccba37c9c5c..bdf8d2ce628 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -9,10 +9,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ - :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines, :merge, :merge_check, + :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check, :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues ] - before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines] + before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines] before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines] before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check] before_action :define_commit_vars, only: [:diffs] @@ -38,7 +38,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController def index @merge_requests = merge_requests_collection @merge_requests = @merge_requests.page(params[:page]) - @merge_requests = @merge_requests.preload(:target_project) + if @merge_requests.out_of_range? && @merge_requests.total_pages != 0 + return redirect_to url_for(params.merge(page: @merge_requests.total_pages)) + end if params[:label_name].present? labels_params = { project_id: @project.id, title: params[:label_name] } @@ -61,7 +63,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController format.html { define_discussion_vars } format.json do - render json: @merge_request + render json: MergeRequestSerializer.new.represent(@merge_request) end format.patch do @@ -83,12 +85,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request_diff = if params[:diff_id] - @merge_request.merge_request_diffs.find(params[:diff_id]) + @merge_request.merge_request_diffs.viewable.find(params[:diff_id]) else @merge_request.merge_request_diff end - @merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff + @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } if params[:start_sha].present? @@ -96,7 +98,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha } unless @start_version - render_404 + @start_sha = @merge_request_diff.head_commit_sha + @start_version = @merge_request_diff end end @@ -202,17 +205,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end - def builds - respond_to do |format| - format.html do - define_discussion_vars - - render 'show' - end - format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_builds') } } - end - end - def pipelines @pipelines = @merge_request.all_pipelines @@ -303,9 +295,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def cancel_merge_when_build_succeeds - return access_denied! unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user) + unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user) + return access_denied! + end - MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user).cancel(@merge_request) + MergeRequests::MergeWhenPipelineSucceedsService + .new(@project, current_user) + .cancel(@merge_request) end def merge @@ -326,16 +322,18 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.update(merge_error: nil) if params[:merge_when_build_succeeds].present? - unless @merge_request.pipeline + unless @merge_request.head_pipeline @status = :failed return end - if @merge_request.pipeline.active? - MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) - .execute(@merge_request) + if @merge_request.head_pipeline.active? + MergeRequests::MergeWhenPipelineSucceedsService + .new(@project, current_user, merge_params) + .execute(@merge_request) + @status = :merge_when_build_succeeds - elsif @merge_request.pipeline.success? + elsif @merge_request.head_pipeline.success? # This can be triggered when a user clicks the auto merge button while # the tests finish at about the same time MergeWorker.perform_async(@merge_request.id, current_user.id, params) @@ -349,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 @@ -399,7 +407,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def ci_status - pipeline = @merge_request.pipeline + pipeline = @merge_request.head_pipeline + if pipeline status = pipeline.status coverage = pipeline.try(:coverage) @@ -410,15 +419,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController else ci_service = @merge_request.source_project.try(:ci_service) status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service - - if ci_service.respond_to?(:commit_coverage) - coverage = ci_service.commit_coverage(merge_request.diff_head_sha, merge_request.source_branch) - end end response = { title: merge_request.title, - sha: merge_request.diff_head_commit.short_id, + sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha), status: status, coverage: coverage } @@ -492,7 +497,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def validates_merge_request # Show git not found page # if there is no saved commits between source & target branch - if @merge_request.commits.blank? + if @merge_request.has_no_commits? # and if target branch doesn't exist return invalid_mr unless @merge_request.target_branch_exists? end @@ -500,13 +505,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_show_vars @noteable = @merge_request - @commits_count = @merge_request.commits.count + @commits_count = @merge_request.commits_count if @merge_request.locked_long_ago? @merge_request.unlock_mr @merge_request.close end + labels define_pipelines_vars end @@ -534,7 +540,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def define_widget_vars - @pipeline = @merge_request.pipeline + @pipeline = @merge_request.head_pipeline end def define_commit_vars @@ -563,11 +569,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_pipelines_vars @pipelines = @merge_request.all_pipelines - - if @pipelines.present? - @pipeline = @pipelines.first - @statuses = @pipeline.statuses.relevant - end + @pipeline = @merge_request.head_pipeline + @statuses_count = @pipeline.present? ? @pipeline.statuses.relevant.count : 0 end def define_new_vars @@ -634,7 +637,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def merge_when_build_succeeds_active? params[:merge_when_build_succeeds].present? && - @merge_request.pipeline && @merge_request.pipeline.active? + @merge_request.head_pipeline && @merge_request.head_pipeline.active? end def build_merge_request diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index ff63f22cb5b..be52b0fa7cf 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -112,6 +112,6 @@ class Projects::MilestonesController < Projects::ApplicationController end def milestone_params - params.require(:milestone).permit(:title, :description, :due_date, :state_event) + params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event) end end diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 0948ad21649..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) @@ -146,24 +147,26 @@ class Projects::NotesController < Projects::ApplicationController end def note_json(note) + attrs = { + award: false, + id: note.id + } + if note.is_a?(AwardEmoji) - { + attrs.merge!( valid: note.valid?, award: true, - id: note.id, name: note.name - } + ) elsif note.persisted? Banzai::NoteRenderer.render([note], @project, current_user) - attrs = { + attrs.merge!( valid: true, - id: note.id, discussion_id: note.discussion_id, html: note_html(note), - award: false, note: note.note - } + ) if note.diff_note? discussion = note.to_discussion @@ -188,15 +191,15 @@ class Projects::NotesController < Projects::ApplicationController attrs[:original_discussion_id] = note.original_discussion_id end end - - attrs else - { + attrs.merge!( valid: false, - award: false, errors: note.errors - } + ) end + + attrs[:commands_changes] = note.commands_changes unless attrs[:award] + attrs end def authorize_admin_note! @@ -215,6 +218,6 @@ class Projects::NotesController < Projects::ApplicationController end def find_current_user_notes - @notes = NotesFinder.new.execute(project, current_user, params) + @notes = NotesFinder.new(project, current_user, params).execute.inc_author end end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 371cc3787fb..84451257b98 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -1,16 +1,39 @@ class Projects::PipelinesController < Projects::ApplicationController before_action :pipeline, except: [:index, :new, :create] - before_action :commit, only: [:show] + before_action :commit, only: [:show, :builds] before_action :authorize_read_pipeline! before_action :authorize_create_pipeline!, only: [:new, :create] before_action :authorize_update_pipeline!, only: [:retry, :cancel] def index @scope = params[:scope] - @pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30) + @pipelines = PipelinesFinder + .new(project) + .execute(scope: @scope) + .page(params[:page]) + .per(30) - @running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count - @pipelines_count = PipelinesFinder.new(project).execute.count + @running_or_pending_count = PipelinesFinder + .new(project).execute(scope: 'running').count + + @pipelines_count = PipelinesFinder + .new(project).execute.count + + respond_to do |format| + format.html + format.json do + render json: { + pipelines: PipelineSerializer + .new(project: @project, user: @current_user) + .with_pagination(request, response) + .represent(@pipelines), + count: { + all: @pipelines_count, + running_or_pending: @running_or_pending_count + } + } + end + end end def new @@ -18,7 +41,9 @@ class Projects::PipelinesController < Projects::ApplicationController end def create - @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute(ignore_skip_ci: true, save_on_errors: false) + @pipeline = Ci::CreatePipelineService + .new(project, current_user, create_params) + .execute(ignore_skip_ci: true, save_on_errors: false) unless @pipeline.persisted? render 'new' return @@ -30,6 +55,23 @@ class Projects::PipelinesController < Projects::ApplicationController def show end + def builds + respond_to do |format| + format.html do + render 'show' + end + end + end + + def stage + @stage = pipeline.stage(params[:stage]) + return not_found unless @stage + + respond_to do |format| + format.json { render json: { html: view_to_html_string('projects/pipelines/_stage') } } + end + end + def retry pipeline.retry_failed(current_user) diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb index 9136633b87a..53ce23221ed 100644 --- a/app/controllers/projects/pipelines_settings_controller.rb +++ b/app/controllers/projects/pipelines_settings_controller.rb @@ -17,7 +17,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController flash[:notice] = "CI/CD Pipelines settings for '#{@project.name}' were successfully updated." redirect_to namespace_project_pipelines_settings_path(@project.namespace, @project) else - render 'index' + render 'show' end end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 699a56ae2f8..6e158e685e9 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -1,33 +1,19 @@ class Projects::ProjectMembersController < Projects::ApplicationController include MembershipActions + include SortingHelper # Authorize before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access] def index - @group_links = @project.project_group_links - - @project_members = @project.project_members - @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) - - if params[:search].present? - users = @project.users.search(params[:search]).to_a - @project_members = @project_members.where(user_id: users) - - @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) - end - - @project_members = @project_members.order(access_level: :desc).page(params[:page]) - - @requesters = AccessRequestsFinder.new(@project).execute(current_user) - - @project_member = @project.project_members.new + sort = params[:sort].presence || sort_value_name + redirect_to namespace_project_settings_members_path(@project.namespace, @project, sort: sort) end def create status = Members::CreateService.new(@project, current_user, params).execute - redirect_url = namespace_project_project_members_path(@project.namespace, @project) + redirect_url = namespace_project_settings_members_path(@project.namespace, @project) if status redirect_to redirect_url, notice: 'Users were successfully added.' @@ -50,14 +36,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController respond_to do |format| format.html do - redirect_to namespace_project_project_members_path(@project.namespace, @project) + redirect_to namespace_project_settings_members_path(@project.namespace, @project) end format.js { head :ok } end end def resend_invite - redirect_path = namespace_project_project_members_path(@project.namespace, @project) + redirect_path = namespace_project_settings_members_path(@project.namespace, @project) @project_member = @project.project_members.find(params[:id]) @@ -80,7 +66,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController return render_404 end - redirect_to(namespace_project_project_members_path(project.namespace, project), + redirect_to(namespace_project_settings_members_path(project.namespace, project), notice: notice) end diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index 0825a4311cb..2c097cb4d8d 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -10,7 +10,14 @@ class Projects::ReleasesController < Projects::ApplicationController end def update - release.update_attributes(release_params) + # Release belongs to Tag which is not active record object, + # it exists only to save a description to each Tag. + # If description is empty we should destroy the existing record. + if release_params[:description].present? + release.update_attributes(release_params) + else + release.destroy + end redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name) end diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 97e6e9471e0..17cb1d5be24 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -9,11 +9,6 @@ class Projects::ServicesController < Projects::ApplicationController layout "project_settings" - def index - @project.build_missing_services - @services = @project.services.visible.reload - end - def edit end @@ -29,6 +24,8 @@ class Projects::ServicesController < Projects::ApplicationController end def test + return render_404 unless @service.can_test? + data = @service.test_data(project, current_user) outcome = @service.test(data) @@ -46,6 +43,6 @@ class Projects::ServicesController < Projects::ApplicationController private def service - @service ||= @project.services.find { |service| service.to_param == params[:id] } + @service ||= @project.find_or_initialize_service(params[:id]) end end diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb new file mode 100644 index 00000000000..fb2a4837735 --- /dev/null +++ b/app/controllers/projects/settings/integrations_controller.rb @@ -0,0 +1,18 @@ +module Projects + module Settings + class IntegrationsController < Projects::ApplicationController + include ServiceParams + + before_action :authorize_admin_project! + layout "project_settings" + + def show + @hooks = @project.hooks + @hook = ProjectHook.new + + # Services + @services = @project.find_or_initialize_services + end + end + end +end diff --git a/app/controllers/projects/settings/members_controller.rb b/app/controllers/projects/settings/members_controller.rb new file mode 100644 index 00000000000..5735e281f66 --- /dev/null +++ b/app/controllers/projects/settings/members_controller.rb @@ -0,0 +1,55 @@ +module Projects + module Settings + class MembersController < Projects::ApplicationController + include SortingHelper + + def show + @sort = params[:sort].presence || sort_value_name + @group_links = @project.project_group_links + + @project_members = @project.project_members + @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) + + group = @project.group + + # group links + @group_links = @project.project_group_links.all + + @skip_groups = @group_links.pluck(:group_id) + @skip_groups << @project.namespace_id unless @project.personal? + + if group + # We need `.where.not(user_id: nil)` here otherwise when a group has an + # invitee, it would make the following query return 0 rows since a NULL + # user_id would be present in the subquery + # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values + group_members = MembersFinder.new(@project_members, group).execute(current_user) + end + + if params[:search].present? + user_ids = @project.users.search(params[:search]).select(:id) + @project_members = @project_members.where(user_id: user_ids) + + if group_members + user_ids = group.users.search(params[:search]).select(:id) + group_members = group_members.where(user_id: user_ids) + end + + @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) + end + + wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"] + wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members + + @project_members = Member. + where(wheres.join(' OR ')). + sort(@sort). + page(params[:page]) + + @requesters = AccessRequestsFinder.new(@project).execute(current_user) + + @project_member = @project.project_members.new + end + end + end +end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index e290a0eadda..02a97c1c574 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -19,11 +19,16 @@ class Projects::SnippetsController < Projects::ApplicationController respond_to :html def index - @snippets = SnippetsFinder.new.execute(current_user, { + @snippets = SnippetsFinder.new.execute( + current_user, filter: :by_project, - project: @project - }) + project: @project, + scope: params[:scope] + ) @snippets = @snippets.page(params[:page]) + if @snippets.out_of_range? && @snippets.total_pages != 0 + redirect_to namespace_project_snippets_path(page: @snippets.total_pages) + end end def new diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 953091492ae..e2d9d5ed460 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -8,7 +8,7 @@ class Projects::TagsController < Projects::ApplicationController before_action :authorize_admin_project!, only: [:destroy] def index - params[:sort] = params[:sort].presence || 'name' + params[:sort] = params[:sort].presence || sort_value_recently_updated @sort = params[:sort] @tags = TagsFinder.new(@repository, params).execute diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb index 5685d0f4e7c..a41fcb85c40 100644 --- a/app/controllers/projects/todos_controller.rb +++ b/app/controllers/projects/todos_controller.rb @@ -16,15 +16,9 @@ class Projects::TodosController < Projects::ApplicationController @issuable ||= begin case params[:issuable_type] when "issue" - issue = @project.issues.find(params[:issuable_id]) - - if can?(current_user, :read_issue, issue) - issue - else - render_404 - end + IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id]) when "merge_request" - @project.merge_requests.find(params[:issuable_id]) + MergeRequestsFinder.new(current_user, project_id: @project.id).find(params[:issuable_id]) end end end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 177ccf5eec9..c3353446fd1 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -115,6 +115,8 @@ class Projects::WikisController < Projects::ApplicationController # Call #wiki to make sure the Wiki Repo is initialized @project_wiki.wiki + + @sidebar_wiki_pages = @project_wiki.pages.first(15) rescue ProjectWiki::CouldNotCreateWikiError flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." redirect_to project_path(@project) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index a8a18b4fa16..444ff837bb3 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -42,19 +42,16 @@ class ProjectsController < Projects::ApplicationController end def update - status = ::Projects::UpdateService.new(@project, current_user, project_params).execute + result = ::Projects::UpdateService.new(@project, current_user, project_params).execute # Refresh the repo in case anything changed - @repository = project.repository + @repository = @project.repository respond_to do |format| - if status + if result[:status] == :success flash[:notice] = "Project '#{@project.name}' was successfully updated." format.html do - redirect_to( - edit_project_path(@project), - notice: "Project '#{@project.name}' was successfully updated." - ) + redirect_to(edit_project_path(@project)) end else format.html { render 'edit' } @@ -127,39 +124,6 @@ class ProjectsController < Projects::ApplicationController redirect_to edit_project_path(@project), alert: ex.message end - def autocomplete_sources - noteable = - case params[:type] - when 'Issue' - IssuesFinder.new(current_user, project_id: @project.id). - execute.find_by(iid: params[:type_id]) - when 'MergeRequest' - MergeRequestsFinder.new(current_user, project_id: @project.id). - execute.find_by(iid: params[:type_id]) - when 'Commit' - @project.commit(params[:type_id]) - else - nil - end - - autocomplete = ::Projects::AutocompleteService.new(@project, current_user) - participants = ::Projects::ParticipantsService.new(@project, current_user).execute(noteable) - - @suggestions = { - emojis: Gitlab::AwardEmoji.urls, - issues: autocomplete.issues, - milestones: autocomplete.milestones, - mergerequests: autocomplete.merge_requests, - labels: autocomplete.labels, - members: participants, - commands: autocomplete.commands(noteable, params[:type]) - } - - respond_to do |format| - format.json { render json: @suggestions } - end - end - def new_issue_address return render_404 unless Gitlab::IncomingEmail.supports_issue_creation? diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 3327f4f2b87..bf27f3d4d51 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -7,17 +7,17 @@ class RegistrationsController < Devise::RegistrationsController end def create - if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha - # To avoid duplicate form fields on the login page, the registration form - # names fields using `new_user`, but Devise still wants the params in - # `user`. - if params["new_#{resource_name}"].present? && params[resource_name].blank? - params[resource_name] = params.delete(:"new_#{resource_name}") - end + # To avoid duplicate form fields on the login page, the registration form + # names fields using `new_user`, but Devise still wants the params in + # `user`. + if params["new_#{resource_name}"].present? && params[resource_name].blank? + params[resource_name] = params.delete(:"new_#{resource_name}") + end + if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha super else - flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code." + flash[:alert] = 'There was an error with the reCAPTCHA. Please re-solve the reCAPTCHA.' flash.delete :recaptcha_error render action: 'new' end @@ -27,7 +27,10 @@ class RegistrationsController < Devise::RegistrationsController DeleteUserService.new(current_user).execute(current_user) respond_to do |format| - format.html { redirect_to new_user_session_path, notice: "Account successfully removed." } + format.html do + session.try(:destroy) + redirect_to new_user_session_path, notice: "Account successfully removed." + end end end @@ -54,7 +57,7 @@ class RegistrationsController < Devise::RegistrationsController end def sign_up_params - params.require(:user).permit(:username, :email, :name, :password, :password_confirmation) + params.require(:user).permit(:username, :email, :email_confirmation, :name, :password) end def resource_name diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index b666aa01d6b..6576ebd5235 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -45,6 +45,8 @@ class SearchController < ApplicationController end @search_objects = @search_results.objects(@scope, params[:page]) + + check_single_commit_result end def autocomplete @@ -59,4 +61,16 @@ class SearchController < ApplicationController render json: search_autocomplete_opts(term).to_json end + + private + + def check_single_commit_result + if @search_results.single_commit_result? + only_commit = @search_results.objects('commits').first + query = params[:search].strip.downcase + found_by_commit_sha = Commit.valid_hash?(query) && only_commit.sha.start_with?(query) + + redirect_to namespace_project_commit_path(@project.namespace, @project, only_commit) if found_by_commit_sha + end + end end diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb index 3085ff33aba..04c36b3ebfe 100644 --- a/app/controllers/sent_notifications_controller.rb +++ b/app/controllers/sent_notifications_controller.rb @@ -12,7 +12,7 @@ class SentNotificationsController < ApplicationController def unsubscribe_and_redirect noteable = @sent_notification.noteable - noteable.unsubscribe(@sent_notification.recipient) + noteable.unsubscribe(@sent_notification.recipient, @sent_notification.project) flash[:notice] = "You have been unsubscribed from this thread." diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 5d7ecfeacf4..93a180b9036 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -31,10 +31,18 @@ class SessionsController < Devise::SessionsController resource.update_attributes(reset_password_token: nil, reset_password_sent_at: nil) end + # hide the signed-in notification + flash[:notice] = nil log_audit_event(current_user, with: authentication_method) end end + def destroy + super + # hide the signed_out notice + flash[:notice] = nil + end + private # Handle an "initial setup" state, where there's only one user, it's an admin, @@ -106,7 +114,7 @@ class SessionsController < Devise::SessionsController def valid_otp_attempt?(user) user.validate_and_consume_otp!(user_params[:otp_attempt]) || - user.invalidate_otp_backup_code!(user_params[:otp_attempt]) + user.invalidate_otp_backup_code!(user_params[:otp_attempt]) end def log_audit_event(user, options = {}) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index c4508ccc3b9..6e29f1e8a65 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -86,7 +86,7 @@ class UsersController < ApplicationController end def exists - render json: { exists: Namespace.where(path: params[:username].downcase).any? } + render json: { exists: !!Namespace.find_by_path_or_name(params[:username]) } end private diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 6297b2db369..1576fc80a6b 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -7,7 +7,7 @@ # current_user - which user use # params: # scope: 'created-by-me' or 'assigned-to-me' or 'all' -# state: 'open' or 'closed' or 'all' +# state: 'opened' or 'closed' or 'all' # group_id: integer # project_id: integer # milestone_title: string @@ -15,15 +15,14 @@ # search: string # label_name: string # sort: string +# non_archived: boolean # -require_relative 'projects_finder' - class IssuableFinder NONE = '0' attr_accessor :current_user, :params - def initialize(current_user, params) + def initialize(current_user, params = {}) @current_user = current_user @params = params end @@ -40,9 +39,48 @@ class IssuableFinder items = by_author(items) items = by_label(items) items = by_due_date(items) + items = by_non_archived(items) sort(items) end + def find(*params) + execute.find(*params) + end + + def find_by(*params) + execute.find_by(*params) + end + + # We often get counts for each state by running a query per state, and + # counting those results. This is typically slower than running one query + # (even if that query is slower than any of the individual state queries) and + # grouping and counting within that query. + # + def count_by_state + count_params = params.merge(state: nil, sort: nil) + labels_count = label_names.any? ? label_names.count : 1 + finder = self.class.new(current_user, count_params) + counts = Hash.new(0) + + # Searching by label includes a GROUP BY in the query, but ours will be last + # because it is added last. Searching by multiple labels also includes a row + # per issuable, so we have to count those in Ruby - which is bad, but still + # better than performing multiple queries. + # + finder.execute.reorder(nil).group(:state).count.each do |key, value| + counts[Array(key).last.to_sym] += value / labels_count + end + + counts[:all] = counts.values.sum + counts[:opened] += counts[:reopened] + + counts + end + + def find_by!(*params) + execute.find_by!(*params) + end + def group return @group if defined?(@group) @@ -127,31 +165,53 @@ class IssuableFinder end end - def assignee? - params[:assignee_id].present? + def assignee_id? + params[:assignee_id].present? && params[:assignee_id] != NONE + end + + def assignee_username? + params[:assignee_username].present? && params[:assignee_username] != NONE + end + + def no_assignee? + # Assignee_id takes precedence over assignee_username + params[:assignee_id] == NONE || params[:assignee_username] == NONE end def assignee return @assignee if defined?(@assignee) @assignee = - if assignee? && params[:assignee_id] != NONE - User.find(params[:assignee_id]) + if assignee_id? + User.find_by(id: params[:assignee_id]) + elsif assignee_username? + User.find_by(username: params[:assignee_username]) else nil end end - def author? - params[:author_id].present? + def author_id? + params[:author_id].present? && params[:author_id] != NONE + end + + def author_username? + params[:author_username].present? && params[:author_username] != NONE + end + + def no_author? + # author_id takes precedence over author_username + params[:author_id] == NONE || params[:author_username] == NONE end def author return @author if defined?(@author) @author = - if author? && params[:author_id] != NONE - User.find(params[:author_id]) + if author_id? + User.find_by(id: params[:author_id]) + elsif author_username? + User.find_by(username: params[:author_username]) else nil end @@ -175,10 +235,13 @@ class IssuableFinder end def by_state(items) - params[:state] ||= 'all' - - if items.respond_to?(params[:state]) - items.public_send(params[:state]) + case params[:state].to_s + when 'closed' + items.closed + when 'merged' + items.respond_to?(:merged) ? items.merged : items.closed + when 'opened' + items.opened else items end @@ -222,16 +285,24 @@ class IssuableFinder end def by_assignee(items) - if assignee? - items = items.where(assignee_id: assignee.try(:id)) + if assignee + items = items.where(assignee_id: assignee.id) + elsif no_assignee? + items = items.where(assignee_id: nil) + elsif assignee_id? || assignee_username? # assignee not found + items = items.none end items end def by_author(items) - if author? - items = items.where(author_id: author.try(:id)) + if author + items = items.where(author_id: author.id) + elsif no_author? + items = items.where(author_id: nil) + elsif author_id? || author_username? # author not found + items = items.none end items @@ -321,6 +392,10 @@ class IssuableFinder end end + def by_non_archived(items) + params[:non_archived].present? ? items.non_archived : items + end + def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' end diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index be00a219205..707eddd4d29 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -23,10 +23,26 @@ class IssuesFinder < IssuableFinder private def init_collection - Issue.visible_to_user(current_user) + IssuesFinder.not_restricted_by_confidentiality(current_user) end def iid_pattern @iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z} end + + def self.not_restricted_by_confidentiality(user) + return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank? + + return Issue.all if user.admin? + + Issue.where(' + issues.confidential IS NULL + OR issues.confidential IS FALSE + OR (issues.confidential = TRUE + AND (issues.author_id = :user_id + OR issues.assignee_id = :user_id + OR issues.project_id IN(:project_ids)))', + user_id: user.id, + project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id)) + end end diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 865f093f04a..fa0e2a5e3d8 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -6,7 +6,7 @@ class LabelsFinder < UnionFinder def execute(skip_authorization: false) @skip_authorization = skip_authorization - items = find_union(label_ids, Label) + items = find_union(label_ids, Label) || Label.none items = with_title(items) sort(items) end @@ -18,9 +18,11 @@ class LabelsFinder < UnionFinder def label_ids label_ids = [] - if project - label_ids << project.group.labels if project.group.present? - label_ids << project.labels + if project? + if project + label_ids << project.group.labels if project.group.present? + label_ids << project.labels + end else label_ids << Label.where(group_id: projects.group_ids) label_ids << Label.where(project_id: projects.select(:id)) @@ -40,16 +42,16 @@ class LabelsFinder < UnionFinder items.where(title: title) end - def group_id - params[:group_id].presence + def group? + params[:group_id].present? end - def project_id - params[:project_id].presence + def project? + params[:project_id].present? end - def projects_ids - params[:project_ids] + def projects? + params[:project_ids].present? end def title @@ -59,8 +61,9 @@ class LabelsFinder < UnionFinder def project return @project if defined?(@project) - if project_id - @project = find_project + if project? + @project = Project.find(params[:project_id]) + @project = nil unless authorized_to_read_labels?(@project) else @project = nil end @@ -68,26 +71,20 @@ class LabelsFinder < UnionFinder @project end - def find_project - if skip_authorization - Project.find_by(id: project_id) - else - available_projects.find_by(id: project_id) - end - end - def projects return @projects if defined?(@projects) - @projects = skip_authorization ? Project.all : available_projects - @projects = @projects.in_namespace(group_id) if group_id - @projects = @projects.where(id: projects_ids) if projects_ids + @projects = skip_authorization ? Project.all : ProjectsFinder.new.execute(current_user) + @projects = @projects.in_namespace(params[:group_id]) if group? + @projects = @projects.where(id: params[:project_ids]) if projects? @projects = @projects.reorder(nil) @projects end - def available_projects - @available_projects ||= ProjectsFinder.new.execute(current_user) + def authorized_to_read_labels?(project) + return true if skip_authorization + + Ability.allowed?(current_user, :read_label, project) end end diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb new file mode 100644 index 00000000000..702944404f5 --- /dev/null +++ b/app/finders/members_finder.rb @@ -0,0 +1,13 @@ +class MembersFinder < Projects::ApplicationController + def initialize(project_members, project_group) + @project_members = project_members + @project_group = project_group + end + + def execute(current_user) + non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id) + group_members = @project_group.group_members.where.not(user_id: non_null_user_ids) + group_members = group_members.non_invite unless can?(current_user, :admin_group, @project_group) + group_members + end +end diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 3b254e7d9d5..8b82255445e 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -14,6 +14,7 @@ # search: string # label_name: string # sort: string +# non_archived: boolean # class MergeRequestsFinder < IssuableFinder def klass diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index 0b7832e6583..4bd8c83081a 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -1,27 +1,102 @@ class NotesFinder FETCH_OVERLAP = 5.seconds - def execute(project, current_user, params) - target_type = params[:target_type] - target_id = params[:target_id] - # Default to 0 to remain compatible with old clients - last_fetched_at = Time.at(params.fetch(:last_fetched_at, 0).to_i) - - notes = - case target_type - when "commit" - project.notes.for_commit_id(target_id).non_diff_notes - when "issue" - project.issues.visible_to_user(current_user).find(target_id).notes.inc_author - when "merge_request" - project.merge_requests.find(target_id).mr_and_commit_notes.inc_author - when "snippet", "project_snippet" - project.snippets.find(target_id).notes + # Used to filter Notes + # When used with target_type and target_id this returns notes specifically for the controller + # + # Arguments: + # current_user - which user check authorizations with + # project - which project to look for notes on + # params: + # target_type: string + # target_id: integer + # last_fetched_at: time + # search: string + # + def initialize(project, current_user, params = {}) + @project = project + @current_user = current_user + @params = params + init_collection + end + + def execute + @notes = since_fetch_at(@params[:last_fetched_at]) if @params[:last_fetched_at] + @notes + end + + private + + def init_collection + if @params[:target_id] + @notes = on_target(@params[:target_type], @params[:target_id]) + else + @notes = notes_of_any_type + end + end + + def notes_of_any_type + types = %w(commit issue merge_request snippet) + note_relations = types.map { |t| notes_for_type(t) } + note_relations.map!{ |notes| search(@params[:search], notes) } if @params[:search] + UnionFinder.new.find_union(note_relations, Note) + end + + def noteables_for_type(noteable_type) + case noteable_type + when "issue" + IssuesFinder.new(@current_user, project_id: @project.id).execute + when "merge_request" + MergeRequestsFinder.new(@current_user, project_id: @project.id).execute + when "snippet", "project_snippet" + SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project) + else + raise 'invalid target_type' + end + end + + def notes_for_type(noteable_type) + if noteable_type == "commit" + if Ability.allowed?(@current_user, :download_code, @project) + @project.notes.where(noteable_type: 'Commit') + else + Note.none + end + else + finder = noteables_for_type(noteable_type) + @project.notes.where(noteable_type: finder.base_class.name, noteable_id: finder.reorder(nil)) + end + end + + def on_target(target_type, target_id) + if target_type == "commit" + notes_for_type('commit').for_commit_id(target_id) + else + target = noteables_for_type(target_type).find(target_id) + + if target.respond_to?(:related_notes) + target.related_notes else - raise 'invalid target_type' + target.notes end + end + end + + # Searches for notes matching the given query. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + def search(query, notes_relation = @notes) + pattern = "%#{query}%" + notes_relation.where(Note.arel_table[:note].matches(pattern)) + end + + # Notes changed since last fetch + # Uses overlapping intervals to avoid worrying about race conditions + def since_fetch_at(fetch_time) + # Default to 0 to remain compatible with old clients + last_fetched_at = Time.at(@params.fetch(:last_fetched_at, 0).to_i) - # Use overlapping intervals to avoid worrying about race conditions - notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh + @notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh end end diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 00ff1611039..da6e6e87a6f 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -1,14 +1,17 @@ class SnippetsFinder def execute(current_user, params = {}) filter = params[:filter] + user = params.fetch(:user, current_user) case filter when :all then snippets(current_user).fresh + when :public then + Snippet.are_public.fresh when :by_user then - by_user(current_user, params[:user], params[:scope]) + by_user(current_user, user, params[:scope]) when :by_project - by_project(current_user, params[:project]) + by_project(current_user, params[:project], params[:scope]) end end @@ -29,35 +32,35 @@ class SnippetsFinder def by_user(current_user, user, scope) snippets = user.snippets.fresh - return snippets.are_public unless current_user - - if user == current_user - case scope - when 'are_internal' then - snippets.are_internal - when 'are_private' then - snippets.are_private - when 'are_public' then - snippets.are_public - else - snippets - end + if current_user + include_private = user == current_user + by_scope(snippets, scope, include_private) else - snippets.public_and_internal + snippets.are_public end end - def by_project(current_user, project) + def by_project(current_user, project, scope) snippets = project.snippets.fresh if current_user - if project.team.member?(current_user) || current_user.admin? - snippets - else - snippets.public_and_internal - end + include_private = project.team.member?(current_user) || current_user.admin? + by_scope(snippets, scope, include_private) else snippets.are_public end end + + def by_scope(snippets, scope = nil, include_private = false) + case scope.to_s + when 'are_private' + include_private ? snippets.are_private : Snippet.none + when 'are_internal' + snippets.are_internal + when 'are_public' + snippets.are_public + else + include_private ? snippets : snippets.public_and_internal + end + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c816b616631..a112928c6de 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -244,7 +244,9 @@ module ApplicationHelper scope: params[:scope], milestone_title: params[:milestone_title], assignee_id: params[:assignee_id], + assignee_username: params[:assignee_username], author_id: params[:author_id], + author_username: params[:author_username], search: params[:search], label_name: params[:label_name] } diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 45a567a1eba..60485160495 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -50,14 +50,14 @@ module ApplicationSettingsHelper def restricted_level_checkboxes(help_block_id) Gitlab::VisibilityLevel.options.map do |name, level| checked = restricted_visibility_levels(true).include?(level) - css_class = 'btn' - css_class += ' active' if checked - checkbox_name = 'application_setting[restricted_visibility_levels][]' + css_class = checked ? 'active' : '' + checkbox_name = "application_setting[restricted_visibility_levels][]" - label_tag(checkbox_name, class: css_class) do + label_tag(name, class: css_class) do check_box_tag(checkbox_name, level, checked, autocomplete: 'off', - 'aria-describedby' => help_block_id) + name + 'aria-describedby' => help_block_id, + id: name) + visibility_level_icon(level) + name end end end @@ -67,14 +67,14 @@ module ApplicationSettingsHelper def import_sources_checkboxes(help_block_id) Gitlab::ImportSources.options.map do |name, source| checked = current_application_settings.import_sources.include?(source) - css_class = 'btn' - css_class += ' active' if checked + css_class = checked ? 'active' : '' checkbox_name = 'application_setting[import_sources][]' - label_tag(checkbox_name, class: css_class) do + label_tag(name, class: css_class) do check_box_tag(checkbox_name, source, checked, autocomplete: 'off', - 'aria-describedby' => help_block_id) + name + 'aria-describedby' => help_block_id, + id: name.tr(' ', '_')) + name end end end @@ -100,4 +100,8 @@ module ApplicationSettingsHelper options_for_select(options, @application_setting.repository_storages) end + + def sidekiq_queue_options_for_select + options_for_select(Sidekiq::Queue.all.map(&:name), @application_setting.sidekiq_throttling_queues) + end end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index cd4d778e508..1ee6c1d3afa 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -1,9 +1,9 @@ module AuthHelper - PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2).freeze + PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze def ldap_enabled? - Gitlab.config.ldap.enabled + Gitlab::LDAP::Config.enabled? end def omniauth_enabled? diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 07ff6fb9488..c3508443d8a 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -188,7 +188,11 @@ module BlobHelper end def gitlab_ci_ymls - @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names + @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names(params[:context]) + end + + def dockerfile_names + @dockerfile_names ||= Gitlab::Template::DockerfileTemplate.dropdown_names end def blob_editor_paths diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index fde297c588e..9fc69e12266 100644 --- a/app/helpers/builds_helper.rb +++ b/app/helpers/builds_helper.rb @@ -12,7 +12,7 @@ module BuildsHelper build_url: namespace_project_build_url(@project.namespace, @project, @build, :json), build_status: @build.status, build_stage: @build.stage, - state1: @build.trace_with_state[:state] + log_state: @build.trace_with_state[:state].to_s } end end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index dee3c78df47..4c7c16d694c 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -16,7 +16,7 @@ module ButtonHelper # See http://clipboardjs.com/#usage def clipboard_button(data = {}) css_class = data[:class] || 'btn-clipboard btn-transparent' - title = data[:title] || 'Copy to Clipboard' + title = data[:title] || 'Copy to clipboard' data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) content_tag :button, icon('clipboard'), diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 895c3d728ad..94f3b480178 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -1,20 +1,15 @@ module CiStatusHelper def ci_status_path(pipeline) project = pipeline.project - builds_namespace_project_commit_path(project.namespace, project, pipeline.sha) + namespace_project_pipeline_path(project.namespace, project, pipeline) end - def ci_status_with_icon(status, target = nil) - content = ci_icon_for_status(status) + ' '.html_safe + ci_label_for_status(status) - klass = "ci-status ci-#{status}" - if target - link_to content, target, class: klass - else - content_tag :span, content, class: klass + # Is used by Commit and Merge Request Widget + def ci_label_for_status(status) + if detailed_status?(status) + return status.label end - end - def ci_label_for_status(status) case status when 'success' 'passed' @@ -31,6 +26,10 @@ module CiStatusHelper end def ci_icon_for_status(status) + if detailed_status?(status) + return custom_icon(status.icon) + end + icon_name = case status when 'success' @@ -94,4 +93,10 @@ module CiStatusHelper class: klass, title: title, data: data end end + + def detailed_status?(status) + status.respond_to?(:text) && + status.respond_to?(:label) && + status.respond_to?(:icon) + end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index ed402b698fb..e9461b9f859 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -128,50 +128,11 @@ module CommitsHelper end def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) - return unless current_user - - tooltip = "Revert this #{commit.change_type_title} in a new merge request" if has_tooltip - - if can_collaborate_with_project? - btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil? - link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" - elsif can?(current_user, :fork_project, @project) - continue_params = { - to: continue_to_path, - notice: edit_in_new_fork_notice + ' Try to revert this commit again.', - notice_now: edit_in_new_fork_notice_now - } - fork_path = namespace_project_forks_path(@project.namespace, @project, - namespace_key: current_user.namespace.id, - continue: continue_params) - - btn_class = "btn btn-grouped btn-warning" unless btn_class.nil? - - link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) - end + commit_action_link('revert', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip) end def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) - return unless current_user - - tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request" - - if can_collaborate_with_project? - btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil? - link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" - elsif can?(current_user, :fork_project, @project) - continue_params = { - to: continue_to_path, - notice: edit_in_new_fork_notice + ' Try to cherry-pick this commit again.', - notice_now: edit_in_new_fork_notice_now - } - fork_path = namespace_project_forks_path(@project.namespace, @project, - namespace_key: current_user.namespace.id, - continue: continue_params) - - btn_class = "btn btn-grouped btn-close" unless btn_class.nil? - link_to 'Cherry-pick', fork_path, class: "#{btn_class}", method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) - end + commit_action_link('cherry-pick', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip) end protected @@ -211,6 +172,28 @@ module CommitsHelper end end + def commit_action_link(action, commit, continue_to_path, btn_class: nil, has_tooltip: true) + return unless current_user + + tooltip = "#{action.capitalize} this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip + btn_class = "btn btn-#{btn_class}" unless btn_class.nil? + + if can_collaborate_with_project? + link_to action.capitalize, "#modal-#{action}-commit", 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" + elsif can?(current_user, :fork_project, @project) + continue_params = { + to: continue_to_path, + notice: "#{edit_in_new_fork_notice} Try to #{action} this commit again.", + notice_now: edit_in_new_fork_notice_now + } + fork_path = namespace_project_forks_path(@project.namespace, @project, + namespace_key: current_user.namespace.id, + continue: continue_params) + + link_to action.capitalize, fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) + end + end + def view_file_btn(commit_sha, diff_new_path, project) link_to( namespace_project_blob_path(project.namespace, project, diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 0725c3f4c56..aed1d7c839f 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -51,11 +51,12 @@ module DiffHelper html.html_safe end - def diff_line_content(line, line_type = nil) + def diff_line_content(line) if line.blank? - " ".html_safe + " ".html_safe else - line[0] = ' ' if %w[new old].include?(line_type) + # We can't use `sub` because the HTML-safeness of `line` will not survive. + line[0] = '' if line.start_with?('+', '-', ' ') line end end @@ -164,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/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index cbab1fd5967..81e0b6bb5ae 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -43,7 +43,7 @@ module DropdownsHelper default_label = data_attr[:default_label] content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}") - output << icon('caret-down') + output << icon('chevron-down') output.html_safe end end diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb new file mode 100644 index 00000000000..ff8550439d0 --- /dev/null +++ b/app/helpers/environment_helper.rb @@ -0,0 +1,31 @@ +module EnvironmentHelper + def environment_for_build(project, build) + return unless build.environment + + project.environments.find_by(name: build.expanded_environment_name) + end + + def environment_link_for_build(project, build) + environment = environment_for_build(project, build) + if environment + link_to environment.name, namespace_project_environment_path(project.namespace, project, environment) + else + content_tag :span, build.expanded_environment_name + end + end + + def deployment_link(deployment, text: nil) + return unless deployment + + link_label = text ? text : "##{deployment.iid}" + + link_to link_label, [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable] + end + + def last_deployment_link_for_environment_build(project, build) + environment = environment_for_build(project, build) + return unless environment + + deployment_link(environment.last_deployment) + end +end diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb new file mode 100644 index 00000000000..515e802e01e --- /dev/null +++ b/app/helpers/environments_helper.rb @@ -0,0 +1,7 @@ +module EnvironmentsHelper + def environments_list_data + { + endpoint: namespace_project_environments_path(@project.namespace, @project, format: :json) + } + end +end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 00e64076408..362046c0270 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -45,6 +45,12 @@ module EventsHelper @project.feature_available?(feature_key, current_user) end + def comments_visible? + event_filter_visible(:repository) || + event_filter_visible(:merge_requests) || + event_filter_visible(:issues) + end + def event_preposition(event) if event.push? || event.commented? || event.target "at" @@ -86,7 +92,7 @@ module EventsHelper elsif event.merge_request? namespace_project_merge_request_url(event.project.namespace, event.project, event.merge_request) - elsif event.note? && event.commit_note? + elsif event.commit_note? namespace_project_commit_url(event.project.namespace, event.project, event.note_target) elsif event.note? @@ -127,7 +133,7 @@ module EventsHelper end def event_note_target_path(event) - if event.note? && event.commit_note? + if event.commit_note? namespace_project_commit_path(event.project.namespace, event.project, event.note_target, diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index 6a43be2cf3e..1182939f656 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -7,12 +7,12 @@ module FormHelper content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do content_tag(:h4, headline) << - content_tag(:ul) do - model.errors.full_messages. - map { |msg| content_tag(:li, msg) }. - join. - html_safe - end + content_tag(:ul) do + model.errors.full_messages. + map { |msg| content_tag(:li, msg) }. + join. + html_safe + end end end end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 0772d848289..6d365ea9251 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -110,6 +110,28 @@ module GitlabMarkdownHelper end end + # Returns the text necessary to reference `entity` across projects + # + # project - Project to reference + # entity - Object that responds to `to_reference` + # + # Examples: + # + # cross_project_reference(project, project.issues.first) + # # => 'namespace1/project1#123' + # + # cross_project_reference(project, project.merge_requests.first) + # # => 'namespace1/project1!345' + # + # Returns a String + def cross_project_reference(project, entity) + if entity.respond_to?(:to_reference) + entity.to_reference(project, full: true) + else + '' + end + end + private # Return +text+, truncated to +max_chars+ characters, excluding any HTML @@ -158,28 +180,6 @@ module GitlabMarkdownHelper end end - # Returns the text necessary to reference `entity` across projects - # - # project - Project to reference - # entity - Object that responds to `to_reference` - # - # Examples: - # - # cross_project_reference(project, project.issues.first) - # # => 'namespace1/project1#123' - # - # cross_project_reference(project, project.merge_requests.first) - # # => 'namespace1/project1!345' - # - # Returns a String - def cross_project_reference(project, entity) - if entity.respond_to?(:to_reference) - "#{project.to_reference}#{entity.to_reference}" - else - '' - end - end - def markdown_toolbar_button(options = {}) data = options[:data].merge({ container: "body" }) content_tag :button, diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index bccf64d1aac..2159e4ce21a 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -82,6 +82,10 @@ module GitlabRoutingHelper namespace_project_merge_request_path(entity.project.namespace, entity.project, entity, *args) end + def pipeline_path(pipeline, *args) + namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, *args) + end + def milestone_path(entity, *args) namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args) end @@ -155,6 +159,11 @@ module GitlabRoutingHelper resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) end + # Snippets + def personal_snippet_url(snippet, *args) + snippet_url(snippet) + end + # Groups ## Members @@ -197,4 +206,13 @@ module GitlabRoutingHelper file_namespace_project_build_artifacts_path(*args) end end + + # Settings + def project_settings_integrations_path(project, *args) + namespace_project_settings_integrations_path(project.namespace, project, *args) + end + + def project_settings_members_path(project, *args) + namespace_project_settings_members_path(project.namespace, project, *args) + end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index ab880ed6de0..77dc9e7d538 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -5,22 +5,25 @@ module GroupsHelper def group_icon(group) if group.is_a?(String) - group = Group.find_by(path: group) + group = Group.find_by_full_path(group) end - if group && group.avatar.present? - group.avatar.url - else - image_path('no_group_avatar.png') - end + group.try(:avatar_url) || image_path('no_group_avatar.png') end def group_title(group, name = nil, url = nil) - full_title = link_to(simple_sanitize(group.name), group_path(group)) + full_title = '' + + group.parents.each do |parent| + full_title += link_to(simple_sanitize(parent.name), group_path(parent)) + full_title += ' / '.html_safe + end + + full_title += link_to(simple_sanitize(group.name), group_path(group)) full_title += ' · '.html_safe + link_to(simple_sanitize(name), url) if name content_tag :span do - full_title + full_title.html_safe end end @@ -48,4 +51,8 @@ module GroupsHelper "#{status.humanize} #{projects_lfs_status(group)}" end end + + def group_issues(group) + IssuesFinder.new(current_user, group_id: group.id).execute + end end diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index 021d2b14718..a0642a1894b 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -4,8 +4,10 @@ module ImportHelper "#{namespace}/#{name}" end - def github_project_link(path_with_namespace) - link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank' + def provider_project_link(provider, path_with_namespace) + url = __send__("#{provider}_project_url", path_with_namespace) + + link_to path_with_namespace, url, target: '_blank' end private @@ -20,4 +22,8 @@ module ImportHelper provider = Gitlab.config.omniauth.providers.find { |p| p.name == 'github' } @github_url = provider.fetch('url', 'https://github.com') if provider end + + def gitea_project_url(path_with_namespace) + "#{@gitea_host_url.sub(%r{/+\z}, '')}/#{path_with_namespace}" + end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 8127c3f3ee3..e5bb8b93e76 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -30,9 +30,13 @@ module IssuablesHelper end end - def can_add_template?(issuable) - names = issuable_templates(issuable) - names.empty? && can?(current_user, :push_code, @project) && !@project.private? + 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) @@ -101,8 +105,8 @@ module IssuablesHelper if issuable.tasks? output << " ".html_safe - output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs") - output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-sm hidden-md hidden-lg") + output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm") + output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg") end output @@ -141,8 +145,33 @@ module IssuablesHelper html.html_safe end + def cached_assigned_issuables_count(assignee, issuable_type, state) + cache_key = hexdigest(['assigned_issuables_count', assignee.id, issuable_type, state].join('-')) + Rails.cache.fetch(cache_key, expires_in: 2.minutes) do + assigned_issuables_count(assignee, issuable_type, state) + end + end + + def issuable_filter_params + [ + :search, + :author_id, + :assignee_id, + :milestone_title, + :label_name + ] + end + + def issuable_filter_present? + issuable_filter_params.any? { |k| params.key?(k) } + end + private + def assigned_issuables_count(assignee, issuable_type, state) + assignee.public_send("assigned_#{issuable_type}").public_send(state).count + end + def sidebar_gutter_collapsed? cookies[:collapsed_gutter] == 'true' end @@ -159,15 +188,10 @@ module IssuablesHelper end end - def issuable_filters_present - params[:search] || params[:author_id] || params[:assignee_id] || params[:milestone_title] || params[:label_name] - end - def issuables_count_for_state(issuable_type, state) - issuables_finder = public_send("#{issuable_type}_finder") - issuables_finder.params[:state] = state - - issuables_finder.execute.page(1).total_count + @counts ||= {} + @counts[issuable_type] ||= public_send("#{issuable_type}_finder").count_by_state + @counts[issuable_type][state] end IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page] @@ -177,6 +201,7 @@ module IssuablesHelper opts = params.with_indifferent_access opts[:state] = state opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY) + opts.delete_if { |_, value| value.blank? } hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-')) end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 1644c346dd8..a2d21b67a77 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -58,12 +58,14 @@ module IssuesHelper end def status_box_class(item) - if item.respond_to?(:expired?) && item.expired? + if item.try(:expired?) 'status-box-expired' - elsif item.respond_to?(:merged?) && item.merged? + elsif item.try(:merged?) 'status-box-merged' elsif item.closed? 'status-box-closed' + elsif item.try(:upcoming?) + 'status-box-upcoming' else 'status-box-open' end @@ -126,8 +128,10 @@ module IssuesHelper names.to_sentence end - def award_active_class(awards, current_user) - if current_user && awards.find { |a| a.user_id == current_user.id } + def award_state_class(awards, current_user) + if !current_user + "disabled" + elsif current_user && awards.find { |a| a.user_id == current_user.id } "active" else "" diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 221a84b042f..e5b1e6e8bc7 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -68,14 +68,6 @@ module LabelsHelper end end - def toggle_subscription_data(label) - return unless label.is_a?(ProjectLabel) - - { - url: toggle_subscription_namespace_project_label_path(label.project.namespace, label.project, label) - } - end - def render_colored_label(label, label_suffix = '', tooltip: true) label_color = label.color || Label::DEFAULT_COLOR text_color = text_color_for_bg(label_color) @@ -90,12 +82,6 @@ module LabelsHelper span.html_safe end - def render_colored_cross_project_label(label, source_project = nil, tooltip: true) - label_suffix = source_project ? source_project.name_with_namespace : label.project.name_with_namespace - label_suffix = " <i>in #{escape_once(label_suffix)}</i>" - render_colored_label(label, label_suffix, tooltip: tooltip) - end - def suggested_colors [ '#0033CC', @@ -148,20 +134,24 @@ module LabelsHelper end end - def label_subscription_status(label) - case label - when GroupLabel then 'Subscribing to group labels is currently not supported.' - when ProjectLabel then label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed' - end + def label_subscription_status(label, project) + return 'project-level' if label.subscribed?(current_user, project) + return 'group-level' if label.subscribed?(current_user) + + 'unsubscribed' end - def label_subscription_toggle_button_text(label) - case label - when GroupLabel then 'Subscribing to group labels is currently not supported.' - when ProjectLabel then label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe' + def group_label_unsubscribe_path(label, project) + case label_subscription_status(label, project) + when 'project-level' then toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) + when 'group-level' then toggle_subscription_group_label_path(label.group, label) end end + def label_subscription_toggle_button_text(label, project) + label.subscribed?(current_user, project) ? 'Unsubscribe' : 'Subscribe' + end + def label_deletion_confirm_text(label) case label when GroupLabel then 'Remove this label? This will affect all projects within the group. Are you sure?' @@ -170,6 +160,5 @@ module LabelsHelper end # Required for Banzai::Filter::LabelReferenceFilter - module_function :render_colored_label, :render_colored_cross_project_label, - :text_color_for_bg, :escape_once + module_function :render_colored_label, :text_color_for_bg, :escape_once end diff --git a/app/helpers/mattermost_helper.rb b/app/helpers/mattermost_helper.rb new file mode 100644 index 00000000000..49ac12db832 --- /dev/null +++ b/app/helpers/mattermost_helper.rb @@ -0,0 +1,9 @@ +module MattermostHelper + def mattermost_teams_options(teams) + teams_options = teams.map do |id, options| + [options['display_name'] || options['name'], id] + end + + teams_options.compact.unshift(['Select team...', '0']) + end +end diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index 877c77050be..41d471cc92f 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -36,4 +36,12 @@ module MembersHelper "Are you sure you want to leave the " \ "\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?" end + + def filter_group_project_member_path(options = {}) + options = params.slice(:search, :sort).merge(options) + + path = request.path + path << "?#{options.to_param}" + path + end end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index a6659ea2fd1..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? @@ -59,6 +67,10 @@ module MergeRequestsHelper @mr_closes_issues ||= @merge_request.closes_issues end + def mr_issues_mentioned_but_not_closing + @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing + end + def mr_change_branches_path(merge_request) new_namespace_project_merge_request_path( @project.namespace, @project, diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 83a2a4ad3ec..729928ce1dd 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -86,6 +86,30 @@ module MilestonesHelper days = milestone.remaining_days content = content_tag(:strong, days) content << " #{'day'.pluralize(days)} remaining" + elsif milestone.upcoming? + content_tag(:strong, 'Upcoming') + elsif milestone.start_date && milestone.start_date.past? + days = milestone.elapsed_days + content = content_tag(:strong, days) + content << " #{'day'.pluralize(days)} elapsed" + end + end + + def milestone_date_range(milestone) + if milestone.start_date && milestone.due_date + "#{milestone.start_date.to_s(:medium)} - #{milestone.due_date.to_s(:medium)}" + elsif milestone.due_date + if milestone.due_date.past? + "expired on #{milestone.due_date.to_s(:medium)}" + else + "expires on #{milestone.due_date.to_s(:medium)}" + end + elsif milestone.start_date + if milestone.start_date.past? + "started on #{milestone.start_date.to_s(:medium)}" + else + "starts on #{milestone.start_date.to_s(:medium)}" + end end end end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index df87fac132d..e21178c7377 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -7,12 +7,12 @@ module NavHelper def page_gutter_class if current_path?('merge_requests#show') || - current_path?('merge_requests#diffs') || - current_path?('merge_requests#commits') || - current_path?('merge_requests#builds') || - current_path?('merge_requests#conflicts') || - current_path?('merge_requests#pipelines') || - current_path?('issues#show') + current_path?('merge_requests#diffs') || + current_path?('merge_requests#commits') || + current_path?('merge_requests#builds') || + current_path?('merge_requests#conflicts') || + current_path?('merge_requests#pipelines') || + current_path?('issues#show') if cookies[:collapsed_gutter] == 'true' "page-gutter right-sidebar-collapsed" else @@ -20,6 +20,11 @@ module NavHelper end elsif current_path?('builds#show') "page-gutter build-sidebar right-sidebar-expanded" + elsif current_path?('wikis#show') || + current_path?('wikis#edit') || + current_path?('wikis#history') || + current_path?('wikis#git_access') + "page-gutter wiki-sidebar right-sidebar-expanded" end end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index a46f2c6e17d..6e68aad4cb7 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -50,7 +50,7 @@ module PreferencesHelper end def default_project_view - return 'readme' unless current_user + return anonymous_project_view unless current_user user_view = current_user.project_view @@ -66,4 +66,8 @@ module PreferencesHelper "customize_workflow" end end + + def anonymous_project_view + @project.empty_repo? || !can?(current_user, :download_code, @project) ? 'activity' : 'readme' + end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 42c00ec3cd5..eb98204285d 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -49,10 +49,10 @@ module ProjectsHelper end end - def project_title(project, name = nil, url = nil) + def project_title(project) namespace_link = if project.group - link_to(simple_sanitize(project.group.name), group_path(project.group)) + group_title(project.group) else owner = project.namespace.owner link_to(simple_sanitize(owner.name), user_path(owner)) @@ -61,15 +61,12 @@ module ProjectsHelper project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } if current_user - project_link << button_tag(type: 'button', class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) do + project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do icon("chevron-down") end end - full_title = "#{namespace_link} / #{project_link}".html_safe - full_title << ' · '.html_safe << link_to(simple_sanitize(name), url) if name - - full_title + "#{namespace_link} / #{project_link}".html_safe end def remove_project_message(project) @@ -93,10 +90,12 @@ module ProjectsHelper end def project_for_deploy_key(deploy_key) - if deploy_key.projects.include?(@project) + if deploy_key.has_access_to?(@project) @project else - deploy_key.projects.find { |project| can?(current_user, :read_project, project) } + deploy_key.projects.find do |project| + can?(current_user, :read_project, project) + end end end @@ -174,48 +173,27 @@ module ProjectsHelper nav_tabs << :merge_requests end - if can?(current_user, :read_pipeline, project) - nav_tabs << :pipelines - end - - if can?(current_user, :read_build, project) - nav_tabs << :builds - end - if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project) nav_tabs << :container_registry end - if can?(current_user, :read_environment, project) - nav_tabs << :environments - end - - if can?(current_user, :admin_project, project) - nav_tabs << :settings - end - - if can?(current_user, :read_project_member, project) - nav_tabs << :team - end - - if can?(current_user, :read_issue, project) - nav_tabs << :issues - end - - if can?(current_user, :read_wiki, project) - nav_tabs << :wiki - end - - if can?(current_user, :read_project_snippet, project) - nav_tabs << :snippets - end - - if can?(current_user, :read_label, project) - nav_tabs << :labels - end + tab_ability_map = { + environments: :read_environment, + milestones: :read_milestone, + pipelines: :read_pipeline, + snippets: :read_project_snippet, + settings: :admin_project, + builds: :read_build, + labels: :read_label, + issues: :read_issue, + team: :read_project_member, + wiki: :read_wiki + } - if can?(current_user, :read_milestone, project) - nav_tabs << :milestones + tab_ability_map.each do |tab, ability| + if can?(current_user, ability, project) + nav_tabs << tab + end end nav_tabs.flatten @@ -249,11 +227,6 @@ module ProjectsHelper end end - def repository_size(project = @project) - size_in_bytes = project.repository_size * 1.megabyte - number_to_human_size(size_in_bytes, delimiter: ',', precision: 2) - end - def default_url_to_repo(project = @project) case default_clone_protocol when 'ssh' @@ -283,13 +256,15 @@ module ProjectsHelper end end - def add_special_file_path(project, file_name:, commit_message: nil) + def add_special_file_path(project, file_name:, commit_message: nil, target_branch: nil, context: nil) namespace_project_new_blob_path( project.namespace, project, project.default_branch || 'master', file_name: file_name, - commit_message: commit_message || "Add #{file_name.downcase}" + commit_message: commit_message || "Add #{file_name.downcase}", + target_branch: target_branch, + context: context ) end @@ -394,39 +369,11 @@ module ProjectsHelper end end - def new_readme_path - ref = @repository.root_ref if @repository - ref ||= 'master' - - namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'README.md') - end - - def new_license_path - ref = @repository.root_ref if @repository - ref ||= 'master' - - namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'LICENSE') - end - def readme_cache_key sha = @project.commit.try(:sha) || 'nil' [@project.path_with_namespace, sha, "readme"].join('-') end - def round_commit_count(project) - count = project.commit_count - - if count > 10000 - '10000+' - elsif count > 5000 - '5000+' - elsif count > 1000 - '1000+' - else - count - end - end - def current_ref @ref || @repository.try(:root_ref) end @@ -458,4 +405,19 @@ module ProjectsHelper def project_child_container_class(view_path) view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}" end + + def project_issues(project) + IssuesFinder.new(current_user, project_id: project.id).execute + end + + def visibility_select_options(project, selected_level) + levels_options_array = Gitlab::VisibilityLevel.values.map do |level| + [ + visibility_level_label(level), + { data: { description: visibility_level_description(level, project) } }, + level + ] + end + options_for_select(levels_options_array, selected_level) + end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index aba3a3f9c5d..6654f6997ce 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -31,34 +31,7 @@ module SearchHelper end def parse_search_result(result) - ref = nil - filename = nil - basename = nil - startline = 0 - - result.each_line.each_with_index do |line, index| - if line =~ /^.*:.*:\d+:/ - ref, filename, startline = line.split(':') - startline = startline.to_i - index - extname = Regexp.escape(File.extname(filename)) - basename = filename.sub(/#{extname}$/, '') - break - end - end - - data = "" - - result.each_line do |line| - data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '') - end - - OpenStruct.new( - filename: filename, - basename: basename, - ref: ref, - startline: startline, - data: data - ) + Gitlab::ProjectSearchResults.parse_search_result(result) end private @@ -66,7 +39,7 @@ module SearchHelper # Autocomplete results for various settings pages def default_autocomplete [ - { category: "Settings", label: "Profile settings", url: profile_path }, + { category: "Settings", label: "User settings", url: profile_path }, { category: "Settings", label: "SSH Keys", url: profile_keys_path }, { category: "Settings", label: "Dashboard", url: root_path }, { category: "Settings", label: "Admin Section", url: admin_root_path }, @@ -102,7 +75,7 @@ module SearchHelper { category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, { category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, { category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, - { category: "Current Project", label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) }, + { category: "Current Project", label: "Members", url: namespace_project_settings_members_path(@project.namespace, @project) }, { category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, ] else diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index 3d4abf76419..715e5893a2c 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -1,22 +1,24 @@ module ServicesHelper def service_event_description(event) case event - when "push" + when "push", "push_events" "Event will be triggered by a push to the repository" - when "tag_push" + when "tag_push", "tag_push_events" "Event will be triggered when a new tag is pushed to the repository" - when "note" + when "note", "note_events" "Event will be triggered when someone adds a comment" - when "issue" + when "issue", "issue_events" "Event will be triggered when an issue is created/updated/closed" - when "confidential_issue" + when "confidential_issue", "confidential_issue_events" "Event will be triggered when a confidential issue is created/updated/closed" - when "merge_request" + when "merge_request", "merge_request_events" "Event will be triggered when a merge request is created/updated/merged" - when "build" + when "build", "build_events" "Event will be triggered when a build status changes" - when "wiki_page" + when "wiki_page", "wiki_page_events" "Event will be triggered when a wiki page is created/updated" + when "commit", "commit_events" + "Event will be triggered when a commit is created/updated" end end @@ -24,4 +26,6 @@ module ServicesHelper event = event.pluralize if %w[merge_request issue confidential_issue].include?(event) "#{event}_events" end + + extend self end diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb index 56749d80bd3..b5017080cfb 100644 --- a/app/helpers/sidekiq_helper.rb +++ b/app/helpers/sidekiq_helper.rb @@ -5,15 +5,11 @@ module SidekiqHelper (?<mem>[\d\.,]+)\s+ (?<state>[DRSTWXZNLsl\+<]+)\s+ (?<start>.+)\s+ - (?<command>sidekiq.*\])\s* + (?<command>sidekiq.*\]) \z/x def parse_sidekiq_ps(line) - match = line.match(SIDEKIQ_PS_REGEXP) - if match - match[1..6] - else - %w[? ? ? ? ? ?] - end + match = line.strip.match(SIDEKIQ_PS_REGEXP) + match ? match[1..6] : Array.new(6, '?') end end diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 7e33a562077..8c02b4061ca 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -8,6 +8,17 @@ module SnippetsHelper end end + # Return the path of a snippets index for a user or for a project + # + # @returns String, path to snippet index + def subject_snippets_path(subject = nil, opts = nil) + if subject.is_a?(Project) + namespace_project_snippets_path(subject.namespace, subject, opts) + else # assume subject === User + dashboard_snippets_path(opts) + end + end + # Get an array of line numbers surrounding a matching # line, bounded by min/max. # diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 8b138a8e69f..ff787fb4131 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -11,6 +11,7 @@ module SortingHelper sort_value_due_date_soon => sort_title_due_date_soon, sort_value_due_date_later => sort_title_due_date_later, sort_value_largest_repo => sort_title_largest_repo, + sort_value_largest_group => sort_title_largest_group, sort_value_recently_signin => sort_title_recently_signin, sort_value_oldest_signin => sort_title_oldest_signin, sort_value_downvotes => sort_title_downvotes, @@ -25,7 +26,7 @@ module SortingHelper sort_value_recently_updated => sort_title_recently_updated, sort_value_oldest_updated => sort_title_oldest_updated, sort_value_recently_created => sort_title_recently_created, - sort_value_oldest_created => sort_title_oldest_created, + sort_value_oldest_created => sort_title_oldest_created } if current_controller?('admin/projects') @@ -35,6 +36,19 @@ module SortingHelper options end + def member_sort_options_hash + { + sort_value_access_level_asc => sort_title_access_level_asc, + sort_value_access_level_desc => sort_title_access_level_desc, + sort_value_last_joined => sort_title_last_joined, + sort_value_oldest_joined => sort_title_oldest_joined, + sort_value_name => sort_title_name_asc, + sort_value_name_desc => sort_title_name_desc, + sort_value_recently_signin => sort_title_recently_signin, + sort_value_oldest_signin => sort_title_oldest_signin + } + end + def sort_title_priority 'Priority' end @@ -79,6 +93,10 @@ module SortingHelper 'Largest repository' end + def sort_title_largest_group + 'Largest group' + end + def sort_title_recently_signin 'Recent sign in' end @@ -95,6 +113,50 @@ module SortingHelper 'Most popular' end + def sort_title_last_joined + 'Last joined' + end + + def sort_title_oldest_joined + 'Oldest joined' + end + + def sort_title_access_level_asc + 'Access level, ascending' + end + + def sort_title_access_level_desc + 'Access level, descending' + end + + def sort_title_name_asc + 'Name, ascending' + end + + def sort_title_name_desc + 'Name, descending' + end + + def sort_value_last_joined + 'last_joined' + end + + def sort_value_oldest_joined + 'oldest_joined' + end + + def sort_value_access_level_asc + 'access_level_asc' + end + + def sort_value_access_level_desc + 'access_level_desc' + end + + def sort_value_name_desc + 'name_desc' + end + def sort_value_priority 'priority' end @@ -136,7 +198,11 @@ module SortingHelper end def sort_value_largest_repo - 'repository_size_desc' + 'storage_size_desc' + end + + def sort_value_largest_group + 'storage_size_desc' end def sort_value_recently_signin diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb new file mode 100644 index 00000000000..e19c67a37ca --- /dev/null +++ b/app/helpers/storage_helper.rb @@ -0,0 +1,7 @@ +module StorageHelper + def storage_counter(size_in_bytes) + precision = size_in_bytes < 1.megabyte ? 0 : 1 + + number_to_human_size(size_in_bytes, delimiter: ',', precision: precision, significant: false) + end +end diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 563ddd2a511..547f6258909 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -106,9 +106,9 @@ module TabHelper def branches_tab_class if current_controller?(:protected_branches) || - current_controller?(:branches) || - current_page?(namespace_project_repository_path(@project.namespace, - @project)) + current_controller?(:branches) || + current_page?(namespace_project_repository_path(@project.namespace, + @project)) 'active' end end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 09c69786791..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 @@ -35,7 +36,7 @@ module TodosHelper else path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target] - path.unshift(:builds) if todo.build_failed? + path.unshift(:pipelines) if todo.build_failed? polymorphic_path(path, anchor: anchor) end diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb index 8cad994d10f..b0135ea2e95 100644 --- a/app/helpers/triggers_helper.rb +++ b/app/helpers/triggers_helper.rb @@ -1,5 +1,13 @@ module TriggersHelper - def builds_trigger_url(project_id) - "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds" + def builds_trigger_url(project_id, ref: nil) + if ref.nil? + "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds" + else + "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/ref/#{ref}/trigger/builds" + end + end + + def service_trigger_url(service) + "#{Settings.gitlab.url}/api/v3/projects/#{service.project_id}/services/#{service.to_param}/trigger" end end diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index 96116e916dd..0d20c9092c4 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -4,6 +4,7 @@ module Emails setup_note_mail(note_id, recipient_id) @commit = @note.noteable + @discussion = @note.to_discussion if @note.diff_note? @target_url = namespace_project_commit_url(*note_target_url_options) mail_answer_thread(@commit, @@ -24,6 +25,7 @@ module Emails setup_note_mail(note_id, recipient_id) @merge_request = @note.noteable + @discussion = @note.to_discussion if @note.diff_note? @target_url = namespace_project_merge_request_url(*note_target_url_options) mail_answer_thread(@merge_request, note_thread_options(recipient_id)) end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 0bc1c19e9cd..0cd3456b4de 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -107,15 +107,11 @@ class Notify < BaseMailer def mail_thread(model, headers = {}) add_project_headers + add_unsubscription_headers_and_links + headers["X-GitLab-#{model.class.name}-ID"] = model.id headers['X-GitLab-Reply-Key'] = reply_key - if !@labels_url && @sent_notification && @sent_notification.unsubscribable? - headers['List-Unsubscribe'] = "<#{unsubscribe_sent_notification_url(@sent_notification, force: true)}>" - - @sent_notification_url = unsubscribe_sent_notification_url(@sent_notification) - end - if Gitlab::IncomingEmail.enabled? address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key)) address.display_name = @project.name_with_namespace @@ -171,4 +167,16 @@ class Notify < BaseMailer headers['X-GitLab-Project-Id'] = @project.id headers['X-GitLab-Project-Path'] = @project.path_with_namespace end + + def add_unsubscription_headers_and_links + return unless !@labels_url && @sent_notification && @sent_notification.unsubscribable? + + list_unsubscribe_methods = [unsubscribe_sent_notification_url(@sent_notification, force: true)] + if Gitlab::IncomingEmail.enabled? && Gitlab::IncomingEmail.supports_wildcard? + list_unsubscribe_methods << "mailto:#{Gitlab::IncomingEmail.unsubscribe_address(reply_key)}" + end + + headers['List-Unsubscribe'] = list_unsubscribe_methods.map { |e| "<#{e}>" }.join(',') + @sent_notification_url = unsubscribe_sent_notification_url(@sent_notification) + end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index bb60cc8736c..8fab77cda0a 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -19,6 +19,7 @@ class ApplicationSetting < ActiveRecord::Base serialize :domain_whitelist, Array serialize :domain_blacklist, Array serialize :repository_storages + serialize :sidekiq_throttling_queues, Array cache_markdown_field :sign_in_text cache_markdown_field :help_page_text @@ -67,6 +68,10 @@ class ApplicationSetting < ActiveRecord::Base presence: true, if: :koding_enabled + validates :plantuml_url, + presence: true, + if: :plantuml_enabled + validates :max_attachment_size, presence: true, numericality: { only_integer: true, greater_than: 0 } @@ -85,6 +90,15 @@ class ApplicationSetting < ActiveRecord::Base presence: { message: 'Domain blacklist cannot be empty if Blacklist is enabled.' }, if: :domain_blacklist_enabled? + validates :sidekiq_throttling_factor, + numericality: { greater_than: 0, less_than: 1 }, + presence: { message: 'Throttling factor cannot be empty if Sidekiq Throttling is enabled.' }, + if: :sidekiq_throttling_enabled? + + validates :sidekiq_throttling_queues, + presence: { message: 'Queues to throttle cannot be empty if Sidekiq Throttling is enabled.' }, + if: :sidekiq_throttling_enabled? + validates :housekeeping_incremental_repack_period, presence: true, numericality: { only_integer: true, greater_than: 0 } @@ -174,12 +188,15 @@ class ApplicationSetting < ActiveRecord::Base akismet_enabled: false, koding_enabled: false, koding_url: nil, + plantuml_enabled: false, + plantuml_url: nil, repository_checks_enabled: true, disabled_oauth_sign_in_sources: [], send_user_confirmation_email: false, container_registry_token_expire_delay: 5, repository_storages: ['default'], user_default_external: false, + sidekiq_throttling_enabled: false, housekeeping_enabled: true, housekeeping_bitmaps_enabled: true, housekeeping_incremental_repack_period: 10, @@ -192,6 +209,10 @@ class ApplicationSetting < ActiveRecord::Base ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url) end + def sidekiq_throttling_column_exists? + ActiveRecord::Base.connection.column_exists?(:application_settings, :sidekiq_throttling_enabled) + end + def domain_whitelist_raw self.domain_whitelist.join("\n") unless self.domain_whitelist.nil? end @@ -245,6 +266,12 @@ class ApplicationSetting < ActiveRecord::Base ensure_health_check_access_token! end + def sidekiq_throttling_enabled? + return false unless sidekiq_throttling_column_exists? + + sidekiq_throttling_enabled + end + private def check_repository_storages diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb new file mode 100644 index 00000000000..f321db75eeb --- /dev/null +++ b/app/models/chat_name.rb @@ -0,0 +1,12 @@ +class ChatName < ActiveRecord::Base + belongs_to :service + belongs_to :user + + validates :user, presence: true + validates :service, presence: true + validates :team_id, presence: true + validates :chat_id, presence: true + + validates :user_id, uniqueness: { scope: [:service_id] } + validates :chat_id, uniqueness: { scope: [:service_id, :team_id] } +end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index bf5f92f8462..5fe8ddf69d7 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -2,13 +2,24 @@ module Ci class Build < CommitStatus include TokenAuthenticatable include AfterCommitQueue + include Presentable belongs_to :runner belongs_to :trigger_request belongs_to :erased_by, class_name: 'User' + has_many :deployments, as: :deployable + + # The "environment" field for builds is a String, and is the unexpanded name + def persisted_environment + @persisted_environment ||= Environment.find_by( + name: expanded_environment_name, + project_id: gl_project_id + ) + end + serialize :options - serialize :yaml_variables + serialize :yaml_variables, Gitlab::Serialize::Ci::Variables validates :coverage, numericality: true, allow_blank: true validates_presence_of :ref @@ -33,6 +44,8 @@ module Ci before_destroy { project } after_create :execute_hooks + after_save :update_project_statistics, if: :artifacts_size_changed? + after_destroy :update_project_statistics class << self def first_pending @@ -68,13 +81,23 @@ module Ci environment: build.environment, status_event: 'enqueue' ) - MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build) + + MergeRequests::AddTodoWhenBuildFailsService + .new(build.project, nil) + .close(new_build) + build.pipeline.mark_as_processable_after_stage(build.stage_idx) new_build end end state_machine :status do + after_transition any => [:pending] do |build| + build.run_after_commit do + BuildQueueWorker.perform_async(id) + end + end + after_transition pending: :running do |build| build.run_after_commit do BuildHooksWorker.perform_async(id) @@ -94,6 +117,12 @@ module Ci end end + def detailed_status(current_user) + Gitlab::Ci::Status::Build::Factory + .new(self, current_user) + .fabricate! + end + def manual? self.when == 'manual' end @@ -117,14 +146,47 @@ module Ci end end + def cancelable? + active? + end + def retryable? - project.builds_enabled? && commands.present? && complete? + project.builds_enabled? && commands.present? && + (success? || failed? || canceled?) end def retried? !self.pipeline.statuses.latest.include?(self) end + def expanded_environment_name + ExpandVariables.expand(environment, simple_variables) if environment + end + + def has_environment? + environment.present? + end + + def starts_environment? + has_environment? && self.environment_action == 'start' + end + + def stops_environment? + has_environment? && self.environment_action == 'stop' + end + + def environment_action + self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options + end + + def outdated_deployment? + success? && !last_deployment.try(:last?) + end + + def last_deployment + deployments.last + end + def depends_on_builds # Get builds of the same type latest_builds = self.pipeline.builds.latest @@ -150,12 +212,25 @@ module Ci project.build_timeout end - def variables + # A slugified version of the build ref, suitable for inclusion in URLs and + # domain names. Rules: + # + # * Lowercased + # * Anything not matching [a-z0-9-] is replaced with a - + # * Maximum length is 63 bytes + def ref_slug + slugified = ref.to_s.downcase + slugified.gsub(/[^a-z0-9]/, '-')[0..62] + end + + # Variables whose value does not depend on other variables + def simple_variables variables = predefined_variables variables += project.predefined_variables variables += pipeline.predefined_variables variables += runner.predefined_variables if runner variables += project.container_registry_variables + variables += project.deployment_variables if has_environment? variables += yaml_variables variables += user_variables variables += project.secret_variables @@ -163,13 +238,20 @@ module Ci variables end + # All variables, including those dependent on other variables + def variables + variables = simple_variables + variables += persisted_environment.predefined_variables if persisted_environment.present? + variables + end + def merge_request merge_requests = MergeRequest.includes(:merge_request_diff) .where(source_branch: ref, source_project_id: pipeline.gl_project_id) .reorder(iid: :asc) merge_requests.find do |merge_request| - merge_request.commits.any? { |ci| ci.id == pipeline.sha } + merge_request.commits_sha.include?(pipeline.sha) end end @@ -271,6 +353,7 @@ module Ci def append_trace(trace_part, offset) recreate_trace_dir + touch if needs_touch? trace_part = hide_secrets(trace_part) @@ -280,6 +363,10 @@ module Ci end end + def needs_touch? + Time.now - updated_at > 15.minutes.to_i + end + def trace_file_path if has_old_trace_file? old_path_to_trace @@ -427,6 +514,10 @@ module Ci end end + def has_expiring_artifacts? + artifacts_expire_at.present? + end + def keep_artifacts! self.update(artifacts_expire_at: nil) end @@ -448,6 +539,10 @@ module Ci ] end + def credentials + Gitlab::Ci::Build::Credentials::Factory.new(self).create! + end + private def update_artifacts_size @@ -475,6 +570,7 @@ module Ci { key: 'CI_BUILD_REF', value: sha, public: true }, { key: 'CI_BUILD_BEFORE_SHA', value: before_sha, public: true }, { key: 'CI_BUILD_REF_NAME', value: ref, public: true }, + { key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true }, { key: 'CI_BUILD_NAME', value: name, public: true }, { key: 'CI_BUILD_STAGE', value: stage, public: true }, { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, @@ -501,5 +597,9 @@ module Ci Ci::MaskSecret.mask!(trace, token) trace end + + def update_project_statistics + ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size]) + end end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 3fee6c18770..fab8497ec7d 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -21,8 +21,6 @@ module Ci after_create :keep_around_commits, unless: :importing? - delegate :stages, to: :statuses - state_machine :status, initial: :created do event :enqueue do transition created: :pending @@ -90,25 +88,66 @@ module Ci end # ref can't be HEAD or SHA, can only be branch/tag name + scope :latest, ->(ref = nil) do + max_id = unscope(:select) + .select("max(#{quoted_table_name}.id)") + .group(:ref, :sha) + + relation = ref ? where(ref: ref) : self + relation.where(id: max_id) + end + + def self.latest_status(ref = nil) + latest(ref).status + end + def self.latest_successful_for(ref) - where(ref: ref).order(id: :desc).success.first + success.latest(ref).order(id: :desc).first end def self.truncate_sha(sha) sha[0...8] end - def self.stages - # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries - CommitStatus.where(pipeline: pluck(:id)).stages - end - def self.total_duration where.not(duration: nil).sum(:duration) end - def stages_with_latest_statuses - statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage) + def stage(name) + stage = Ci::Stage.new(self, name: name) + stage unless stage.statuses_count.zero? + end + + def stages_count + statuses.select(:stage).distinct.count + end + + def stages_name + statuses.order(:stage_idx).distinct. + pluck(:stage, :stage_idx).map(&:first) + end + + def stages + # TODO, this needs refactoring, see gitlab-ce#26481. + + stages_query = statuses + .group('stage').select(:stage).order('max(stage_idx)') + + status_sql = statuses.latest.where('stage=sg.stage').status_sql + + warnings_sql = statuses.latest.select('COUNT(*) > 0') + .where('stage=sg.stage').failed_but_allowed.to_sql + + stages_with_statuses = CommitStatus.from(stages_query, :sg) + .pluck('sg.stage', status_sql, "(#{warnings_sql})") + + stages_with_statuses.map do |stage| + Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)]) + end + end + + def artifacts + builds.latest.with_artifacts_not_expired.includes(project: [:namespace]) end def project_id @@ -157,27 +196,35 @@ module Ci end def manual_actions - builds.latest.manual_actions + builds.latest.manual_actions.includes(project: [:namespace]) + end + + def stuck? + builds.pending.any?(&:stuck?) end def retryable? - builds.latest.any? do |build| - (build.failed? || build.canceled?) && build.retryable? - end + builds.latest.failed_or_canceled.any?(&:retryable?) end def cancelable? - builds.running_or_pending.any? + statuses.cancelable.any? end def cancel_running - builds.running_or_pending.each(&:cancel) + Gitlab::OptimisticLocking.retry_lock( + statuses.cancelable) do |cancelable| + cancelable.each(&:cancel) + end end def retry_failed(user) - builds.latest.failed.select(&:retryable?).each do |build| - Ci::Build.retry(build, user) - end + Gitlab::OptimisticLocking.retry_lock( + builds.latest.failed_or_canceled) do |failed_or_canceled| + failed_or_canceled.select(&:retryable?).each do |build| + Ci::Build.retry(build, user) + end + end end def mark_as_processable_after_stage(stage_idx) @@ -245,6 +292,10 @@ module Ci end end + def has_yaml_errors? + yaml_errors.present? + end + def environments builds.where.not(environment: nil).success.pluck(:environment).uniq end @@ -313,7 +364,13 @@ module Ci def merge_requests @merge_requests ||= project.merge_requests .where(source_branch: self.ref) - .select { |merge_request| merge_request.pipeline.try(:id) == self.id } + .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id } + end + + def detailed_status(current_user) + Gitlab::Ci::Status::Pipeline::Factory + .new(self, current_user) + .fabricate! end private diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 123930273e0..ed1843ba005 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -2,6 +2,7 @@ module Ci class Runner < ActiveRecord::Base extend Ci::Model + RUNNER_QUEUE_EXPIRY_TIME = 60.minutes LAST_CONTACT_TIME = 1.hour.ago AVAILABLE_SCOPES = %w[specific shared active paused online] FORM_EDITABLE = %i[description tag_list active run_untagged locked] @@ -21,6 +22,8 @@ module Ci scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) } scope :ordered, ->() { order(id: :desc) } + after_save :tick_runner_queue, if: :form_editable_changed? + scope :owned_or_shared, ->(project_id) do joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id') .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) @@ -122,8 +125,38 @@ module Ci ] end + def tick_runner_queue + SecureRandom.hex.tap do |new_update| + Gitlab::Redis.with do |redis| + redis.set(runner_queue_key, new_update, ex: RUNNER_QUEUE_EXPIRY_TIME) + end + end + end + + def ensure_runner_queue_value + Gitlab::Redis.with do |redis| + value = SecureRandom.hex + redis.set(runner_queue_key, value, ex: RUNNER_QUEUE_EXPIRY_TIME, nx: true) + redis.get(runner_queue_key) + end + end + + def is_runner_queue_value_latest?(value) + ensure_runner_queue_value == value if value.present? + end + private + def runner_queue_key + "runner:build_queue:#{self.token}" + end + + def form_editable_changed? + FORM_EDITABLE.any? do |editable| + public_send("#{editable}_changed?") + end + end + def tag_constraints unless has_tags? || run_untagged? errors.add(:tags_list, diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb new file mode 100644 index 00000000000..ca74c91b062 --- /dev/null +++ b/app/models/ci/stage.rb @@ -0,0 +1,56 @@ +module Ci + # Currently this is artificial object, constructed dynamically + # We should migrate this object to actual database record in the future + class Stage + include StaticModel + + attr_reader :pipeline, :name + + delegate :project, to: :pipeline + + def initialize(pipeline, name:, status: nil, warnings: nil) + @pipeline = pipeline + @name = name + @status = status + @warnings = warnings + end + + def to_param + name + end + + def statuses_count + @statuses_count ||= statuses.count + end + + def status + @status ||= statuses.latest.status + end + + def detailed_status(current_user) + Gitlab::Ci::Status::Stage::Factory + .new(self, current_user) + .fabricate! + end + + def statuses + @statuses ||= pipeline.statuses.where(stage: name) + end + + def builds + @builds ||= pipeline.builds.where(stage: name) + end + + def success? + status.to_s == 'success' + end + + def has_warnings? + if @warnings.nil? + statuses.latest.failed_but_allowed.any? + else + @warnings + end + end + end +end diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 94d9e2b3208..2c8698d8b5d 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -4,10 +4,10 @@ module Ci belongs_to :project, foreign_key: :gl_project_id - validates_uniqueness_of :key, scope: :gl_project_id validates :key, presence: true, - length: { within: 0..255 }, + uniqueness: { scope: :gl_project_id }, + length: { maximum: 255 }, format: { with: /\A[a-zA-Z0-9_]+\z/, message: "can contain only letters, digits and '_'." } diff --git a/app/models/commit.rb b/app/models/commit.rb index 9e7fde9503d..316bd2e512b 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -21,6 +21,9 @@ class Commit DIFF_HARD_LIMIT_FILES = 1000 DIFF_HARD_LIMIT_LINES = 50000 + # The SHA can be between 7 and 40 hex characters. + COMMIT_SHA_PATTERN = '\h{7,40}' + class << self def decorate(commits, project) commits.map do |commit| @@ -48,6 +51,14 @@ class Commit max_lines: DIFF_HARD_LIMIT_LINES, } end + + def from_hash(hash, project) + new(Gitlab::Git::Commit.new(hash), project) + end + + def valid_hash?(key) + !!(/\A#{COMMIT_SHA_PATTERN}\z/ =~ key) + end end attr_accessor :raw @@ -73,8 +84,6 @@ class Commit # Pattern used to extract commit references from text # - # The SHA can be between 7 and 40 hex characters. - # # This pattern supports cross-project references. def self.reference_pattern @reference_pattern ||= %r{ @@ -84,23 +93,15 @@ class Commit end def self.link_reference_pattern - @link_reference_pattern ||= super("commit", /(?<commit>\h{7,40})/) + @link_reference_pattern ||= super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})/) end - def to_reference(from_project = nil) - if cross_project_reference?(from_project) - project.to_reference + self.class.reference_prefix + self.id - else - self.id - end + def to_reference(from_project = nil, full: false) + commit_reference(from_project, id, full: full) end def reference_link_text(from_project = nil) - if cross_project_reference?(from_project) - project.to_reference + self.class.reference_prefix + self.short_id - else - self.short_id - end + commit_reference(from_project, short_id) end def diff_line_count @@ -232,13 +233,9 @@ class Commit def status(ref = nil) @statuses ||= {} - if @statuses.key?(ref) - @statuses[ref] - elsif ref - @statuses[ref] = pipelines.where(ref: ref).status - else - @statuses[ref] = pipelines.status - end + return @statuses[ref] if @statuses.key?(ref) + + @statuses[ref] = pipelines.latest_status(ref) end def revert_branch_name @@ -249,44 +246,47 @@ class Commit project.repository.next_branch("cherry-pick-#{short_id}", mild: true) end - def revert_description - if merged_merge_request - "This reverts merge request #{merged_merge_request.to_reference}" + def revert_description(user) + if merged_merge_request?(user) + "This reverts merge request #{merged_merge_request(user).to_reference}" else "This reverts commit #{sha}" end end - def revert_message - %Q{Revert "#{title.strip}"\n\n#{revert_description}} + def revert_message(user) + %Q{Revert "#{title.strip}"\n\n#{revert_description(user)}} end - def reverts_commit?(commit) - description? && description.include?(commit.revert_description) + def reverts_commit?(commit, user) + description? && description.include?(commit.revert_description(user)) end def merge_commit? parents.size > 1 end - def merged_merge_request - return @merged_merge_request if defined?(@merged_merge_request) + def merged_merge_request(current_user) + # Memoize with per-user access check + @merged_merge_request_hash ||= Hash.new do |hash, user| + hash[user] = merged_merge_request_no_cache(user) + end - @merged_merge_request = project.merge_requests.find_by(merge_commit_sha: id) if merge_commit? + @merged_merge_request_hash[current_user] end - def has_been_reverted?(current_user = nil, noteable = self) + def has_been_reverted?(current_user, noteable = self) ext = all_references(current_user) noteable.notes_with_associations.system.each do |note| note.all_references(current_user, extractor: ext) end - ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self) } + ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self, current_user) } end - def change_type_title - merged_merge_request ? 'merge request' : 'commit' + def change_type_title(user) + merged_merge_request?(user) ? 'merge request' : 'commit' end # Get the URI type of the given path @@ -323,8 +323,32 @@ 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) + reference = project.to_reference(from_project, full: full) + + if reference.present? + "#{reference}#{self.class.reference_prefix}#{referable_commit_id}" + else + referable_commit_id + end + end + def find_author_by_any_email User.find_by_any_email(author_email.downcase) end @@ -344,4 +368,12 @@ class Commit changes end + + def merged_merge_request?(user) + !!merged_merge_request(user) + end + + def merged_merge_request_no_cache(user) + MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit? + end end diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index ac2477fd973..84e2e8a5dd5 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -89,22 +89,25 @@ class CommitRange alias_method :id, :to_s - def to_reference(from_project = nil) - if cross_project_reference?(from_project) - project.to_reference + self.class.reference_prefix + self.id + def to_reference(from_project = nil, full: false) + project_reference = project.to_reference(from_project, full: full) + + if project_reference.present? + project_reference + self.class.reference_prefix + self.id else self.id end end def reference_link_text(from_project = nil) - reference = ref_from + notation + ref_to + project_reference = project.to_reference(from_project) + reference = ref_from + notation + ref_to - if cross_project_reference?(from_project) - reference = project.to_reference + self.class.reference_prefix + reference + if project_reference.present? + project_reference + self.class.reference_prefix + reference + else + reference end - - reference end # Return a Hash of parameters for passing to a URL helper diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index d159fc6c5c7..9547c57b2ae 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -31,18 +31,13 @@ class CommitStatus < ActiveRecord::Base end scope :exclude_ignored, -> do - quoted_when = connection.quote_column_name('when') # We want to ignore failed_but_allowed jobs where("allow_failure = ? OR status IN (?)", - false, all_state_names - [:failed, :canceled]). - # We want to ignore skipped manual jobs - where("#{quoted_when} <> ? OR status <> ?", 'manual', 'skipped'). - # We want to ignore skipped on_failure - where("#{quoted_when} <> ? OR status <> ?", 'on_failure', 'skipped') + false, all_state_names - [:failed, :canceled]) end - scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) } - scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) } + scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) } + scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } state_machine :status do event :enqueue do @@ -117,33 +112,35 @@ class CommitStatus < ActiveRecord::Base name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip end - def self.stages - # We group by stage name, but order stages by theirs' index - unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage') - end - - def self.stages_status - # We execute subquery for each stage to calculate a stage status - statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql) - statuses.inject({}) do |h, k| - h[k.first] = k.last - h - end - end - def failed_but_allowed? allow_failure? && (failed? || canceled?) end + def duration + calculate_duration + end + def playable? false end - def duration - calculate_duration + def stuck? + false end - def stuck? + def has_trace? false end + + def detailed_status(current_user) + Gitlab::Ci::Status::Factory + .new(self, current_user) + .fabricate! + end + + def sortable_name + name.split(/(\d+)/).map do |v| + v =~ /\d+/ ? v.to_i : v + end + end end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index ef3e73a4072..431c0354969 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -1,10 +1,11 @@ module HasStatus extend ActiveSupport::Concern + DEFAULT_STATUS = 'created' AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped] STARTED_STATUSES = %w[running success failed skipped] ACTIVE_STATUSES = %w[pending running] - COMPLETED_STATUSES = %w[success failed canceled] + COMPLETED_STATUSES = %w[success failed canceled skipped] ORDERED_STATUSES = %w[failed pending running canceled success skipped] class_methods do @@ -23,9 +24,10 @@ module HasStatus canceled = scope.canceled.select('count(*)').to_sql "(CASE + WHEN (#{builds})=(#{skipped}) THEN 'skipped' WHEN (#{builds})=(#{success}) THEN 'success' WHEN (#{builds})=(#{created}) THEN 'created' - WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'skipped' + WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success' WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled' WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending' WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running' @@ -73,6 +75,11 @@ module HasStatus scope :skipped, -> { where(status: 'skipped') } scope :running_or_pending, -> { where(status: [:running, :pending]) } scope :finished, -> { where(status: [:success, :failed, :canceled]) } + scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) } + + scope :cancelable, -> do + where(status: [:running, :pending, :created]) + end end def started? diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 664bb594aa9..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 @@ -41,7 +42,7 @@ module Issuable has_one :metrics validates :author, presence: true - validates :title, presence: true, length: { within: 0..255 } + validates :title, presence: true, length: { maximum: 255 } scope :authored, ->(user) { where(author_id: user) } scope :assigned_to, ->(u) { where(assignee_id: u.id)} @@ -92,8 +93,9 @@ module Issuable after_save :record_metrics def update_assignee_cache_counts - # make sure we flush the cache for both the old *and* new assignee - User.find(assignee_id_was).update_cache_counts if assignee_id_was + # make sure we flush the cache for both the old *and* new assignees(if they exist) + previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was + previous_assignee.update_cache_counts if previous_assignee assignee.update_cache_counts if assignee end @@ -215,7 +217,7 @@ module Issuable end end - def subscribed_without_subscriptions?(user) + def subscribed_without_subscriptions?(user, project) participants(user).include?(user) end @@ -251,6 +253,17 @@ module Issuable self.class.to_ability_name end + # Convert this Issuable class name to a format usable by notifications. + # + # Examples: + # + # issuable.class # => MergeRequest + # issuable.human_class_name # => "merge request" + + def human_class_name + @human_class_name ||= self.class.name.titleize.downcase + end + # Returns a Hash of attributes to be used for Twitter card metadata def card_attributes { diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index eb2ff0428f6..8ab0401d288 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -1,6 +1,6 @@ # == Mentionable concern # -# Contains functionality related to objects that can mention Users, Issues, MergeRequests, or Commits by +# Contains functionality related to objects that can mention Users, Issues, MergeRequests, Commits or Snippets by # GFM references. # # Used by Issue, Note, MergeRequest, and Commit. diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 7bcc78247ba..e9450dd0c26 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -1,17 +1,25 @@ module Milestoneish - def closed_items_count(user = nil) - issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size + def closed_items_count(user) + memoize_per_user(user, :closed_items_count) do + (count_issues_by_state(user)['closed'] || 0) + merge_requests.closed_and_merged.size + end end - def total_items_count(user = nil) - issues_visible_to_user(user).size + merge_requests.size + def total_items_count(user) + memoize_per_user(user, :total_items_count) do + total_issues_count(user) + merge_requests.size + end end - def complete?(user = nil) + 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 - def percent_complete(user = nil) + def percent_complete(user) ((closed_items_count(user) * 100) / total_items_count(user)).abs rescue ZeroDivisionError 0 @@ -23,7 +31,54 @@ module Milestoneish (due_date - Date.today).to_i end - def issues_visible_to_user(user = nil) - issues.visible_to_user(user) + def elapsed_days + return 0 if !start_date || start_date.future? + + (Date.today - start_date).to_i + end + + def issues_visible_to_user(user) + memoize_per_user(user, :issues_visible_to_user) do + IssuesFinder.new(user, issues_finder_params) + .execute.where(milestone_id: milestoneish_ids) + end + end + + def upcoming? + start_date && start_date.future? + end + + def expires_at + if due_date + if due_date.past? + "expired on #{due_date.to_s(:medium)}" + else + "expires on #{due_date.to_s(:medium)}" + end + end + end + + def expired? + due_date && due_date.past? + end + + private + + def count_issues_by_state(user) + memoize_per_user(user, :count_issues_by_state) do + issues_visible_to_user(user).reorder(nil).group(:state).count + end + end + + def memoize_per_user(user, method_name) + @memoized ||= {} + @memoized[method_name] ||= {} + @memoized[method_name][user.try!(:id)] ||= yield + end + + # override in a class that includes this module to get a faster query + # from IssuesFinder + def issues_finder_params + {} end end diff --git a/app/models/concerns/presentable.rb b/app/models/concerns/presentable.rb new file mode 100644 index 00000000000..7b33b837004 --- /dev/null +++ b/app/models/concerns/presentable.rb @@ -0,0 +1,7 @@ +module Presentable + def present(**attributes) + Gitlab::View::Presenter::Factory + .new(self, attributes) + .fabricate! + end +end diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 6d88951c713..60734bc6660 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -32,6 +32,6 @@ module ProjectFeaturesCompatibility build_project_feature unless project_feature access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED - project_feature.update_attribute(field, access_level) + project_feature.send(:write_attribute, field, access_level) end end diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb index 7fd0905ee81..9dd4d9c6f24 100644 --- a/app/models/concerns/protected_branch_access.rb +++ b/app/models/concerns/protected_branch_access.rb @@ -2,6 +2,9 @@ module ProtectedBranchAccess extend ActiveSupport::Concern included do + belongs_to :protected_branch + delegate :project, to: :protected_branch + scope :master, -> { where(access_level: Gitlab::Access::MASTER) } scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } end @@ -9,4 +12,10 @@ module ProtectedBranchAccess def humanize self.class.human_access_levels[self.access_level] end + + def check_access(user) + return true if user.is_admin? + + project.team.max_member_access(user.id) >= access_level + end end diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb new file mode 100644 index 00000000000..2589215ad19 --- /dev/null +++ b/app/models/concerns/reactive_caching.rb @@ -0,0 +1,118 @@ +# The ReactiveCaching concern is used to fetch some data in the background and +# store it in the Rails cache, keeping it up-to-date for as long as it is being +# requested. If the data hasn't been requested for +reactive_cache_lifetime+, +# it stop being refreshed, and then be removed. +# +# Example of use: +# +# class Foo < ActiveRecord::Base +# include ReactiveCaching +# +# self.reactive_cache_key = ->(thing) { ["foo", thing.id] } +# +# after_save :clear_reactive_cache! +# +# def calculate_reactive_cache +# # Expensive operation here. The return value of this method is cached +# end +# +# def result +# with_reactive_cache do |data| +# # ... +# end +# end +# end +# +# In this example, the first time `#result` is called, it will return `nil`. +# However, it will enqueue a background worker to call `#calculate_reactive_cache` +# and set an initial cache lifetime of ten minutes. +# +# Each time the background job completes, it stores the return value of +# `#calculate_reactive_cache`. It is also re-enqueued to run again after +# `reactive_cache_refresh_interval`, so keeping the stored value up to date. +# Calculations are never run concurrently. +# +# Calling `#result` while a value is in the cache will call the block given to +# `#with_reactive_cache`, yielding the cached value. It will also extend the +# lifetime by `reactive_cache_lifetime`. +# +# Once the lifetime has expired, no more background jobs will be enqueued and +# calling `#result` will again return `nil` - starting the process all over +# again +module ReactiveCaching + extend ActiveSupport::Concern + + included do + class_attribute :reactive_cache_lease_timeout + + class_attribute :reactive_cache_key + class_attribute :reactive_cache_lifetime + class_attribute :reactive_cache_refresh_interval + + # defaults + self.reactive_cache_lease_timeout = 2.minutes + + self.reactive_cache_refresh_interval = 1.minute + self.reactive_cache_lifetime = 10.minutes + + def calculate_reactive_cache(*args) + raise NotImplementedError + end + + def with_reactive_cache(*args, &blk) + within_reactive_cache_lifetime(*args) do + data = Rails.cache.read(full_reactive_cache_key(*args)) + yield data if data.present? + end + ensure + Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime) + ReactiveCachingWorker.perform_async(self.class, id, *args) + end + + def clear_reactive_cache!(*args) + Rails.cache.delete(full_reactive_cache_key(*args)) + end + + def exclusively_update_reactive_cache!(*args) + locking_reactive_cache(*args) do + within_reactive_cache_lifetime(*args) do + enqueuing_update(*args) do + value = calculate_reactive_cache(*args) + Rails.cache.write(full_reactive_cache_key(*args), value) + end + end + end + end + + private + + def full_reactive_cache_key(*qualifiers) + prefix = self.class.reactive_cache_key + prefix = prefix.call(self) if prefix.respond_to?(:call) + + ([prefix].flatten + qualifiers).join(':') + end + + def alive_reactive_cache_key(*qualifiers) + full_reactive_cache_key(*(qualifiers + ['alive'])) + end + + def locking_reactive_cache(*args) + lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key(*args), timeout: reactive_cache_lease_timeout) + uuid = lease.try_obtain + yield if uuid + ensure + Gitlab::ExclusiveLease.cancel(full_reactive_cache_key(*args), uuid) + end + + def within_reactive_cache_lifetime(*args) + yield if Rails.cache.read(alive_reactive_cache_key(*args)) + end + + def enqueuing_update(*args) + yield + ensure + ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id, *args) + end + end +end diff --git a/app/models/concerns/reactive_service.rb b/app/models/concerns/reactive_service.rb new file mode 100644 index 00000000000..e1f868a299b --- /dev/null +++ b/app/models/concerns/reactive_service.rb @@ -0,0 +1,10 @@ +module ReactiveService + extend ActiveSupport::Concern + + included do + include ReactiveCaching + + # Default cache key: class name + project_id + self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] } + end +end diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb index dee940a3f88..da803c7f481 100644 --- a/app/models/concerns/referable.rb +++ b/app/models/concerns/referable.rb @@ -17,7 +17,7 @@ module Referable # Issue.last.to_reference(other_project) # => "cross-project#1" # # Returns a String - def to_reference(_from_project = nil) + def to_reference(_from_project = nil, full:) '' end @@ -72,17 +72,4 @@ module Referable }x end end - - private - - # Check if a reference is being done cross-project - # - # from_project - Refering Project object - def cross_project_reference?(from_project) - if self.is_a?(Project) - self != from_project - else - from_project && self.project && self.project != from_project - end - end end diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb new file mode 100644 index 00000000000..1108a64c59e --- /dev/null +++ b/app/models/concerns/routable.rb @@ -0,0 +1,71 @@ +# Store object full path in separate table for easy lookup and uniq validation +# Object must have path db field and respond to full_path and full_path_changed? methods. +module Routable + extend ActiveSupport::Concern + + included do + has_one :route, as: :source, autosave: true, dependent: :destroy + + validates_associated :route + validates :route, presence: true + + before_validation :update_route_path, if: :full_path_changed? + end + + class_methods do + # Finds a single object by full path match in routes table. + # + # Usage: + # + # Klass.find_by_full_path('gitlab-org/gitlab-ce') + # + # Returns a single object, or nil. + def find_by_full_path(path) + # On MySQL we want to ensure the ORDER BY uses a case-sensitive match so + # any literal matches come first, for this we have to use "BINARY". + # Without this there's still no guarantee in what order MySQL will return + # rows. + binary = Gitlab::Database.mysql? ? 'BINARY' : '' + + order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)" + + where_full_path_in([path]).reorder(order_sql).take + end + + # Builds a relation to find multiple objects by their full paths. + # + # Usage: + # + # Klass.where_full_path_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee}) + # + # Returns an ActiveRecord::Relation. + def where_full_path_in(paths) + wheres = [] + cast_lower = Gitlab::Database.postgresql? + + paths.each do |path| + path = connection.quote(path) + where = "(routes.path = #{path})" + + if cast_lower + where = "(#{where} OR (LOWER(routes.path) = LOWER(#{path})))" + end + + wheres << where + end + + if wheres.empty? + none + else + joins(:route).where(wheres.join(' OR ')) + end + end + end + + private + + def update_route_path + route || build_route(source: self) + route.path = full_path + end +end diff --git a/app/models/concerns/select_for_project_authorization.rb b/app/models/concerns/select_for_project_authorization.rb new file mode 100644 index 00000000000..50a1d7fc3e1 --- /dev/null +++ b/app/models/concerns/select_for_project_authorization.rb @@ -0,0 +1,9 @@ +module SelectForProjectAuthorization + extend ActiveSupport::Concern + + module ClassMethods + def select_for_project_authorization + select("members.user_id, projects.id AS project_id, members.access_level") + end + end +end diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb index 083257f1005..83daa9b1a64 100644 --- a/app/models/concerns/subscribable.rb +++ b/app/models/concerns/subscribable.rb @@ -12,39 +12,71 @@ module Subscribable has_many :subscriptions, dependent: :destroy, as: :subscribable end - def subscribed?(user) - if subscription = subscriptions.find_by_user_id(user.id) + def subscribed?(user, project = nil) + if subscription = subscriptions.find_by(user: user, project: project) subscription.subscribed else - subscribed_without_subscriptions?(user) + subscribed_without_subscriptions?(user, project) end end # Override this method to define custom logic to consider a subscribable as # subscribed without an explicit subscription record. - def subscribed_without_subscriptions?(user) + def subscribed_without_subscriptions?(user, project) false end - def subscribers - subscriptions.where(subscribed: true).map(&:user) + def subscribers(project) + subscriptions_available(project). + where(subscribed: true). + map(&:user) end - def toggle_subscription(user) - subscriptions. - find_or_initialize_by(user_id: user.id). - update(subscribed: !subscribed?(user)) + def toggle_subscription(user, project = nil) + unsubscribe_from_other_levels(user, project) + + find_or_initialize_subscription(user, project). + update(subscribed: !subscribed?(user, project)) + end + + def subscribe(user, project = nil) + unsubscribe_from_other_levels(user, project) + + find_or_initialize_subscription(user, project) + .update(subscribed: true) + end + + def unsubscribe(user, project = nil) + unsubscribe_from_other_levels(user, project) + + find_or_initialize_subscription(user, project) + .update(subscribed: false) end - def subscribe(user) + private + + def unsubscribe_from_other_levels(user, project) + other_subscriptions = subscriptions.where(user: user) + + other_subscriptions = + if project.blank? + other_subscriptions.where.not(project: nil) + else + other_subscriptions.where(project: nil) + end + + other_subscriptions.update_all(subscribed: false) + end + + def find_or_initialize_subscription(user, project) subscriptions. - find_or_initialize_by(user_id: user.id). - update(subscribed: true) + find_or_initialize_by(user_id: user.id, project_id: project.try(:id)) end - def unsubscribe(user) + def subscriptions_available(project) + t = Subscription.arel_table + subscriptions. - find_or_initialize_by(user_id: user.id). - update(subscribed: false) + where(t[:project_id].eq(nil).or(t[:project_id].eq(project.try(:id)))) 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/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 04d30f46210..1ca7f91dc03 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -39,6 +39,10 @@ module TokenAuthenticatable current_token.blank? ? write_new_token(token_field) : current_token end + define_method("set_#{token_field}") do |token| + write_attribute(token_field, token) if token + end + define_method("ensure_#{token_field}!") do send("reset_#{token_field}!") if read_attribute(token_field).blank? read_attribute(token_field) diff --git a/app/models/concerns/valid_attribute.rb b/app/models/concerns/valid_attribute.rb new file mode 100644 index 00000000000..8c35cea8d58 --- /dev/null +++ b/app/models/concerns/valid_attribute.rb @@ -0,0 +1,10 @@ +module ValidAttribute + extend ActiveSupport::Concern + + # Checks whether an attribute has failed validation or not + # + # +attribute+ The symbolised name of the attribute i.e :name + def valid_attribute?(attribute) + self.errors.empty? || self.errors.messages[attribute].nil? + end +end diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index 8ed4a56b19b..d2e626c22e8 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -1,103 +1,38 @@ class CycleAnalytics - include Gitlab::Database::Median - include Gitlab::Database::DateTime + STAGES = %i[issue plan code test review staging production].freeze - DEPLOYMENT_METRIC_STAGES = %i[production staging] - - def initialize(project, from:) + def initialize(project, options) @project = project - @from = from + @options = options end def summary - @summary ||= Summary.new(@project, from: @from) - end - - def issue - 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]]) - end - - def plan - 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 - calculate_metric(:code, - Issue::Metrics.arel_table[:first_mentioned_in_commit_at], - MergeRequest.arel_table[:created_at]) + @summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project, + from: @options[:from], + current_user: @options[:current_user]).data end - def test - calculate_metric(:test, - MergeRequest::Metrics.arel_table[:latest_build_started_at], - MergeRequest::Metrics.arel_table[:latest_build_finished_at]) + def stats + @stats ||= stats_per_stage end - def review - calculate_metric(:review, - MergeRequest.arel_table[:created_at], - MergeRequest::Metrics.arel_table[:merged_at]) + def no_stats? + stats.all? { |hash| hash[:value].nil? } end - def staging - calculate_metric(:staging, - MergeRequest::Metrics.arel_table[:merged_at], - MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) + def permissions(user:) + Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project) end - def production - calculate_metric(:production, - Issue.arel_table[:created_at], - MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) + def [](stage_name) + Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options) end private - 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), end_time_attrs, start_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) - arel_table = MergeRequestsClosingIssues.arel_table - - # Load issues - query = arel_table.join(Issue.arel_table).on(Issue.arel_table[:id].eq(arel_table[:issue_id])). - join(Issue::Metrics.arel_table).on(Issue.arel_table[:id].eq(Issue::Metrics.arel_table[:issue_id])). - where(Issue.arel_table[:project_id].eq(@project.id)). - where(Issue.arel_table[:deleted_at].eq(nil)). - where(Issue.arel_table[:created_at].gteq(@from)) - - # Load merge_requests - query = query.join(MergeRequest.arel_table, Arel::Nodes::OuterJoin). - on(MergeRequest.arel_table[:id].eq(arel_table[:merge_request_id])). - join(MergeRequest::Metrics.arel_table). - on(MergeRequest.arel_table[:id].eq(MergeRequest::Metrics.arel_table[:merge_request_id])) - - if DEPLOYMENT_METRIC_STAGES.include?(name) - # Limit to merge requests that have been deployed to production after `@from` - query.where(MergeRequest::Metrics.arel_table[:first_deployed_to_production_at].gteq(@from)) + def stats_per_stage + STAGES.map do |stage_name| + self[stage_name].as_json end - - query end end diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb index b46db449bf3..e69de29bb2d 100644 --- a/app/models/cycle_analytics/summary.rb +++ b/app/models/cycle_analytics/summary.rb @@ -1,42 +0,0 @@ -class CycleAnalytics - class Summary - def initialize(project, from:) - @project = project - @from = from - end - - def new_issues - @project.issues.created_after(@from).count - end - - def commits - ref = @project.default_branch.presence - count_commits_for(ref) - end - - def deploys - @project.deployments.where("created_at > ?", @from).count - end - - private - - # Don't use the `Gitlab::Git::Repository#log` method, because it enforces - # a limit. Since we need a commit count, we _can't_ enforce a limit, so - # the easiest way forward is to replicate the relevant portions of the - # `log` function here. - def count_commits_for(ref) - return unless ref - - repository = @project.repository.raw_repository - sha = @project.repository.commit(ref).sha - - cmd = %W(git --git-dir=#{repository.path} log) - cmd << '--format=%H' - cmd << "--after=#{@from.iso8601}" - cmd << sha - - raw_output = IO.popen(cmd) { |io| io.read } - raw_output.lines.count - end - end -end diff --git a/app/models/dashboard_milestone.rb b/app/models/dashboard_milestone.rb new file mode 100644 index 00000000000..646c1e5ce1a --- /dev/null +++ b/app/models/dashboard_milestone.rb @@ -0,0 +1,5 @@ +class DashboardMilestone < GlobalMilestone + def issues_finder_params + { authorized_only: true } + end +end diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index 2c525d4cd7a..053f2a11aa0 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -20,4 +20,18 @@ class DeployKey < Key def destroyed_when_orphaned? self.private? end + + def has_access_to?(project) + projects.include?(project) + end + + def can_push_to?(project) + can_push? && has_access_to?(project) + end + + private + + # we don't want to notify the user for deploy keys + def notify_user + end end diff --git a/app/models/discussion.rb b/app/models/discussion.rb index de06c13481a..bbe813db823 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -25,7 +25,12 @@ class Discussion to: :last_resolved_note, allow_nil: true - delegate :blob, :highlighted_diff_lines, to: :diff_file, allow_nil: true + delegate :blob, + :highlighted_diff_lines, + :diff_lines, + + to: :diff_file, + allow_nil: true def self.for_notes(notes) notes.group_by(&:discussion_id).values.map { |notes| new(notes) } @@ -83,6 +88,10 @@ class Discussion @first_note ||= @notes.first end + def first_note_to_resolve + @first_note_to_resolve ||= notes.detect(&:to_be_resolved?) + end + def last_note @last_note ||= @notes.last end @@ -159,10 +168,11 @@ class Discussion end # Returns an array of at most 16 highlighted lines above a diff note - def truncated_diff_lines + def truncated_diff_lines(highlight: true) + lines = highlight ? highlighted_diff_lines : diff_lines prev_lines = [] - highlighted_diff_lines.each do |line| + lines.each do |line| if line.meta? prev_lines.clear else diff --git a/app/models/environment.rb b/app/models/environment.rb index 5c662bbab87..8bca47373f8 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -1,25 +1,38 @@ class Environment < ActiveRecord::Base + # Used to generate random suffixes for the slug + NUMBERS = '0'..'9' + SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a + belongs_to :project, required: true, validate: true has_many :deployments before_validation :nullify_external_url + before_validation :generate_slug, if: ->(env) { env.slug.blank? } + before_save :set_environment_type validates :name, presence: true, uniqueness: { scope: :project_id }, - length: { within: 0..255 }, + length: { maximum: 255 }, format: { with: Gitlab::Regex.environment_name_regex, message: Gitlab::Regex.environment_name_regex_message } + validates :slug, + presence: true, + uniqueness: { scope: :project_id }, + length: { maximum: 24 }, + format: { with: Gitlab::Regex.environment_slug_regex, + message: Gitlab::Regex.environment_slug_regex_message } + validates :external_url, uniqueness: { scope: :project_id }, length: { maximum: 255 }, allow_nil: true, addressable_url: true - delegate :stop_action, to: :last_deployment, allow_nil: true + delegate :stop_action, :manual_actions, to: :last_deployment, allow_nil: true scope :available, -> { with_state(:available) } scope :stopped, -> { with_state(:stopped) } @@ -37,6 +50,17 @@ class Environment < ActiveRecord::Base state :stopped end + def predefined_variables + [ + { key: 'CI_ENVIRONMENT_NAME', value: name, public: true }, + { key: 'CI_ENVIRONMENT_SLUG', value: slug, public: true }, + ] + end + + def recently_updated_on_branch?(ref) + ref.to_s == last_deployment.try(:ref) + end + def last_deployment deployments.last end @@ -63,7 +87,7 @@ class Environment < ActiveRecord::Base end def update_merge_request_metrics? - self.name == "production" + (environment_type || name) == "production" end def first_deployment_for(commit) @@ -92,11 +116,60 @@ class Environment < ActiveRecord::Base def run_stop!(current_user) return unless available? - if stop_action.present? - stop_action.play(current_user) - else - stop - nil + stop + stop_action.play(current_user) + end + + def actions_for(environment) + return [] unless manual_actions + + manual_actions.select do |action| + action.expanded_environment_name == environment end end + + def has_terminals? + project.deployment_service.present? && available? && last_deployment.present? + end + + def terminals + project.deployment_service.terminals(self) if has_terminals? + end + + # An environment name is not necessarily suitable for use in URLs, DNS + # or other third-party contexts, so provide a slugified version. A slug has + # the following properties: + # * contains only lowercase letters (a-z), numbers (0-9), and '-' + # * begins with a letter + # * has a maximum length of 24 bytes (OpenShift limitation) + # * cannot end with `-` + def generate_slug + # Lowercase letters and numbers only + slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-') + + # Must start with a letter + slugified = "env-" + slugified if NUMBERS.cover?(slugified[0]) + + # Maximum length: 24 characters (OpenShift limitation) + slugified = slugified[0..23] + + # Cannot end with a "-" character (Kubernetes label limitation) + slugified = slugified[0..-2] if slugified[-1] == "-" + + # Add a random suffix, shortening the current string if necessary, if it + # has been slugified. This ensures uniqueness. + slugified = slugified[0..16] + "-" + random_suffix if slugified != name + + self.slug = slugified + end + + private + + # Slugifying a name may remove the uniqueness guarantee afforded by it being + # based on name (which must be unique). To compensate, we add a random + # 6-byte suffix in those circumstances. This is not *guaranteed* uniqueness, + # but the chance of collisions is vanishingly small + def random_suffix + (0..5).map { SUFFIX_CHARS.sample }.join + end end diff --git a/app/models/event.rb b/app/models/event.rb index c76d88b1c7b..2662f170765 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -43,12 +43,6 @@ class Event < ActiveRecord::Base scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) } class << self - def reset_event_cache_for(target) - Event.where(target_id: target.id, target_type: target.class.to_s). - order('id DESC').limit(100). - update_all(updated_at: Time.now) - end - # Update Gitlab::ContributionsCalendar#activity_dates if this changes def contributions where("action = ? OR (target_type in (?) AND action in (?))", @@ -62,7 +56,7 @@ class Event < ActiveRecord::Base end def visible_to_user?(user = nil) - if push? + if push? || commit_note? Ability.allowed?(user, :download_code, project) elsif membership_changed? true @@ -283,7 +277,7 @@ class Event < ActiveRecord::Base end def commit_note? - target.for_commit? + note? && target && target.for_commit? end def issue_note? @@ -295,7 +289,7 @@ class Event < ActiveRecord::Base end def project_snippet_note? - target.for_snippet? + note? && target && target.for_snippet? end def note_target @@ -353,6 +347,10 @@ class Event < ActiveRecord::Base update_all(last_activity_at: created_at) end + def authored_by?(user) + user ? author_id == user.id : false + end + private def recent_update? diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 91b508eb325..26712c19b5a 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -38,7 +38,7 @@ class ExternalIssue @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} end - def to_reference(_from_project = nil) + def to_reference(_from_project = nil, full: nil) id end diff --git a/app/models/forked_project_link.rb b/app/models/forked_project_link.rb index 9803bae0bee..36cf7ad6a28 100644 --- a/app/models/forked_project_link.rb +++ b/app/models/forked_project_link.rb @@ -1,4 +1,4 @@ class ForkedProjectLink < ActiveRecord::Base - belongs_to :forked_to_project, class_name: Project - belongs_to :forked_from_project, class_name: Project + belongs_to :forked_to_project, class_name: 'Project' + belongs_to :forked_from_project, class_name: 'Project' end diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb index fa54e3540d0..8867ba0d2ff 100644 --- a/app/models/generic_commit_status.rb +++ b/app/models/generic_commit_status.rb @@ -1,6 +1,10 @@ class GenericCommitStatus < CommitStatus before_validation :set_default_values + validates :target_url, addressable_url: true, + length: { maximum: 255 }, + allow_nil: true + # GitHub compatible API alias_attribute :context, :name @@ -12,4 +16,10 @@ class GenericCommitStatus < CommitStatus def tags [:external] end + + def detailed_status(current_user) + Gitlab::Ci::Status::External::Factory + .new(self, current_user) + .fabricate! + end end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index cde4a568577..b991d78e27f 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -1,6 +1,8 @@ class GlobalMilestone include Milestoneish + EPOCH = DateTime.parse('1970-01-01') + attr_accessor :title, :milestones alias_attribute :name, :title @@ -8,13 +10,22 @@ class GlobalMilestone @first_milestone end - def self.build_collection(milestones) - milestones = milestones.group_by(&:title) + def self.build_collection(projects, params) + child_milestones = MilestonesFinder.new.execute(projects, params) - milestones.map do |title, milestones| - milestones_relation = Milestone.where(id: milestones.map(&:id)) + milestones = child_milestones.select(:id, :title).group_by(&:title).map do |title, grouped| + milestones_relation = Milestone.where(id: grouped.map(&:id)) new(title, milestones_relation) end + + milestones.sort_by { |milestone| milestone.due_date || EPOCH } + end + + def self.build(projects, title) + child_milestones = Milestone.of_projects(projects).where(title: title) + return if child_milestones.blank? + + new(title, child_milestones) end def initialize(title, milestones) @@ -24,30 +35,24 @@ class GlobalMilestone @first_milestone = milestones.find {|m| m.description.present? } || milestones.first end - def safe_title - @title.to_slug.normalize.to_s + def milestoneish_ids + milestones.select(:id) end - def expired? - if due_date - due_date.past? - else - false - end + def safe_title + @title.to_slug.normalize.to_s end def projects - @projects ||= Project.for_milestones(milestones.select(:id)) + @projects ||= Project.for_milestones(milestoneish_ids) end def state - state = milestones.map { |milestone| milestone.state } - - if state.count('closed') == state.size - 'closed' - else - 'active' + milestones.each do |milestone| + return 'active' if milestone.state != 'closed' end + + 'closed' end def active? @@ -59,11 +64,11 @@ class GlobalMilestone end def issues - @issues ||= Issue.of_milestones(milestones.select(:id)).includes(:project, :assignee, :labels) + @issues ||= Issue.of_milestones(milestoneish_ids).includes(:project, :assignee, :labels) end def merge_requests - @merge_requests ||= MergeRequest.of_milestones(milestones.select(:id)).includes(:target_project, :assignee, :labels) + @merge_requests ||= MergeRequest.of_milestones(milestoneish_ids).includes(:target_project, :assignee, :labels) end def participants @@ -81,18 +86,15 @@ class GlobalMilestone @due_date = if @milestones.all? { |x| x.due_date == @milestones.first.due_date } @milestones.first.due_date - else - nil end end - def expires_at - if due_date - if due_date.past? - "expired on #{due_date.to_s(:medium)}" - else - "expires on #{due_date.to_s(:medium)}" + def start_date + return @start_date if defined?(@start_date) + + @start_date = + if @milestones.all? { |x| x.start_date == @milestones.first.start_date } + @milestones.first.start_date end - end end end diff --git a/app/models/group.rb b/app/models/group.rb index d9e90cd256a..99675ddb366 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -5,6 +5,7 @@ class Group < Namespace include Gitlab::VisibilityLevel include AccessRequestable include Referable + include SelectForProjectAuthorization has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source alias_method :members, :group_members @@ -47,7 +48,13 @@ class Group < Namespace end def sort(method) - order_by(method) + if method == 'storage_size_desc' + # storage_size is a virtual column so we need to + # pass a string to avoid AR adding the table name + reorder('storage_size DESC, namespaces.id DESC') + else + order_by(method) + end end def reference_prefix @@ -61,9 +68,19 @@ class Group < Namespace def visible_to_user(user) where(id: user.authorized_groups.select(:id).reorder(nil)) end + + def select_for_project_authorization + if current_scope.joins_values.include?(:shared_projects) + joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id') + .where('project_namespace.share_with_group_lock = ?', false) + .select("members.user_id, projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level") + else + super + end + end end - def to_reference(_from_project = nil) + def to_reference(_from_project = nil, full: nil) "#{self.class.reference_prefix}#{name}" end @@ -72,7 +89,7 @@ class Group < Namespace end def human_name - name + full_name end def visibility_level_field @@ -144,15 +161,17 @@ class Group < Namespace end def has_owner?(user) - owners.include?(user) + members_with_parents.owners.where(user_id: user).any? end def has_master?(user) - members.masters.where(user_id: user).any? + members_with_parents.masters.where(user_id: user).any? end + # Check if user is a last owner of the group. + # Parent owners are ignored for nested groups. def last_owner?(user) - has_owner?(user) && owners.size == 1 + owners.include?(user) && owners.size == 1 end def avatar_type @@ -176,4 +195,16 @@ class Group < Namespace def system_hook_service SystemHooksService.new end + + def refresh_members_authorized_projects + UserProjectAccessChangedService.new(users_with_parents.pluck(:id)).execute + end + + def members_with_parents + GroupMember.where(requested_at: nil, source_id: parents.map(&:id).push(id)) + end + + def users_with_parents + User.where(id: members_with_parents.select(:user_id)) + end end diff --git a/app/models/group_label.rb b/app/models/group_label.rb index 68841ace2e6..92c83b54861 100644 --- a/app/models/group_label.rb +++ b/app/models/group_label.rb @@ -8,8 +8,4 @@ class GroupLabel < Label def subject_foreign_key 'group_id' end - - def to_reference(source_project = nil, target_project = nil, format: :id) - super(source_project, target_project, format: format) - end end diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb new file mode 100644 index 00000000000..7b6db2634b7 --- /dev/null +++ b/app/models/group_milestone.rb @@ -0,0 +1,19 @@ +class GroupMilestone < GlobalMilestone + attr_accessor :group + + def self.build_collection(group, projects, params) + super(projects, params).each do |milestone| + milestone.group = group + end + end + + def self.build(group, projects, title) + super(projects, title).tap do |milestone| + milestone.group = group if milestone + end + end + + def issues_finder_params + { group_id: group.id } + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index adbca510ef7..65638d9a299 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -39,6 +39,8 @@ class Issue < ActiveRecord::Base scope :created_after, -> (datetime) { where("created_at >= ?", datetime) } + scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) } + attr_spammable :title, spam_title: true attr_spammable :description, spam_description: true @@ -60,61 +62,6 @@ class Issue < ActiveRecord::Base attributes end - class << self - private - - # Returns the project that the current scope belongs to if any, nil otherwise. - # - # Examples: - # - my_project.issues.without_due_date.owner_project => my_project - # - Issue.all.owner_project => nil - def owner_project - # No owner if we're not being called from an association - return unless all.respond_to?(:proxy_association) - - owner = all.proxy_association.owner - - # Check if the association is or belongs to a project - if owner.is_a?(Project) - owner - else - begin - owner.association(:project).target - rescue ActiveRecord::AssociationNotFoundError - nil - end - end - end - end - - def self.visible_to_user(user) - return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank? - return all if user.admin? - - # Check if we are scoped to a specific project's issues - if owner_project - if owner_project.authorized_for_user?(user, Gitlab::Access::REPORTER) - # If the project is authorized for the user, they can see all issues in the project - return all - else - # else only non confidential and authored/assigned to them - return where('issues.confidential IS NULL OR issues.confidential IS FALSE - OR issues.author_id = :user_id OR issues.assignee_id = :user_id', - user_id: user.id) - end - end - - where(' - issues.confidential IS NULL - OR issues.confidential IS FALSE - OR (issues.confidential = TRUE - AND (issues.author_id = :user_id - OR issues.assignee_id = :user_id - OR issues.project_id IN(:project_ids)))', - user_id: user.id, - project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id)) - end - def self.reference_prefix '#' end @@ -150,14 +97,10 @@ class Issue < ActiveRecord::Base end end - def to_reference(from_project = nil) + def to_reference(from_project = nil, full: false) reference = "#{self.class.reference_prefix}#{iid}" - if cross_project_reference?(from_project) - reference = project.to_reference + reference - end - - reference + "#{project.to_reference(from_project, full: full)}#{reference}" end def referenced_merge_requests(current_user = nil) @@ -182,18 +125,6 @@ class Issue < ActiveRecord::Base branches_with_iid - branches_with_merge_request end - # Reset issue events cache - # - # Since we do cache @event we need to reset cache in special cases: - # * when an issue is updated - # Events cache stored like events/23-20130109142513. - # The cache key includes updated_at timestamp. - # Thus it will automatically generate a new fragment - # when the event is updated because the key changes. - def reset_events_cache - Event.reset_event_cache_for(self) - end - # To allow polymorphism with MergeRequest. def source_project project @@ -266,7 +197,7 @@ class Issue < ActiveRecord::Base def as_json(options = {}) super(options).tap do |json| - json[:subscribed] = subscribed?(options[:user]) if options.has_key?(:user) + json[:subscribed] = subscribed?(options[:user], project) if options.has_key?(:user) && options[:user] if options.has_key?(:labels) json[:labels] = labels.as_json( diff --git a/app/models/key.rb b/app/models/key.rb index 568a60b8af3..9c74ca84753 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -4,14 +4,24 @@ class Key < ActiveRecord::Base include AfterCommitQueue include Sortable - belongs_to :user + LAST_USED_AT_REFRESH_TIME = 1.day.to_i - before_validation :strip_white_space, :generate_fingerprint + belongs_to :user - validates :title, presence: true, length: { within: 0..255 } - validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ } - validates :key, format: { without: /\n|\r/, message: 'should be a single line' } - validates :fingerprint, uniqueness: true, presence: { message: 'cannot be generated' } + before_validation :generate_fingerprint + + validates :title, + presence: true, + length: { maximum: 255 } + validates :key, + presence: true, + length: { maximum: 5000 }, + format: { with: /\A(ssh|ecdsa)-.*\Z/ } + validates :key, + format: { without: /\n|\r/, message: 'should be a single line' } + validates :fingerprint, + uniqueness: true, + presence: { message: 'cannot be generated' } delegate :name, :email, to: :user, prefix: true @@ -21,8 +31,9 @@ class Key < ActiveRecord::Base after_destroy :remove_from_shell after_destroy :post_destroy_hook - def strip_white_space - self.key = key.strip unless key.blank? + def key=(value) + value.strip! unless value.blank? + write_attribute(:key, value) end def publishable_key @@ -40,6 +51,13 @@ class Key < ActiveRecord::Base "key-#{id}" end + def update_last_used_at + lease = Gitlab::ExclusiveLease.new("key_update_last_used_at:#{id}", timeout: LAST_USED_AT_REFRESH_TIME) + return unless lease.try_obtain + + UseKeyWorker.perform_async(id) + end + def add_to_shell GitlabShellWorker.perform_async( :add_key, @@ -48,10 +66,6 @@ class Key < ActiveRecord::Base ) end - def notify_user - run_after_commit { NotificationService.new.new_key(self) } - end - def post_create_hook SystemHooksService.new.execute_hooks_for(self, :create) end @@ -77,4 +91,8 @@ class Key < ActiveRecord::Base self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint end + + def notify_user + run_after_commit { NotificationService.new.new_key(self) } + end end diff --git a/app/models/label.rb b/app/models/label.rb index d9287f2dc29..5b6b9a7a736 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -26,6 +26,7 @@ class Label < ActiveRecord::Base # Don't allow ',' for label titles validates :title, presence: true, format: { with: /\A[^,]+\z/ } validates :title, uniqueness: { scope: [:group_id, :project_id] } + validates :title, length: { maximum: 255 } default_scope { order(title: :asc) } @@ -144,18 +145,19 @@ class Label < ActiveRecord::Base # # Examples: # - # Label.first.to_reference # => "~1" - # Label.first.to_reference(format: :name) # => "~\"bug\"" - # Label.first.to_reference(project1, project2) # => "gitlab-org/gitlab-ce~1" + # Label.first.to_reference # => "~1" + # Label.first.to_reference(format: :name) # => "~\"bug\"" + # Label.first.to_reference(project, target_project: same_namespace_project) # => "gitlab-ce~1" + # Label.first.to_reference(project, target_project: another_namespace_project) # => "gitlab-org/gitlab-ce~1" # # Returns a String # - def to_reference(source_project = nil, target_project = nil, format: :id) + def to_reference(from_project = nil, target_project: nil, format: :id, full: false) format_reference = label_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" - if cross_project_reference?(source_project, target_project) - source_project.to_reference + reference + if from_project + "#{from_project.to_reference(target_project, full: full)}#{reference}" else reference end @@ -169,10 +171,6 @@ class Label < ActiveRecord::Base private - def cross_project_reference?(source_project, target_project) - source_project && target_project && source_project != target_project - end - def issues_count(user, params = {}) params.merge!(subject_foreign_key => subject.id, label_name: title, scope: 'all') IssuesFinder.new(user, params.with_indifferent_access).execute.count diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb index 0fd5f089db9..007eed5600a 100644 --- a/app/models/lfs_objects_project.rb +++ b/app/models/lfs_objects_project.rb @@ -5,4 +5,13 @@ class LfsObjectsProject < ActiveRecord::Base validates :lfs_object_id, presence: true validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" } validates :project_id, presence: true + + after_create :update_project_statistics + after_destroy :update_project_statistics + + private + + def update_project_statistics + ProjectCacheWorker.perform_async(project_id, [], [:lfs_objects_size]) + end end diff --git a/app/models/member.rb b/app/models/member.rb index b89ba8ecbb8..c585e0b450e 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -57,12 +57,18 @@ class Member < ActiveRecord::Base scope :owners, -> { active.where(access_level: OWNER) } scope :owners_and_masters, -> { active.where(access_level: [OWNER, MASTER]) } + scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) } + scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) } + scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) } + scope :order_oldest_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'ASC')) } + before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } after_create :send_invite, if: :invite?, unless: :importing? after_create :send_request, if: :request?, unless: :importing? after_create :create_notification_setting, unless: [:pending?, :importing?] after_create :post_create_hook, unless: [:pending?, :importing?] + after_create :refresh_member_authorized_projects, if: :importing? after_update :post_update_hook, unless: [:pending?, :importing?] after_destroy :post_destroy_hook, unless: :pending? @@ -71,6 +77,34 @@ class Member < ActiveRecord::Base default_value_for :notification_level, NotificationSetting.levels[:global] class << self + def search(query) + joins(:user).merge(User.search(query)) + end + + def sort(method) + case method.to_s + when 'access_level_asc' then reorder(access_level: :asc) + when 'access_level_desc' then reorder(access_level: :desc) + when 'recent_sign_in' then order_recent_sign_in + when 'oldest_sign_in' then order_oldest_sign_in + when 'last_joined' then order_created_desc + when 'oldest_joined' then order_created_asc + else + order_by(method) + end + end + + def left_join_users + users = User.arel_table + members = Member.arel_table + + member_users = members.join(users, Arel::Nodes::OuterJoin). + on(members[:user_id].eq(users[:id])). + join_sources + + joins(member_users) + end + def access_for_user_ids(user_ids) where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h end @@ -88,8 +122,8 @@ class Member < ActiveRecord::Base member = if user.is_a?(User) source.members.find_by(user_id: user.id) || - source.requesters.find_by(user_id: user.id) || - source.members.build(user_id: user.id) + source.requesters.find_by(user_id: user.id) || + source.members.build(user_id: user.id) else source.members.build(invite_email: user) end @@ -113,6 +147,8 @@ class Member < ActiveRecord::Base member.save end + UserProjectAccessChangedService.new(user.id).execute if user.is_a?(User) + member end @@ -239,17 +275,28 @@ class Member < ActiveRecord::Base end def post_create_hook + UserProjectAccessChangedService.new(user.id).execute system_hook_service.execute_hooks_for(self, :create) end def post_update_hook - # override in subclass + UserProjectAccessChangedService.new(user.id).execute if access_level_changed? end def post_destroy_hook + refresh_member_authorized_projects system_hook_service.execute_hooks_for(self, :destroy) end + def refresh_member_authorized_projects + # If user/source is being destroyed, project access are gonna be destroyed eventually + # because of DB foreign keys, so we shouldn't bother with refreshing after each + # member is destroyed through association + return if destroyed_by_association.present? + + UserProjectAccessChangedService.new(user_id).execute + end + def after_accept_invite post_create_hook end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index d76feb9680e..cd5b345bae5 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -22,7 +22,8 @@ class MergeRequest < ActiveRecord::Base after_create :ensure_merge_request_diff, unless: :importing? after_update :reload_diff_if_branch_changed - delegate :commits, :real_size, to: :merge_request_diff, prefix: nil + delegate :commits, :real_size, :commits_sha, :commits_count, + to: :merge_request_diff, prefix: nil # When this attribute is true some MR validation is ignored # It allows us to close or modify broken merge requests @@ -90,17 +91,23 @@ 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?] validates :source_branch, presence: true validates :target_project, presence: true validates :target_branch, presence: true - validates :merge_user, presence: true, if: :merge_when_build_succeeds? + validates :merge_user, presence: true, if: :merge_when_build_succeeds?, unless: :importing? validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?] validate :validate_fork, unless: :closed_without_fork? - scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } + scope :by_source_or_target_branch, ->(branch_name) do + where("source_branch = :branch OR target_branch = :branch", branch: branch_name) + end scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :of_projects, ->(ids) { where(target_project_id: ids) } @@ -172,14 +179,10 @@ class MergeRequest < ActiveRecord::Base work_in_progress?(title) ? title : "WIP: #{title}" end - def to_reference(from_project = nil) + def to_reference(from_project = nil, full: false) reference = "#{self.class.reference_prefix}#{iid}" - if cross_project_reference?(from_project) - reference = project.to_reference + reference - end - - reference + "#{project.to_reference(from_project, full: full)}#{reference}" end def first_commit @@ -199,7 +202,9 @@ class MergeRequest < ActiveRecord::Base end def diff_size - diffs(diff_options).size + opts = diff_options || {} + + raw_diffs(opts).size end def diff_base_commit @@ -220,7 +225,7 @@ class MergeRequest < ActiveRecord::Base # true base commit, so we can't simply have `#diff_base_commit` fall back on # this method. def likely_diff_base_commit - first_commit.parent || first_commit + first_commit.try(:parent) || first_commit end def diff_start_commit @@ -453,7 +458,7 @@ class MergeRequest < ActiveRecord::Base should_remove_source_branch? || force_remove_source_branch? end - def mr_and_commit_notes + def related_notes # Fetch comments only from last 100 commits commits_for_notes_limit = 100 commit_ids = commits.last(commits_for_notes_limit).map(&:id) @@ -469,7 +474,7 @@ class MergeRequest < ActiveRecord::Base end def discussions - @discussions ||= self.mr_and_commit_notes. + @discussions ||= self.related_notes. inc_relations_for_view. fresh. discussions @@ -479,6 +484,14 @@ class MergeRequest < ActiveRecord::Base @diff_discussions ||= self.notes.diff_notes.discussions end + def resolvable_discussions + @resolvable_discussions ||= diff_discussions.select(&:to_be_resolved?) + end + + def discussions_can_be_resolved_by?(user) + resolvable_discussions.all? { |discussion| discussion.can_resolve?(user) } + end + def find_diff_discussion(discussion_id) notes = self.notes.diff_notes.where(discussion_id: discussion_id).fresh.to_a return if notes.empty? @@ -494,10 +507,14 @@ class MergeRequest < ActiveRecord::Base discussions_resolvable? && diff_discussions.none?(&:to_be_resolved?) end + def discussions_to_be_resolved? + discussions_resolvable? && !discussions_resolved? + end + def mergeable_discussions_state? return true unless project.only_allow_merge_if_all_discussions_are_resolved? - discussions_resolved? + !discussions_to_be_resolved? end def hook_attrs @@ -557,6 +574,15 @@ class MergeRequest < ActiveRecord::Base end end + def issues_mentioned_but_not_closing(current_user = self.author) + return [] unless target_branch == project.default_branch + + ext = Gitlab::ReferenceExtractor.new(project, current_user) + ext.analyze(description) + + ext.issues - closes_issues + end + def target_project_path if target_project target_project.path_with_namespace @@ -601,25 +627,24 @@ class MergeRequest < ActiveRecord::Base self.target_project.repository.branch_names.include?(self.target_branch) end - # Reset merge request events cache - # - # Since we do cache @event we need to reset cache in special cases: - # * when a merge request is updated - # Events cache stored like events/23-20130109142513. - # The cache key includes updated_at timestamp. - # Thus it will automatically generate a new fragment - # when the event is updated because the key changes. - def reset_events_cache - Event.reset_event_cache_for(self) - end - - def merge_commit_message - message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n" - message << "#{title}\n\n" - message << "#{description}\n\n" if description.present? + def merge_commit_message(include_description: false) + closes_issues_references = closes_issues.map do |issue| + issue.to_reference(target_project) + end + + message = [ + "Merge branch '#{source_branch}' into '#{target_branch}'", + title + ] + + if !include_description && closes_issues_references.present? + message << "Closes #{closes_issues_references.to_sentence}" + end + + message << "#{description}" if include_description && description.present? message << "See merge request #{to_reference}" - message + message.join("\n\n") end def reset_merge_when_build_succeeds @@ -670,7 +695,7 @@ class MergeRequest < ActiveRecord::Base end def broken? - self.commits.blank? || branch_missing? || cannot_be_merged? + has_no_commits? || branch_missing? || cannot_be_merged? end def can_be_merged_by?(user) @@ -686,18 +711,21 @@ class MergeRequest < ActiveRecord::Base def mergeable_ci_state? return true unless project.only_allow_merge_if_build_succeeds? - !pipeline || pipeline.success? + !head_pipeline || head_pipeline.success? || head_pipeline.skipped? end def environments return [] unless diff_head_commit - @environments ||= - begin - envs = target_project.environments_for(target_branch, diff_head_commit, with_tags: true) - envs.concat(source_project.environments_for(source_branch, diff_head_commit)) if source_project - envs.uniq - end + @environments ||= begin + target_envs = target_project.environments_for( + target_branch, commit: diff_head_commit, with_tags: true) + + source_envs = source_project.environments_for( + source_branch, commit: diff_head_commit) if source_project + + (target_envs.to_a + source_envs.to_a).uniq + end end def state_human_name @@ -775,18 +803,14 @@ class MergeRequest < ActiveRecord::Base diverged_commits_count > 0 end - def commits_sha - commits.map(&:sha) - end - - def pipeline + def head_pipeline return unless diff_head_sha && source_project - @pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha) + @head_pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha) end def all_pipelines - return unless source_project + return Ci::Pipeline.none unless source_project @all_pipelines ||= source_project.pipelines .where(sha: all_commits_sha, ref: source_branch) @@ -809,7 +833,7 @@ class MergeRequest < ActiveRecord::Base @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha end - def can_be_reverted?(current_user = nil) + def can_be_reverted?(current_user) merge_commit && !merge_commit.has_been_reverted?(current_user, self) end @@ -876,4 +900,24 @@ class MergeRequest < ActiveRecord::Base @conflicts_can_be_resolved_in_ui = false end end + + def has_commits? + 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/metrics.rb b/app/models/merge_request/metrics.rb index 99c49a020c9..cdc408738be 100644 --- a/app/models/merge_request/metrics.rb +++ b/app/models/merge_request/metrics.rb @@ -1,5 +1,6 @@ class MergeRequest::Metrics < ActiveRecord::Base belongs_to :merge_request + belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :pipeline_id def record! if merge_request.merged? && self.merged_at.blank? diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index dd65a9a8b86..dadb81f9b6e 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -1,7 +1,7 @@ class MergeRequestDiff < ActiveRecord::Base include Sortable include Importable - include EncodingHelper + include Gitlab::Git::EncodingHelper # Prevent store of diff if commits amount more then 500 COMMITS_SAFE_SIZE = 100 @@ -11,6 +11,9 @@ class MergeRequestDiff < ActiveRecord::Base belongs_to :merge_request + serialize :st_commits + serialize :st_diffs + state_machine :state, initial: :empty do state :collected state :overflow @@ -22,8 +25,7 @@ class MergeRequestDiff < ActiveRecord::Base state :overflow_diff_lines_limit end - serialize :st_commits - serialize :st_diffs + scope :viewable, -> { without_state(:empty) } # All diff information is collected from repository after object is created. # It allows you to override variables like head_commit_sha before getting diff. @@ -125,11 +127,7 @@ class MergeRequestDiff < ActiveRecord::Base end def commits_sha - if @commits - commits.map(&:sha) - else - st_commits.map { |commit| commit[:id] } - end + st_commits.map { |commit| commit[:id] } end def diff_refs @@ -174,6 +172,10 @@ class MergeRequestDiff < ActiveRecord::Base CompareService.new.execute(project, head_commit_sha, project, sha, straight: straight) end + def commits_count + st_commits.count + end + private # Old GitLab implementations may have generated diffs as ["--broken-diff"]. @@ -232,28 +234,28 @@ class MergeRequestDiff < ActiveRecord::Base # and save it as array of hashes in st_diffs db field def save_diffs new_attributes = {} - new_diffs = [] if commits.size.zero? new_attributes[:state] = :empty else diff_collection = compare.diffs(Commit.max_diff_options) - - if diff_collection.overflow? - # Set our state to 'overflow' to make the #empty? and #collected? - # methods (generated by StateMachine) return false. - new_attributes[:state] = :overflow - end - - new_attributes[:real_size] = diff_collection.real_size + new_attributes[:real_size] = compare.diffs.real_size if diff_collection.any? new_diffs = dump_diffs(diff_collection) new_attributes[:state] = :collected end + + new_attributes[:st_diffs] = new_diffs || [] + + # Set our state to 'overflow' to make the #empty? and #collected? + # methods (generated by StateMachine) return false. + # + # This attribution has to come at the end of the method so 'overflow' + # state does not get overridden by 'collected'. + new_attributes[:state] = :overflow if diff_collection.overflow? end - new_attributes[:st_diffs] = new_diffs update_columns_serialized(new_attributes) end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 23aecbfa3a6..7331000a9f2 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -29,6 +29,7 @@ class Milestone < ActiveRecord::Base validates :title, presence: true, uniqueness: { scope: :project_id } validates :project, presence: true + validate :start_date_should_be_less_than_due_date, if: Proc.new { |m| m.start_date.present? && m.due_date.present? } strip_attributes :title @@ -112,41 +113,24 @@ class Milestone < ActiveRecord::Base # # Examples: # - # Milestone.first.to_reference # => "%1" - # Milestone.first.to_reference(format: :name) # => "%\"goal\"" - # Milestone.first.to_reference(project) # => "gitlab-org/gitlab-ce%1" + # Milestone.first.to_reference # => "%1" + # Milestone.first.to_reference(format: :name) # => "%\"goal\"" + # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1" + # Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1" # - def to_reference(from_project = nil, format: :iid) + def to_reference(from_project = nil, format: :iid, full: false) format_reference = milestone_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" - if cross_project_reference?(from_project) - project.to_reference + reference - else - reference - end + "#{project.to_reference(from_project, full: full)}#{reference}" end def reference_link_text(from_project = nil) self.title end - def expired? - if due_date - due_date.past? - else - false - end - end - - def expires_at - if due_date - if due_date.past? - "expired on #{due_date.to_s(:medium)}" - else - "expires on #{due_date.to_s(:medium)}" - end - end + def milestoneish_ids + id end def can_be_closed? @@ -212,4 +196,14 @@ class Milestone < ActiveRecord::Base def sanitize_title(value) CGI.unescape_html(Sanitize.clean(value.to_s)) end + + def start_date_should_be_less_than_due_date + if due_date <= start_date + errors.add(:start_date, "Can't be greater than due date") + end + end + + def issues_finder_params + { project_id: project_id } + end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index b67049f0f55..dd33975731f 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -4,29 +4,34 @@ class Namespace < ActiveRecord::Base include CacheMarkdownField include Sortable include Gitlab::ShellAdapter + include Routable cache_markdown_field :description, pipeline: :description has_many :projects, dependent: :destroy + has_many :project_statistics belongs_to :owner, class_name: "User" + belongs_to :parent, class_name: "Namespace" + has_many :children, class_name: "Namespace", foreign_key: :parent_id + validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :name, - length: { within: 0..255 }, - namespace_name: true, presence: true, - uniqueness: true + uniqueness: { scope: :parent_id }, + length: { maximum: 255 }, + namespace_name: true - validates :description, length: { within: 0..255 } + validates :description, length: { maximum: 255 } validates :path, - length: { within: 1..255 }, - namespace: true, presence: true, - uniqueness: { case_sensitive: false } + length: { maximum: 255 }, + namespace: true delegate :name, to: :owner, allow_nil: true, prefix: true after_update :move_dir, if: :path_changed? + after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') } # Save the storage paths before the projects are destroyed to use them on after destroy before_destroy(prepend: true) { @old_repository_storage_paths = repository_storage_paths } @@ -34,6 +39,18 @@ class Namespace < ActiveRecord::Base scope :root, -> { where('type IS NULL') } + scope :with_statistics, -> do + joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id') + .group('namespaces.id') + .select( + 'namespaces.*', + 'COALESCE(SUM(ps.storage_size), 0) AS storage_size', + 'COALESCE(SUM(ps.repository_size), 0) AS repository_size', + 'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size', + 'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size', + ) + end + class << self def by_path(path) find_by('lower(path) = :value', value: path.downcase) @@ -85,7 +102,7 @@ class Namespace < ActiveRecord::Base end def to_param - path + full_path end def human_name @@ -94,7 +111,7 @@ class Namespace < ActiveRecord::Base def move_dir if any_project_has_container_registry_tags? - raise Exception.new('Namespace cannot be moved, because at least one project has tags in container registry') + raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry') end # Move the namespace directory in all storages paths used by member projects @@ -103,14 +120,18 @@ class Namespace < ActiveRecord::Base gitlab_shell.add_namespace(repository_storage_path, path_was) unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path) + Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}" + # if we cannot move namespace directory we should rollback # db changes in order to prevent out of sync between db and fs - raise Exception.new('namespace directory cannot be moved') + raise Gitlab::UpdatePathError.new('namespace directory cannot be moved') end end Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) + remove_exports! + # If repositories moved successfully we need to # send update instructions to users. # However we cannot allow rollback since we moved namespace dir @@ -147,6 +168,27 @@ class Namespace < ActiveRecord::Base Gitlab.config.lfs.enabled end + def full_path + if parent + parent.full_path + '/' + path + else + path + end + end + + def full_name + @full_name ||= + if parent + parent.full_name + ' / ' + name + else + name + end + end + + def parents + @parents ||= parent ? parent.parents + [parent] : [] + end + private def repository_storage_paths @@ -174,5 +216,34 @@ class Namespace < ActiveRecord::Base GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path) end end + + remove_exports! + end + + def refresh_access_of_projects_invited_groups + Group. + joins(project_group_links: :project). + where(projects: { namespace_id: id }). + find_each(&:refresh_members_authorized_projects) + end + + def full_path_changed? + path_changed? || parent_id_changed? + end + + def remove_exports! + Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete)) + end + + def export_path + File.join(Gitlab::ImportExport.storage_path, full_path_was) + end + + def full_path_was + if parent + parent.full_path + '/' + path_was + else + path_was + end end end diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index 345041a6ad1..b524ca50ee8 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -161,8 +161,8 @@ module Network def is_overlap?(range, overlap_space) range.each do |i| if i != range.first && - i != range.last && - @commits[i].spaces.include?(overlap_space) + i != range.last && + @commits[i].spaces.include?(overlap_space) return true end diff --git a/app/models/note.rb b/app/models/note.rb index 2d644b03e4d..0c1b05dabf2 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -7,6 +7,7 @@ class Note < ActiveRecord::Base include Importable include FasterCacheKeys include CacheMarkdownField + include AfterCommitQueue cache_markdown_field :note, pipeline: :note @@ -18,6 +19,9 @@ class Note < ActiveRecord::Base # Banzai::ObjectRenderer attr_accessor :user_visible_reference_count + # Attribute used to store the attributes that have ben changed by slash commands. + attr_accessor :commands_changes + default_value_for :system, false attr_mentionable :note, pipeline: :note @@ -95,7 +99,7 @@ class Note < ActiveRecord::Base end def discussions - Discussion.for_notes(all) + Discussion.for_notes(fresh) end def grouped_diff_discussions @@ -103,23 +107,6 @@ class Note < ActiveRecord::Base Discussion.for_diff_notes(active_notes). map { |d| [d.line_code, d] }.to_h end - - # Searches for notes matching the given query. - # - # This method uses ILIKE on PostgreSQL and LIKE on MySQL. - # - # query - The search query as a String. - # as_user - Limit results to those viewable by a specific user - # - # Returns an ActiveRecord::Relation. - def search(query, as_user: nil) - table = arel_table - pattern = "%#{query}%" - - Note.joins('LEFT JOIN issues ON issues.id = noteable_id'). - where(table[:note].matches(pattern)). - merge(Issue.visible_to_user(as_user)) - end end def cross_reference? @@ -197,19 +184,6 @@ class Note < ActiveRecord::Base super(noteable_type.to_s.classify.constantize.base_class.to_s) end - # Reset notes events cache - # - # Since we do cache @event we need to reset cache in special cases: - # * when a note is updated - # * when a note is removed - # Events cache stored like events/23-20130109142513. - # The cache key includes updated_at timestamp. - # Thus it will automatically generate a new fragment - # when the event is updated because the key changes. - def reset_events_cache - Event.reset_event_cache_for(self) - end - def editable? !system? end diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 43fc218de2b..58f6214bea7 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -37,6 +37,10 @@ class NotificationSetting < ActiveRecord::Base :success_pipeline ] + EXCLUDED_WATCHER_EVENTS = [ + :success_pipeline + ] + store :events, accessors: EMAIL_EVENTS, coder: JSON before_create :set_events diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index c4b095e0c04..10a34c42fd8 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -2,6 +2,8 @@ class PersonalAccessToken < ActiveRecord::Base include TokenAuthenticatable add_authentication_token_field :token + serialize :scopes, Array + belongs_to :user scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") } diff --git a/app/models/project.rb b/app/models/project.rb index bbe590b5a8a..cd35601d76b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -12,7 +12,10 @@ class Project < ActiveRecord::Base include AfterCommitQueue include CaseSensitivity include TokenAuthenticatable + include ValidAttribute include ProjectFeaturesCompatibility + include SelectForProjectAuthorization + include Routable extend Gitlab::ConfigHelper @@ -23,7 +26,9 @@ class Project < ActiveRecord::Base cache_markdown_field :description, pipeline: :description - delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true + delegate :feature_available?, :builds_enabled?, :wiki_enabled?, + :merge_requests_enabled?, :issues_enabled?, to: :project_feature, + allow_nil: true default_value_for :archived, false default_value_for :visibility_level, gitlab_config_features.visibility_level @@ -35,10 +40,12 @@ class Project < ActiveRecord::Base default_value_for :builds_enabled, gitlab_config_features.builds default_value_for :wiki_enabled, gitlab_config_features.wiki default_value_for :snippets_enabled, gitlab_config_features.snippets + default_value_for :only_allow_merge_if_all_discussions_are_resolved, false after_create :ensure_dir_exist after_create :create_project_feature, unless: :project_feature after_save :ensure_dir_exist, if: :namespace_id_changed? + after_save :update_project_statistics, if: :namespace_id_changed? # set last_activity_at to the same as created_at after_create :set_last_activity_at @@ -59,6 +66,8 @@ class Project < ActiveRecord::Base end end + after_validation :check_pending_delete + ActsAsTaggableOn.strict_case_match = true acts_as_taggable_on :tags @@ -76,7 +85,6 @@ class Project < ActiveRecord::Base has_many :boards, before_add: :validate_board_limit, dependent: :destroy # Project services - has_many :services has_one :campfire_service, dependent: :destroy has_one :drone_ci_service, dependent: :destroy has_one :emails_on_push_service, dependent: :destroy @@ -89,6 +97,9 @@ class Project < ActiveRecord::Base has_one :assembla_service, dependent: :destroy has_one :asana_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy + has_one :mattermost_slash_commands_service, dependent: :destroy + has_one :mattermost_service, dependent: :destroy + has_one :slack_slash_commands_service, dependent: :destroy has_one :slack_service, dependent: :destroy has_one :buildkite_service, dependent: :destroy has_one :bamboo_service, dependent: :destroy @@ -100,6 +111,7 @@ class Project < ActiveRecord::Base has_one :bugzilla_service, dependent: :destroy has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project has_one :external_wiki_service, dependent: :destroy + has_one :kubernetes_service, dependent: :destroy, inverse_of: :project has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_from_project, through: :forked_project_link @@ -109,8 +121,6 @@ class Project < ActiveRecord::Base # Merge Requests for target project should be removed with it has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id' - # Merge requests from source project should be kept when source project was removed - has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest has_many :issues, dependent: :destroy has_many :labels, dependent: :destroy, class_name: 'ProjectLabel' has_many :services, dependent: :destroy @@ -121,6 +131,8 @@ class Project < ActiveRecord::Base has_many :hooks, dependent: :destroy, class_name: 'ProjectHook' has_many :protected_branches, dependent: :destroy + has_many :project_authorizations + has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User' has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source alias_method :members, :project_members has_many :users, through: :project_members @@ -141,6 +153,7 @@ class Project < ActiveRecord::Base has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :project_feature, dependent: :destroy + has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id @@ -158,18 +171,20 @@ class Project < ActiveRecord::Base delegate :name, to: :owner, allow_nil: true, prefix: true delegate :members, to: :team, prefix: true delegate :add_user, to: :team + delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team # Validations validates :creator, presence: true, on: :create validates :description, length: { maximum: 2000 }, allow_blank: true validates :name, presence: true, - length: { within: 0..255 }, + length: { maximum: 255 }, format: { with: Gitlab::Regex.project_name_regex, message: Gitlab::Regex.project_name_regex_message } validates :path, presence: true, - length: { within: 0..255 }, + project_path: true, + length: { maximum: 255 }, format: { with: Gitlab::Regex.project_path_regex, message: Gitlab::Regex.project_path_regex_message } validates :namespace, presence: true @@ -208,6 +223,7 @@ class Project < ActiveRecord::Base scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) } scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') } + scope :with_statistics, -> { includes(:statistics) } # "enabled" here means "not disabled". It includes private features! scope :with_feature_enabled, ->(feature) { @@ -315,94 +331,15 @@ class Project < ActiveRecord::Base non_archived.where(table[:name].matches(pattern)) end - # Finds a single project for the given path. - # - # path - The full project path (including namespace path). - # - # Returns a Project, or nil if no project could be found. - def find_with_namespace(path) - namespace_path, project_path = path.split('/', 2) - - return unless namespace_path && project_path - - namespace_path = connection.quote(namespace_path) - project_path = connection.quote(project_path) - - # On MySQL we want to ensure the ORDER BY uses a case-sensitive match so - # any literal matches come first, for this we have to use "BINARY". - # Without this there's still no guarantee in what order MySQL will return - # rows. - binary = Gitlab::Database.mysql? ? 'BINARY' : '' - - order_sql = "(CASE WHEN #{binary} namespaces.path = #{namespace_path} " \ - "AND #{binary} projects.path = #{project_path} THEN 0 ELSE 1 END)" - - where_paths_in([path]).reorder(order_sql).take - end - - # Builds a relation to find multiple projects by their full paths. - # - # Each path must be in the following format: - # - # namespace_path/project_path - # - # For example: - # - # gitlab-org/gitlab-ce - # - # Usage: - # - # Project.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee}) - # - # This would return the projects with the full paths matching the values - # given. - # - # paths - An Array of full paths (namespace path + project path) for which - # to find the projects. - # - # Returns an ActiveRecord::Relation. - def where_paths_in(paths) - wheres = [] - cast_lower = Gitlab::Database.postgresql? - - paths.each do |path| - namespace_path, project_path = path.split('/', 2) - - next unless namespace_path && project_path - - namespace_path = connection.quote(namespace_path) - project_path = connection.quote(project_path) - - where = "(namespaces.path = #{namespace_path} - AND projects.path = #{project_path})" - - if cast_lower - where = "( - #{where} - OR ( - LOWER(namespaces.path) = LOWER(#{namespace_path}) - AND LOWER(projects.path) = LOWER(#{project_path}) - ) - )" - end - - wheres << where - end - - if wheres.empty? - none - else - joins(:namespace).where(wheres.join(' OR ')) - end - end - def visibility_levels Gitlab::VisibilityLevel.options end def sort(method) - if method == 'repository_size_desc' - reorder(repository_size: :desc, id: :desc) + if method == 'storage_size_desc' + # storage_size is a joined column so we need to + # pass a string to avoid AR adding the table name + reorder('project_statistics.storage_size DESC, projects.id DESC') else order_by(method) end @@ -410,7 +347,11 @@ class Project < ActiveRecord::Base def reference_pattern name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR - %r{(?<project>#{name_pattern}/#{name_pattern})} + + %r{ + ((?<namespace>#{name_pattern})\/)? + (?<project>#{name_pattern}) + }x end def trending @@ -427,6 +368,10 @@ class Project < ActiveRecord::Base def group_ids joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id) end + + # Add alias for Routable method for compatibility with old code. + # In future all calls `find_with_namespace` should be replaced with `find_by_full_path` + alias_method :find_with_namespace, :find_by_full_path end def lfs_enabled? @@ -594,6 +539,10 @@ class Project < ActiveRecord::Base import_type == 'gitlab_project' end + def gitea_import? + import_type == 'gitea' + end + def check_limit unless creator.can_create_project? or namespace.kind == 'group' projects_limit = creator.projects_limit @@ -641,18 +590,26 @@ class Project < ActiveRecord::Base end end - def to_reference(_from_project = nil) - path_with_namespace + def to_reference(from_project = nil, full: false) + if full || cross_namespace_reference?(from_project) + path_with_namespace + elsif cross_project_reference?(from_project) + path + end + end + + def to_human_reference(from_project = nil) + if cross_namespace_reference?(from_project) + name_with_namespace + elsif cross_project_reference?(from_project) + name + end end def web_url Gitlab::Routing.url_helpers.namespace_project_url(self.namespace, self) end - def web_url_without_protocol - web_url.split('://')[1] - end - def new_issue_address(author) return unless Gitlab::IncomingEmail.supports_issue_creation? && author @@ -678,9 +635,9 @@ class Project < ActiveRecord::Base self.id end - def get_issue(issue_id) + def get_issue(issue_id, current_user) if default_issues_tracker? - issues.find_by(iid: issue_id) + IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id) else ExternalIssue.new(issue_id, self) end @@ -748,27 +705,32 @@ class Project < ActiveRecord::Base update_column(:has_external_wiki, services.external_wikis.any?) end - def build_missing_services + def find_or_initialize_services services_templates = Service.where(template: true) - Service.available_services_names.each do |service_name| + Service.available_services_names.map do |service_name| service = find_service(services, service_name) - # If service is available but missing in db - if service.nil? + if service + service + else # We should check if template for the service exists template = find_service(services_templates, service_name) if template.nil? - # If no template, we should create an instance. Ex `create_gitlab_ci_service` - public_send("create_#{service_name}_service") + # If no template, we should create an instance. Ex `build_gitlab_ci_service` + public_send("build_#{service_name}_service") else - Service.create_from_template(self.id, template) + Service.build_from_template(id, template) end end end end + def find_or_initialize_service(name) + find_or_initialize_services.find { |service| service.to_param == name } + end + def create_labels Label.templates.each do |label| params = label.attributes.except('id', 'template', 'created_at', 'updated_at') @@ -788,6 +750,14 @@ class Project < ActiveRecord::Base @ci_service ||= ci_services.reorder(nil).find_by(active: true) end + def deployment_services + services.where(category: :deployment) + end + + def deployment_service + @deployment_service ||= deployment_services.reorder(nil).find_by(active: true) + end + def jira_tracker? issues_tracker.to_param == 'jira' end @@ -849,13 +819,14 @@ class Project < ActiveRecord::Base end alias_method :human_name, :name_with_namespace - def path_with_namespace - if namespace - namespace.path + '/' + path + def full_path + if namespace && path + namespace.full_path + '/' + path else path end end + alias_method :path_with_namespace, :full_path def execute_hooks(data, hooks_scope = :push_hooks) hooks.send(hooks_scope).each do |hook| @@ -878,7 +849,7 @@ class Project < ActiveRecord::Base end def empty_repo? - !repository.exists? || !repository.has_visible_content? + repository.empty_repo? end def repo @@ -952,7 +923,7 @@ class Project < ActiveRecord::Base Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present" # we currently doesn't support renaming repository if it contains tags in container registry - raise Exception.new('Project cannot be renamed, because tags are present in its container registry') + raise StandardError.new('Project cannot be renamed, because tags are present in its container registry') end if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace) @@ -962,7 +933,6 @@ class Project < ActiveRecord::Base begin gitlab_shell.mv_repository(repository_storage_path, "#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki") send_move_instructions(old_path_with_namespace) - reset_events_cache @old_path_with_namespace = old_path_with_namespace @@ -980,7 +950,7 @@ class Project < ActiveRecord::Base # if we cannot move namespace directory we should rollback # db changes in order to prevent out of sync between db and fs - raise Exception.new('repository cannot be renamed') + raise StandardError.new('repository cannot be renamed') end Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}" @@ -1029,22 +999,6 @@ class Project < ActiveRecord::Base attrs end - # Reset events cache related to this project - # - # Since we do cache @event we need to reset cache in special cases: - # * when project was moved - # * when project was renamed - # * when the project avatar changes - # Events cache stored like events/23-20130109142513. - # The cache key includes updated_at timestamp. - # Thus it will automatically generate a new fragment - # when the event is updated because the key changes. - def reset_events_cache - Event.where(project_id: self.id). - order('id DESC').limit(100). - update_all(updated_at: Time.now) - end - def project_member(user) project_members.find_by(user_id: user) end @@ -1076,7 +1030,7 @@ class Project < ActiveRecord::Base "refs/heads/#{branch}", force: true) repository.copy_gitattributes(branch) - repository.expire_avatar_cache(branch) + repository.after_change_head reload_default_branch end @@ -1084,14 +1038,6 @@ class Project < ActiveRecord::Base forked? && project == forked_from_project end - def update_repository_size - update_attribute(:repository_size, repository.size) - end - - def update_commit_count - update_attribute(:commit_count, repository.commit_count) - end - def forks_count forks.count end @@ -1282,18 +1228,10 @@ class Project < ActiveRecord::Base end end - # Checks if `user` is authorized for this project, with at least the - # `min_access_level` (if given). - # - # If you change the logic of this method, please also update `User#authorized_projects` - def authorized_for_user?(user, min_access_level = nil) - return false unless user + def deployment_variables + return [] unless deployment_service - return true if personal? && namespace_id == user.namespace_id - - authorized_for_user_by_group?(user, min_access_level) || - authorized_for_user_by_members?(user, min_access_level) || - authorized_for_user_by_shared_projects?(user, min_access_level) + deployment_service.predefined_variables end def append_or_update_attribute(name, value) @@ -1318,65 +1256,86 @@ class Project < ActiveRecord::Base Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) } end - def environments_for(ref, commit, with_tags: false) - environment_ids = deployments.group(:environment_id). - select(:environment_id) + def environments_for(ref, commit: nil, with_tags: false) + deployments_query = with_tags ? 'ref = ? OR tag IS TRUE' : 'ref = ?' - environment_ids = - if with_tags - environment_ids.where('ref=? OR tag IS TRUE', ref) - else - environment_ids.where(ref: ref) - end + environment_ids = deployments + .where(deployments_query, ref.to_s) + .group(:environment_id) + .select(:environment_id) + + environments_found = environments.available + .where(id: environment_ids).to_a + + return environments_found unless commit - environments.available.where(id: environment_ids).select do |environment| + environments_found.select do |environment| environment.includes_commit?(commit) end end + def environments_recently_updated_on_branch(branch) + environments_for(branch).select do |environment| + environment.recently_updated_on_branch?(branch) + end + end + private + # Check if a reference is being done cross-project + # + # from_project - Refering Project object + def cross_project_reference?(from_project) + from_project && self != from_project + end + def pushes_since_gc_redis_key "projects/#{id}/pushes_since_gc" end + def cross_namespace_reference?(from_project) + from_project && namespace != from_project.namespace + end + def default_branch_protected? current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE end - def authorized_for_user_by_group?(user, min_access_level) - member = user.group_members.find_by(source_id: group) - - member && (!min_access_level || member.access_level >= min_access_level) + # Similar to the normal callbacks that hook into the life cycle of an + # Active Record object, you can also define callbacks that get triggered + # when you add an object to an association collection. If any of these + # callbacks throw an exception, the object will not be added to the + # collection. Before you add a new board to the boards collection if you + # already have 1, 2, or n it will fail, but it if you have 0 that is lower + # than the number of permitted boards per project it won't fail. + def validate_board_limit(board) + raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS end - def authorized_for_user_by_members?(user, min_access_level) - member = members.find_by(user_id: user) + def full_path_changed? + path_changed? || namespace_id_changed? + end - member && (!min_access_level || member.access_level >= min_access_level) + def update_project_statistics + stats = statistics || build_statistics + stats.update(namespace_id: namespace_id) end - def authorized_for_user_by_shared_projects?(user, min_access_level) - shared_projects = user.group_members.joins(group: :shared_projects). - where(project_group_links: { project_id: self }) + def check_pending_delete + return if valid_attribute?(:name) && valid_attribute?(:path) + return unless pending_delete_twin - if min_access_level - members_scope = { access_level: Gitlab::Access.values.select { |access| access >= min_access_level } } - shared_projects = shared_projects.where(members: members_scope) + %i[route route.path name path].each do |error| + errors.delete(error) end - shared_projects.any? + errors.add(:base, "The project is still being deleted. Please try again later.") end - # Similar to the normal callbacks that hook into the life cycle of an - # Active Record object, you can also define callbacks that get triggered - # when you add an object to an association collection. If any of these - # callbacks throw an exception, the object will not be added to the - # collection. Before you add a new board to the boards collection if you - # already have 1, 2, or n it will fail, but it if you have 0 that is lower - # than the number of permitted boards per project it won't fail. - def validate_board_limit(board) - raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS + def pending_delete_twin + return false unless path + + Project.unscoped.where(pending_delete: true).find_with_namespace(path_with_namespace) end end diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb new file mode 100644 index 00000000000..4c7f4f5a429 --- /dev/null +++ b/app/models/project_authorization.rb @@ -0,0 +1,21 @@ +class ProjectAuthorization < ActiveRecord::Base + belongs_to :user + belongs_to :project + + validates :project, presence: true + validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true + validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true + + def self.insert_authorizations(rows, per_batch = 1000) + rows.each_slice(per_batch) do |slice| + tuples = slice.map do |tuple| + tuple.map { |value| connection.quote(value) } + end + + connection.execute <<-EOF.strip_heredoc + INSERT INTO project_authorizations (user_id, project_id, access_level) + VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')} + EOF + end + end +end diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 34fd5a57b5e..03194fc2141 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -49,23 +49,21 @@ class ProjectFeature < ActiveRecord::Base end def builds_enabled? - return true unless builds_access_level - builds_access_level > DISABLED end def wiki_enabled? - return true unless wiki_access_level - wiki_access_level > DISABLED end def merge_requests_enabled? - return true unless merge_requests_access_level - merge_requests_access_level > DISABLED end + def issues_enabled? + issues_access_level > DISABLED + end + private # Validates builds and merge requests access level diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb index db46def11eb..6149c35cc61 100644 --- a/app/models/project_group_link.rb +++ b/app/models/project_group_link.rb @@ -16,6 +16,9 @@ class ProjectGroupLink < ActiveRecord::Base validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true validate :different_group + after_create :refresh_group_members_authorized_projects + after_destroy :refresh_group_members_authorized_projects + def self.access_options Gitlab::Access.options end @@ -35,4 +38,8 @@ class ProjectGroupLink < ActiveRecord::Base errors.add(:base, "Project cannot be shared with the project it is in.") end end + + def refresh_group_members_authorized_projects + group.refresh_members_authorized_projects + end end diff --git a/app/models/project_label.rb b/app/models/project_label.rb index 82f47f0e8fd..313815e5869 100644 --- a/app/models/project_label.rb +++ b/app/models/project_label.rb @@ -16,8 +16,8 @@ class ProjectLabel < Label 'project_id' end - def to_reference(target_project = nil, format: :id) - super(project, target_project, format: format) + def to_reference(target_project = nil, format: :id, full: false) + super(project, target_project: target_project, format: format, full: full) end private diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 7c23b766763..3728f5642e4 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -25,7 +25,7 @@ You can create a Personal Access Token here: http://app.asana.com/-/account_api' end - def to_param + def self.to_param 'asana' end @@ -44,7 +44,7 @@ http://app.asana.com/-/account_api' ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb index d839221d315..aeeff8917bf 100644 --- a/app/models/project_services/assembla_service.rb +++ b/app/models/project_services/assembla_service.rb @@ -12,7 +12,7 @@ class AssemblaService < Service 'Project Management Software (Source Commits Endpoint)' end - def to_param + def self.to_param 'assembla' end @@ -23,7 +23,7 @@ class AssemblaService < Service ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index b5c76e4d4fe..400020ee04a 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -1,4 +1,6 @@ class BambooService < CiService + include ReactiveService + prop_accessor :bamboo_url, :build_key, :username, :password validates :bamboo_url, presence: true, url: true, if: :activated? @@ -38,7 +40,7 @@ class BambooService < CiService 'You must set up automatic revision labeling and a repository trigger in Bamboo.' end - def to_param + def self.to_param 'bamboo' end @@ -54,35 +56,46 @@ class BambooService < CiService ] end - def supported_events - %w(push) + def build_page(sha, ref) + with_reactive_cache(sha, ref) {|cached| cached[:build_page] } end - def build_info(sha) - @response = get_path("rest/api/latest/result?label=#{sha}") + def commit_status(sha, ref) + with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } end - def build_page(sha, ref) - build_info(sha) if @response.nil? || !@response.code + def execute(data) + return unless supported_events.include?(data[:object_kind]) + + get_path("updateAndBuild.action?buildKey=#{build_key}") + end + + def calculate_reactive_cache(sha, ref) + response = get_path("rest/api/latest/result?label=#{sha}") + + { build_page: read_build_page(response), commit_status: read_commit_status(response) } + end - if @response.code != 200 || @response['results']['results']['size'] == '0' + private + + def read_build_page(response) + if response.code != 200 || response['results']['results']['size'] == '0' # If actual build link can't be determined, send user to build summary page. URI.join("#{bamboo_url}/", "browse/#{build_key}").to_s else # If actual build link is available, go to build result page. - result_key = @response['results']['results']['result']['planResultKey']['key'] + result_key = response['results']['results']['result']['planResultKey']['key'] URI.join("#{bamboo_url}/", "browse/#{result_key}").to_s end end - def commit_status(sha, ref) - build_info(sha) if @response.nil? || !@response.code - return :error unless @response.code == 200 || @response.code == 404 + def read_commit_status(response) + return :error unless response.code == 200 || response.code == 404 - status = if @response.code == 404 || @response['results']['results']['size'] == '0' + status = if response.code == 404 || response['results']['results']['size'] == '0' 'Pending' else - @response['results']['results']['result']['buildState'] + response['results']['results']['result']['buildState'] end if status.include?('Success') @@ -96,14 +109,6 @@ class BambooService < CiService end end - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - get_path("updateAndBuild.action?buildKey=#{build_key}") - end - - private - def build_url(path) URI.join("#{bamboo_url}/", path).to_s end diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb index 338e685339a..046e2809f45 100644 --- a/app/models/project_services/bugzilla_service.rb +++ b/app/models/project_services/bugzilla_service.rb @@ -19,7 +19,7 @@ class BugzillaService < IssueTrackerService end end - def to_param + def self.to_param 'bugzilla' end end diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index 86a06321e21..0956c4a4ede 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -1,9 +1,12 @@ require "addressable/uri" class BuildkiteService < CiService + include ReactiveService + ENDPOINT = "https://buildkite.com" - prop_accessor :project_url, :token, :enable_ssl_verification + prop_accessor :project_url, :token + boolean_accessor :enable_ssl_verification validates :project_url, presence: true, url: true, if: :activated? validates :token, presence: true, if: :activated? @@ -21,10 +24,6 @@ class BuildkiteService < CiService hook.save end - def supported_events - %w(push) - end - def execute(data) return unless supported_events.include?(data[:object_kind]) @@ -32,13 +31,7 @@ class BuildkiteService < CiService end def commit_status(sha, ref) - response = HTTParty.get(commit_status_path(sha), verify: false) - - if response.code == 200 && response['status'] - response['status'] - else - :error - end + with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } end def commit_status_path(sha) @@ -57,7 +50,7 @@ class BuildkiteService < CiService 'Continuous integration and deployments' end - def to_param + def self.to_param 'buildkite' end @@ -77,6 +70,19 @@ class BuildkiteService < CiService ] end + def calculate_reactive_cache(sha, ref) + response = HTTParty.get(commit_status_path(sha), verify: false) + + status = + if response.code == 200 && response['status'] + response['status'] + else + :error + end + + { commit_status: status } + end + private def webhook_token diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index 201b94b065b..ebd21e37189 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -19,11 +19,11 @@ class BuildsEmailService < Service 'Email the builds status to a list of recipients.' end - def to_param + def self.to_param 'builds_email' end - def supported_events + def self.supported_events %w(build) end diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index 5af93860d09..0de59af5652 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -12,7 +12,7 @@ class CampfireService < Service 'Simple web-based real-time group chat' end - def to_param + def self.to_param 'campfire' end @@ -24,7 +24,7 @@ class CampfireService < Service ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/slack_service/base_message.rb b/app/models/project_services/chat_message/base_message.rb index f1182824687..a03605d01fb 100644 --- a/app/models/project_services/slack_service/base_message.rb +++ b/app/models/project_services/chat_message/base_message.rb @@ -1,6 +1,6 @@ require 'slack-notifier' -class SlackService +module ChatMessage class BaseMessage def initialize(params) raise NotImplementedError diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/chat_message/build_message.rb index 0fca4267bad..53e35cb21bf 100644 --- a/app/models/project_services/slack_service/build_message.rb +++ b/app/models/project_services/chat_message/build_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class BuildMessage < BaseMessage attr_reader :sha attr_reader :ref_type diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb index cd87a79d0c6..14fd64e5332 100644 --- a/app/models/project_services/slack_service/issue_message.rb +++ b/app/models/project_services/chat_message/issue_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class IssueMessage < BaseMessage attr_reader :user_name attr_reader :title diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb index b7615c96068..ab5e8b24167 100644 --- a/app/models/project_services/slack_service/merge_message.rb +++ b/app/models/project_services/chat_message/merge_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class MergeMessage < BaseMessage attr_reader :user_name attr_reader :project_name diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/chat_message/note_message.rb index 9e84e90f38c..ca1d7207034 100644 --- a/app/models/project_services/slack_service/note_message.rb +++ b/app/models/project_services/chat_message/note_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class NoteMessage < BaseMessage attr_reader :message attr_reader :user_name @@ -46,25 +46,25 @@ class SlackService commit_sha = commit[:id] commit_sha = Commit.truncate_sha(commit_sha) commented_on_message( - "[commit #{commit_sha}](#{@note_url})", + "commit #{commit_sha}", format_title(commit[:message])) end def create_issue_note(issue) commented_on_message( - "[issue ##{issue[:iid]}](#{@note_url})", + "issue ##{issue[:iid]}", format_title(issue[:title])) end def create_merge_note(merge_request) commented_on_message( - "[merge request !#{merge_request[:iid]}](#{@note_url})", + "merge request !#{merge_request[:iid]}", format_title(merge_request[:title])) end def create_snippet_note(snippet) commented_on_message( - "[snippet ##{snippet[:id]}](#{@note_url})", + "snippet ##{snippet[:id]}", format_title(snippet[:title])) end @@ -76,8 +76,8 @@ class SlackService "[#{@project_name}](#{@project_url})" end - def commented_on_message(target_link, title) - @message = "#{@user_name} commented on #{target_link} in #{project_link}: *#{title}*" + def commented_on_message(target, title) + @message = "#{@user_name} [commented on #{target}](#{@note_url}) in #{project_link}: *#{title}*" end end end diff --git a/app/models/project_services/slack_service/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb index f06b3562965..210027565a8 100644 --- a/app/models/project_services/slack_service/pipeline_message.rb +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -1,11 +1,10 @@ -class SlackService +module ChatMessage class PipelineMessage < BaseMessage - attr_reader :sha, :ref_type, :ref, :status, :project_name, :project_url, + attr_reader :ref_type, :ref, :status, :project_name, :project_url, :user_name, :duration, :pipeline_id def initialize(data) pipeline_attributes = data[:object_attributes] - @sha = pipeline_attributes[:sha] @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' @ref = pipeline_attributes[:ref] @status = pipeline_attributes[:status] @@ -14,7 +13,7 @@ class SlackService @project_name = data[:project][:path_with_namespace] @project_url = data[:project][:web_url] - @user_name = data[:commit] && data[:commit][:author_name] + @user_name = (data[:user] && data[:user][:name]) || 'API' end def pretext @@ -73,7 +72,7 @@ class SlackService end def pipeline_link - "[#{Commit.truncate_sha(sha)}](#{pipeline_url})" + "[##{pipeline_id}](#{pipeline_url})" end end end diff --git a/app/models/project_services/slack_service/push_message.rb b/app/models/project_services/chat_message/push_message.rb index b26f3e9ddce..2d73b71ec37 100644 --- a/app/models/project_services/slack_service/push_message.rb +++ b/app/models/project_services/chat_message/push_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class PushMessage < BaseMessage attr_reader :after attr_reader :before diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb index 160ca3ac115..134083e4504 100644 --- a/app/models/project_services/slack_service/wiki_page_message.rb +++ b/app/models/project_services/chat_message/wiki_page_message.rb @@ -1,4 +1,4 @@ -class SlackService +module ChatMessage class WikiPageMessage < BaseMessage attr_reader :user_name attr_reader :title diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb new file mode 100644 index 00000000000..8468934425f --- /dev/null +++ b/app/models/project_services/chat_notification_service.rb @@ -0,0 +1,149 @@ +# Base class for Chat notifications services +# This class is not meant to be used directly, but only to inherit from. +class ChatNotificationService < Service + include ChatMessage + + default_value_for :category, 'chat' + + prop_accessor :webhook, :username, :channel + boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines + + validates :webhook, presence: true, url: true, if: :activated? + + def initialize_properties + # Custom serialized properties initialization + self.supported_events.each { |event| self.class.prop_accessor(event_channel_name(event)) } + + if properties.nil? + self.properties = {} + self.notify_only_broken_builds = true + self.notify_only_broken_pipelines = true + end + end + + def can_test? + valid? + end + + def self.supported_events + %w[push issue confidential_issue merge_request note tag_push + build pipeline wiki_page] + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + return unless webhook.present? + + object_kind = data[:object_kind] + + data = data.merge( + project_url: project_url, + project_name: project_name + ) + + # WebHook events often have an 'update' event that follows a 'open' or + # 'close' action. Ignore update events for now to prevent duplicate + # messages from arriving. + + message = get_message(object_kind, data) + + return false unless message + + channel_name = get_channel_field(object_kind).presence || channel + + opts = {} + opts[:channel] = channel_name if channel_name + opts[:username] = username if username + + notifier = Slack::Notifier.new(webhook, opts) + notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) + + true + end + + def event_channel_names + supported_events.map { |event| event_channel_name(event) } + end + + def event_field(event) + fields.find { |field| field[:name] == event_channel_name(event) } + end + + def global_fields + fields.reject { |field| field[:name].end_with?('channel') } + end + + def default_channel_placeholder + raise NotImplementedError + end + + private + + def get_message(object_kind, data) + case object_kind + when "push", "tag_push" + ChatMessage::PushMessage.new(data) + when "issue" + ChatMessage::IssueMessage.new(data) unless is_update?(data) + when "merge_request" + ChatMessage::MergeMessage.new(data) unless is_update?(data) + when "note" + ChatMessage::NoteMessage.new(data) + when "build" + ChatMessage::BuildMessage.new(data) if should_build_be_notified?(data) + when "pipeline" + ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data) + when "wiki_page" + ChatMessage::WikiPageMessage.new(data) + end + end + + def get_channel_field(event) + field_name = event_channel_name(event) + self.public_send(field_name) + end + + def build_event_channels + supported_events.reduce([]) do |channels, event| + channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel_placeholder } + end + end + + def event_channel_name(event) + "#{event}_channel" + end + + def project_name + project.name_with_namespace.gsub(/\s/, '') + end + + def project_url + project.web_url + end + + def is_update?(data) + data[:object_attributes][:action] == 'update' + end + + def should_build_be_notified?(data) + case data[:commit][:status] + when 'success' + !notify_only_broken_builds? + when 'failed' + true + else + false + end + end + + def should_pipeline_be_notified?(data) + case data[:object_attributes][:status] + when 'success' + !notify_only_broken_pipelines? + when 'failed' + true + else + false + end + end +end diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb new file mode 100644 index 00000000000..2bcff541cc0 --- /dev/null +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -0,0 +1,56 @@ +# Base class for Chat services +# This class is not meant to be used directly, but only to inherrit from. +class ChatSlashCommandsService < Service + default_value_for :category, 'chat' + + prop_accessor :token + + has_many :chat_names, foreign_key: :service_id, dependent: :destroy + + def valid_token?(token) + self.respond_to?(:token) && + self.token.present? && + ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) + end + + def self.supported_events + %w() + end + + def can_test? + false + end + + def fields + [ + { type: 'text', name: 'token', placeholder: '' } + ] + end + + def trigger(params) + return unless valid_token?(params[:token]) + + user = find_chat_user(params) + unless user + url = authorize_chat_name_url(params) + return presenter.authorize_chat_name(url) + end + + Gitlab::ChatCommands::Command.new(project, user, + params).execute + end + + private + + def find_chat_user(params) + ChatNames::FindUserService.new(self, params).execute + end + + def authorize_chat_name_url(params) + ChatNames::AuthorizeUserService.new(self, params).execute + end + + def presenter + Gitlab::ChatCommands::Presenter.new + end +end diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index 596c00705ad..82979c8bd34 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -8,19 +8,11 @@ class CiService < Service self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) end - def supported_events + def self.supported_events %w(push) end - def merge_request_page(iid, sha, ref) - commit_page(sha, ref) - end - - def commit_page(sha, ref) - build_page(sha, ref) - end - - # Return complete url to merge_request page + # Return complete url to build page # # Ex. # http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c @@ -35,23 +27,6 @@ class CiService < Service # # # Ex. - # @service.merge_request_status(9, '13be4ac', 'dev') - # # => 'success' - # - # @service.merge_request_status(10, '2abe4ac', 'dev) - # # => 'running' - # - # - def merge_request_status(iid, sha, ref) - commit_status(sha, ref) - end - - # Return string with build status or :error symbol - # - # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped' - # - # - # Ex. # @service.commit_status('13be4ac', 'master') # # => 'success' # diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index b2f426dc2ac..dea915a4d05 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -23,7 +23,7 @@ class CustomIssueTrackerService < IssueTrackerService end end - def to_param + def self.to_param 'custom_issue_tracker' end diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb new file mode 100644 index 00000000000..91a55514a9a --- /dev/null +++ b/app/models/project_services/deployment_service.rb @@ -0,0 +1,33 @@ +# Base class for deployment services +# +# These services integrate with a deployment solution like Kubernetes/OpenShift, +# Mesosphere, etc, to provide additional features to environments. +class DeploymentService < Service + default_value_for :category, 'deployment' + + def self.supported_events + %w() + end + + def predefined_variables + [] + end + + # Environments may have a number of terminals. Should return an array of + # hashes describing them, e.g.: + # + # [{ + # :selectors => {"a" => "b", "foo" => "bar"}, + # :url => "wss://external.example.com/exec", + # :headers => {"Authorization" => "Token xxx"}, + # :subprotocols => ["foo"], + # :ca_pem => "----BEGIN CERTIFICATE...", # optional + # :created_at => Time.now.utc + # }] + # + # Selectors should be a set of values that uniquely identify a particular + # terminal + def terminals(environment) + raise NotImplementedError + end +end diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index 5e4dd101c53..0a217d8caba 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -1,5 +1,8 @@ class DroneCiService < CiService - prop_accessor :drone_url, :token, :enable_ssl_verification + include ReactiveService + + prop_accessor :drone_url, :token + boolean_accessor :enable_ssl_verification validates :drone_url, presence: true, url: true, if: :activated? validates :token, presence: true, if: :activated? @@ -29,18 +32,10 @@ class DroneCiService < CiService true end - def supported_events + def self.supported_events %w(push merge_request tag_push) end - def merge_request_status_path(iid, sha = nil, ref = nil) - url = [drone_url, - "gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}", - "?access_token=#{token}"] - - URI.join(*url).to_s - end - def commit_status_path(sha, ref) url = [drone_url, "gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}", @@ -49,54 +44,34 @@ class DroneCiService < CiService URI.join(*url).to_s end - def merge_request_status(iid, sha, ref) - response = HTTParty.get(merge_request_status_path(iid), verify: enable_ssl_verification) - - if response.code == 200 and response['status'] - case response['status'] - when 'killed' - :canceled - when 'failure', 'error' - # Because drone return error if some test env failed - :failed - else - response["status"] - end - else - :error - end - rescue Errno::ECONNREFUSED - :error + def commit_status(sha, ref) + with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } end - def commit_status(sha, ref) + def calculate_reactive_cache(sha, ref) response = HTTParty.get(commit_status_path(sha, ref), verify: enable_ssl_verification) - if response.code == 200 and response['status'] - case response['status'] - when 'killed' - :canceled - when 'failure', 'error' - # Because drone return error if some test env failed - :failed + status = + if response.code == 200 and response['status'] + case response['status'] + when 'killed' + :canceled + when 'failure', 'error' + # Because drone return error if some test env failed + :failed + else + response["status"] + end else - response["status"] + :error end - else - :error - end - rescue Errno::ECONNREFUSED - :error - end - def merge_request_page(iid, sha, ref) - url = [drone_url, - "gitlab/#{project.namespace.path}/#{project.path}/redirect/pulls/#{iid}"] - - URI.join(*url).to_s + { commit_status: status } + rescue Errno::ECONNREFUSED + { commit_status: :error } end - def commit_page(sha, ref) + def build_page(sha, ref) url = [drone_url, "gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}", "?branch=#{URI::encode(ref.to_s)}"] @@ -104,14 +79,6 @@ class DroneCiService < CiService URI.join(*url).to_s end - def commit_coverage(sha, ref) - nil - end - - def build_page(sha, ref) - commit_page(sha, ref) - end - def title 'Drone CI' end @@ -120,7 +87,7 @@ class DroneCiService < CiService 'Drone is a Continuous Integration platform built on Docker, written in Go' end - def to_param + def self.to_param 'drone_ci' end diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index e0083c43adb..f4f913ee0b6 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -1,6 +1,6 @@ class EmailsOnPushService < Service - prop_accessor :send_from_committer_email - prop_accessor :disable_diffs + boolean_accessor :send_from_committer_email + boolean_accessor :disable_diffs prop_accessor :recipients validates :recipients, presence: true, if: :activated? @@ -12,11 +12,11 @@ class EmailsOnPushService < Service 'Email the commits and diff of each push to a list of recipients.' end - def to_param + def self.to_param 'emails_on_push' end - def supported_events + def self.supported_events %w(push tag_push) end @@ -24,20 +24,20 @@ class EmailsOnPushService < Service return unless supported_events.include?(push_data[:object_kind]) EmailsOnPushWorker.perform_async( - project_id, - recipients, - push_data, - send_from_committer_email: send_from_committer_email?, - disable_diffs: disable_diffs? + project_id, + recipients, + push_data, + send_from_committer_email: send_from_committer_email?, + disable_diffs: disable_diffs? ) end def send_from_committer_email? - self.send_from_committer_email == "1" + Gitlab::Utils.to_boolean(self.send_from_committer_email) end def disable_diffs? - self.disable_diffs == "1" + Gitlab::Utils.to_boolean(self.disable_diffs) end def fields diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index d7b6e505191..bdf6fa6a586 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -13,7 +13,7 @@ class ExternalWikiService < Service 'Replaces the link to the internal wiki with a link to an external wiki.' end - def to_param + def self.to_param 'external_wiki' end @@ -29,4 +29,8 @@ class ExternalWikiService < Service nil end end + + def self.supported_events + %w() + end end diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index dd00275187f..10a13c3fbdc 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -12,7 +12,7 @@ class FlowdockService < Service 'Flowdock is a collaboration web app for technical teams.' end - def to_param + def self.to_param 'flowdock' end @@ -22,7 +22,7 @@ class FlowdockService < Service ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 598aca5e06d..f271e1f1739 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -12,7 +12,7 @@ class GemnasiumService < Service 'Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.' end - def to_param + def self.to_param 'gemnasium' end @@ -23,7 +23,7 @@ class GemnasiumService < Service ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 6bd8d4ec568..ad4eb9536e1 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -7,7 +7,7 @@ class GitlabIssueTrackerService < IssueTrackerService default_value_for :default, true - def to_param + def self.to_param 'gitlab' end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 660a8ae3421..72da219df28 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -8,8 +8,8 @@ class HipchatService < Service ul ol li dl dt dd ] - prop_accessor :token, :room, :server, :notify, :color, :api_version - boolean_accessor :notify_only_broken_builds + prop_accessor :token, :room, :server, :color, :api_version + boolean_accessor :notify_only_broken_builds, :notify validates :token, presence: true, if: :activated? def initialize_properties @@ -27,7 +27,7 @@ class HipchatService < Service 'Private group chat and IM' end - def to_param + def self.to_param 'hipchat' end @@ -45,7 +45,7 @@ class HipchatService < Service ] end - def supported_events + def self.supported_events %w(push issue confidential_issue merge_request note tag_push build) end @@ -75,7 +75,7 @@ class HipchatService < Service end def message_options(data = nil) - { notify: notify.present? && notify == '1', color: message_color(data) } + { notify: notify.present? && Gitlab::Utils.to_boolean(notify), color: message_color(data) } end def create_message(data) diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index ce7d1c5d5b1..5d93064f9b3 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -2,7 +2,8 @@ require 'uri' class IrkerService < Service prop_accessor :server_host, :server_port, :default_irc_uri - prop_accessor :colorize_messages, :recipients, :channels + prop_accessor :recipients, :channels + boolean_accessor :colorize_messages validates :recipients, presence: true, if: :activated? before_validation :get_channels @@ -16,11 +17,11 @@ class IrkerService < Service 'gateway.' end - def to_param + def self.to_param 'irker' end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 207bb816ad1..9e65fdbf9d6 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -57,7 +57,7 @@ class IssueTrackerService < Service end end - def supported_events + def self.supported_events %w(push) end @@ -85,8 +85,8 @@ class IssueTrackerService < Service def enabled_in_gitlab_config Gitlab.config.issues_tracker && - Gitlab.config.issues_tracker.values.any? && - issues_tracker + Gitlab.config.issues_tracker.values.any? && + issues_tracker end def issues_tracker diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 2dbe0075465..2ac76e97de0 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -1,24 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# - class JiraService < IssueTrackerService include Gitlab::Routing.url_helpers @@ -30,6 +9,13 @@ class JiraService < IssueTrackerService before_update :reset_password + # This is confusing, but JiraService does not really support these events. + # The values here are required to display correct options in the service + # configuration screen. + def self.supported_events + %w(commit merge_request) + end + # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1 def reference_pattern @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} @@ -70,13 +56,13 @@ class JiraService < IssueTrackerService end def jira_project - @jira_project ||= client.Project.find(project_key) + @jira_project ||= jira_request { client.Project.find(project_key) } end def help - 'See the ' \ - '[integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) '\ - 'for details.' + 'You need to configure JIRA before enabling this service. For more details + read the + [JIRA service documentation](https://docs.gitlab.com/ce/project_services/jira.html).' end def title @@ -95,7 +81,7 @@ class JiraService < IssueTrackerService end end - def to_param + def self.to_param 'jira' end @@ -122,27 +108,43 @@ class JiraService < IssueTrackerService "#{url}/secure/CreateIssue.jspa" end - def execute(push, issue = nil) - if issue.nil? - # No specific issue, that means - # we just want to test settings - test_settings - else - close_issue(push, issue) - end + def execute(push) + # This method is a no-op, because currently JiraService does not + # support any events. + end + + def close_issue(entity, external_issue) + issue = jira_request { client.Issue.find(external_issue.iid) } + + return if issue.nil? || issue.resolution.present? || !jira_issue_transition_id.present? + + commit_id = if entity.is_a?(Commit) + entity.id + elsif entity.is_a?(MergeRequest) + entity.diff_head_sha + end + + commit_url = build_entity_url(:commit, commit_id) + + # Depending on the JIRA project's workflow, a comment during transition + # may or may not be allowed. Refresh the issue after transition and check + # if it is closed, so we don't have one comment for every commit. + issue = jira_request { client.Issue.find(issue.key) } if transition_issue(issue) + add_issue_solved_comment(issue, commit_id, commit_url) if issue.resolution end def create_cross_reference_note(mentioned, noteable, author) - issue_key = mentioned.id - project = self.project - noteable_name = noteable.class.name.underscore.downcase - noteable_id = if noteable.is_a?(Commit) - noteable.id - else - noteable.iid - end + unless can_cross_reference?(noteable) + return "Events for #{noteable.model_name.plural.humanize(capitalize: false)} are disabled." + end + + jira_issue = jira_request { client.Issue.find(mentioned.id) } + + return unless jira_issue.present? - entity_url = build_entity_url(noteable_name.to_sym, noteable_id) + noteable_id = noteable.respond_to?(:iid) ? noteable.iid : noteable.id + noteable_type = noteable_name(noteable) + entity_url = build_entity_url(noteable_type, noteable_id) data = { user: { @@ -150,17 +152,17 @@ class JiraService < IssueTrackerService url: resource_url(user_path(author)), }, project: { - name: project.path_with_namespace, - url: resource_url(namespace_project_path(project.namespace, project)) + name: self.project.path_with_namespace, + url: resource_url(namespace_project_path(project.namespace, self.project)) }, entity: { - name: noteable_name.humanize.downcase, + name: noteable_type.humanize.downcase, url: entity_url, title: noteable.title } } - add_comment(data, issue_key) + add_comment(data, jira_issue) end # reason why service cannot be tested @@ -168,6 +170,11 @@ class JiraService < IssueTrackerService "Please fill in Password and Username." end + def test(_) + result = test_settings + { success: result.present?, result: result } + end + def can_test? username.present? && password.present? end @@ -181,91 +188,124 @@ class JiraService < IssueTrackerService def test_settings return unless url.present? # Test settings by getting the project - jira_project - - rescue Errno::ECONNREFUSED, JIRA::HTTPError => e - Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{url}." - false + jira_request { jira_project.present? } end private - def close_issue(entity, issue) - commit_id = if entity.is_a?(Commit) - entity.id - elsif entity.is_a?(MergeRequest) - entity.diff_head_sha - end - - commit_url = build_entity_url(:commit, commit_id) - - # Depending on the JIRA project's workflow, a comment during transition - # may or may not be allowed. Split the operation in to two calls so the - # comment always works. - transition_issue(issue) - add_issue_solved_comment(issue, commit_id, commit_url) + def can_cross_reference?(noteable) + case noteable + when Commit then commit_events + when MergeRequest then merge_requests_events + else true + end end def transition_issue(issue) - issue = client.Issue.find(issue.iid) issue.transitions.build.save(transition: { id: jira_issue_transition_id }) end def add_issue_solved_comment(issue, commit_id, commit_url) - comment = "Issue solved with [#{commit_id}|#{commit_url}]." - send_message(issue.iid, comment) + link_title = "GitLab: Solved by commit #{commit_id}." + comment = "Issue solved with [#{commit_id}|#{commit_url}]." + link_props = build_remote_link_props(url: commit_url, title: link_title, resolved: true) + send_message(issue, comment, link_props) end - def add_comment(data, issue_key) - user_name = data[:user][:name] - user_url = data[:user][:url] - entity_name = data[:entity][:name] - entity_url = data[:entity][:url] + def add_comment(data, issue) + user_name = data[:user][:name] + user_url = data[:user][:url] + entity_name = data[:entity][:name] + entity_url = data[:entity][:url] entity_title = data[:entity][:title] project_name = data[:project][:name] - message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'" + message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title.chomp}'" + link_title = "GitLab: Mentioned on #{entity_name} - #{entity_title}" + link_props = build_remote_link_props(url: entity_url, title: link_title) - unless comment_exists?(issue_key, message) - send_message(issue_key, message) + unless comment_exists?(issue, message) + send_message(issue, message, link_props) end end - def comment_exists?(issue_key, message) - comments = client.Issue.find(issue_key).comments - comments.map { |comment| comment.body.include?(message) }.any? + def comment_exists?(issue, message) + comments = jira_request { issue.comments } + + comments.present? && comments.any? { |comment| comment.body.include?(message) } end - def send_message(issue_key, message) + def send_message(issue, message, remote_link_props) return unless url.present? - issue = client.Issue.find(issue_key) + jira_request do + if issue.comments.build.save!(body: message) + remote_link = issue.remotelink.build + remote_link.save!(remote_link_props) + result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}." + end - if issue.comments.build.save!(body: message) - result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}." + Rails.logger.info(result_message) + result_message end + end - Rails.logger.info(result_message) - result_message - rescue URI::InvalidURIError, Errno::ECONNREFUSED, JIRA::HTTPError => e - Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}" + # Build remote link on JIRA properties + # Icons here must be available on WEB so JIRA can read the URL + # We are using a open word graphics icon which have LGPL license + def build_remote_link_props(url:, title:, resolved: false) + status = { + resolved: resolved + } + + if resolved + status[:icon] = { + title: 'Closed', + url16x16: 'http://www.openwebgraphics.com/resources/data/1768/16x16_apply.png' + } + end + + { + GlobalID: 'GitLab', + object: { + url: url, + title: title, + status: status, + icon: { title: 'GitLab', url16x16: 'https://gitlab.com/favicon.ico' } + } + } end def resource_url(resource) "#{Settings.gitlab.base_url.chomp("/")}#{resource}" end - def build_entity_url(entity_name, entity_id) - resource_url( - polymorphic_url( - [ - self.project.namespace.becomes(Namespace), - self.project, - entity_name - ], - id: entity_id, - routing_type: :path - ) + def build_entity_url(noteable_type, entity_id) + polymorphic_url( + [ + self.project.namespace.becomes(Namespace), + self.project, + noteable_type.to_sym + ], + id: entity_id, + host: Settings.gitlab.base_url ) end + + def noteable_name(noteable) + name = noteable.model_name.singular + + # ProjectSnippet inherits from Snippet class so it causes + # routing error building the URL. + name == "project_snippet" ? "snippet" : name + end + + # Handle errors when doing JIRA API calls + def jira_request + yield + + rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError => e + Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}" + nil + end end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb new file mode 100644 index 00000000000..fa3cedc4354 --- /dev/null +++ b/app/models/project_services/kubernetes_service.rb @@ -0,0 +1,173 @@ +class KubernetesService < DeploymentService + include Gitlab::Kubernetes + include ReactiveCaching + + self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] } + + # Namespace defaults to the project path, but can be overridden in case that + # is an invalid or inappropriate name + prop_accessor :namespace + + # Access to kubernetes is directly through the API + prop_accessor :api_url + + # Bearer authentication + # TODO: user/password auth, client certificates + prop_accessor :token + + # Provide a custom CA bundle for self-signed deployments + prop_accessor :ca_pem + + with_options presence: true, if: :activated? do + validates :api_url, url: true + validates :token + + validates :namespace, + format: { + with: Gitlab::Regex.kubernetes_namespace_regex, + message: Gitlab::Regex.kubernetes_namespace_regex_message, + }, + length: 1..63 + end + + after_save :clear_reactive_cache! + + def initialize_properties + if properties.nil? + self.properties = {} + self.namespace = project.path if project.present? + end + end + + def title + 'Kubernetes' + end + + def description + 'Kubernetes / Openshift integration' + end + + def help + 'To enable terminal access to Kubernetes environments, label your ' \ + 'deployments with `app=$CI_ENVIRONMENT_SLUG`' + end + + def self.to_param + 'kubernetes' + end + + def fields + [ + { type: 'text', + name: 'namespace', + title: 'Kubernetes namespace', + placeholder: 'Kubernetes namespace', + }, + { type: 'text', + name: 'api_url', + title: 'API URL', + placeholder: 'Kubernetes API URL, like https://kube.example.com/', + }, + { type: 'text', + name: 'token', + title: 'Service token', + placeholder: 'Service token', + }, + { type: 'textarea', + name: 'ca_pem', + title: 'Custom CA bundle', + placeholder: 'Certificate Authority bundle (PEM format)', + }, + ] + end + + # Check we can connect to the Kubernetes API + def test(*args) + kubeclient = build_kubeclient! + + kubeclient.discover + { success: kubeclient.discovered, result: "Checked API discovery endpoint" } + rescue => err + { success: false, result: err } + end + + def predefined_variables + variables = [ + { key: 'KUBE_URL', value: api_url, public: true }, + { key: 'KUBE_TOKEN', value: token, public: false }, + { key: 'KUBE_NAMESPACE', value: namespace, public: true } + ] + variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } if ca_pem.present? + variables + end + + # Constructs a list of terminals from the reactive cache + # + # Returns nil if the cache is empty, in which case you should try again a + # short time later + def terminals(environment) + with_reactive_cache do |data| + pods = data.fetch(:pods, nil) + filter_pods(pods, app: environment.slug). + flat_map { |pod| terminals_for_pod(api_url, namespace, pod) }. + map { |terminal| add_terminal_auth(terminal, token, ca_pem) } + end + end + + # Caches all pods in the namespace so other calls don't need to block on + # network access. + def calculate_reactive_cache + return unless active? && project && !project.pending_delete? + + kubeclient = build_kubeclient! + + # Store as hashes, rather than as third-party types + pods = begin + kubeclient.get_pods(namespace: namespace).as_json + rescue KubeException => err + raise err unless err.error_code == 404 + [] + end + + # We may want to cache extra things in the future + { pods: pods } + end + + private + + def build_kubeclient!(api_path: 'api', api_version: 'v1') + raise "Incomplete settings" unless api_url && namespace && token + + ::Kubeclient::Client.new( + join_api_url(api_path), + api_version, + auth_options: kubeclient_auth_options, + ssl_options: kubeclient_ssl_options, + http_proxy_uri: ENV['http_proxy'] + ) + end + + def kubeclient_ssl_options + opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } + + if ca_pem.present? + opts[:cert_store] = OpenSSL::X509::Store.new + opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) + end + + opts + end + + def kubeclient_auth_options + { bearer_token: token } + end + + def join_api_url(*parts) + url = URI.parse(api_url) + prefix = url.path.sub(%r{/+\z}, '') + + url.path = [ prefix, *parts ].join("/") + + url.to_s + end +end diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb new file mode 100644 index 00000000000..4ebc5318da1 --- /dev/null +++ b/app/models/project_services/mattermost_service.rb @@ -0,0 +1,41 @@ +class MattermostService < ChatNotificationService + def title + 'Mattermost notifications' + end + + def description + 'Receive event notifications in Mattermost' + end + + def self.to_param + 'mattermost' + end + + def help + 'This service sends notifications about projects events to Mattermost channels.<br /> + To set up this service: + <ol> + <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li> + <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li> + <li>Paste the webhook <strong>URL</strong> into the field bellow. </li> + <li>Select events below to enable notifications. The channel and username are optional. </li> + </ol>' + end + + def fields + default_fields + build_event_channels + end + + def default_fields + [ + { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' }, + { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, + ] + end + + def default_channel_placeholder + "town-square" + end +end diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb new file mode 100644 index 00000000000..50a011db74e --- /dev/null +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -0,0 +1,51 @@ +class MattermostSlashCommandsService < ChatSlashCommandsService + include TriggersHelper + + prop_accessor :token + + def can_test? + false + end + + def title + 'Mattermost Command' + end + + def description + "Perform common operations on GitLab in Mattermost" + end + + def self.to_param + 'mattermost_slash_commands' + end + + def configure(user, params) + token = Mattermost::Command.new(user). + create(command(params)) + + update(active: true, token: token) if token + rescue Mattermost::Error => e + [false, e.message] + end + + def list_teams(user) + Mattermost::Team.new(user).all + rescue Mattermost::Error => e + [[], e.message] + end + + private + + def command(params) + pretty_project_name = project.name_with_namespace + + params.merge( + auto_complete: true, + auto_complete_desc: "Perform common operations on: #{pretty_project_name}", + auto_complete_hint: '[help]', + description: "Perform common operations on: #{pretty_project_name}", + display_name: "GitLab / #{pretty_project_name}", + method: 'P', + username: 'GitLab') + end +end diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb index 745f9bd1b43..ac617f409d9 100644 --- a/app/models/project_services/pipelines_email_service.rb +++ b/app/models/project_services/pipelines_email_service.rb @@ -15,11 +15,11 @@ class PipelinesEmailService < Service 'Email the pipelines status to a list of recipients.' end - def to_param + def self.to_param 'pipelines_email' end - def supported_events + def self.supported_events %w[pipeline] end diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index 5301f9fa0ff..9cc642591f4 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -14,7 +14,7 @@ class PivotaltrackerService < Service 'Project Management Software (Source Commits Endpoint)' end - def to_param + def self.to_param 'pivotaltracker' end @@ -34,7 +34,7 @@ class PivotaltrackerService < Service ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index 3dd878e4c7d..a963d27a376 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -13,7 +13,7 @@ class PushoverService < Service 'Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.' end - def to_param + def self.to_param 'pushover' end @@ -61,7 +61,7 @@ class PushoverService < Service ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index f9da273cf08..6acf611eba5 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -19,7 +19,7 @@ class RedmineService < IssueTrackerService end end - def to_param + def self.to_param 'redmine' end end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index e1b937817f4..f77d2d7c60b 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -1,176 +1,40 @@ -class SlackService < Service - prop_accessor :webhook, :username, :channel - boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines - validates :webhook, presence: true, url: true, if: :activated? - - def initialize_properties - # Custom serialized properties initialization - self.supported_events.each { |event| self.class.prop_accessor(event_channel_name(event)) } - - if properties.nil? - self.properties = {} - self.notify_only_broken_builds = true - self.notify_only_broken_pipelines = true - end - end - +class SlackService < ChatNotificationService def title - 'Slack' + 'Slack notifications' end def description - 'A team communication tool for the 21st century' + 'Receive event notifications in Slack' end - def to_param + def self.to_param 'slack' end def help - 'This service sends notifications to your Slack channel.<br/> - To setup this Service you need to create a new <b>"Incoming webhook"</b> in your Slack integration panel, - and enter the Webhook URL below.' + 'This service sends notifications about projects events to Slack channels.<br /> + To setup this service: + <ol> + <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li> + <li>Paste the <strong>Webhook URL</strong> into the field below. </li> + <li>Select events below to enable notifications. The channel and username are optional. </li> + </ol>' end def fields - default_fields = - [ - { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, - { type: 'text', name: 'username', placeholder: 'username' }, - { type: 'text', name: 'channel', placeholder: "#general" }, - { type: 'checkbox', name: 'notify_only_broken_builds' }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - ] - default_fields + build_event_channels end - def supported_events - %w[push issue confidential_issue merge_request note tag_push - build pipeline wiki_page] - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - return unless webhook.present? - - object_kind = data[:object_kind] - - data = data.merge( - project_url: project_url, - project_name: project_name - ) - - # WebHook events often have an 'update' event that follows a 'open' or - # 'close' action. Ignore update events for now to prevent duplicate - # messages from arriving. - - message = get_message(object_kind, data) - - if message - opt = {} - - event_channel = get_channel_field(object_kind) || channel - - opt[:channel] = event_channel if event_channel - opt[:username] = username if username - - notifier = Slack::Notifier.new(webhook, opt) - notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) - - true - else - false - end - end - - def event_channel_names - supported_events.map { |event| event_channel_name(event) } - end - - def event_field(event) - fields.find { |field| field[:name] == event_channel_name(event) } + def default_fields + [ + { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, + { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, + ] end - def global_fields - fields.reject { |field| field[:name].end_with?('channel') } - end - - private - - def get_message(object_kind, data) - case object_kind - when "push", "tag_push" - PushMessage.new(data) - when "issue" - IssueMessage.new(data) unless is_update?(data) - when "merge_request" - MergeMessage.new(data) unless is_update?(data) - when "note" - NoteMessage.new(data) - when "build" - BuildMessage.new(data) if should_build_be_notified?(data) - when "pipeline" - PipelineMessage.new(data) if should_pipeline_be_notified?(data) - when "wiki_page" - WikiPageMessage.new(data) - end - end - - def get_channel_field(event) - field_name = event_channel_name(event) - self.public_send(field_name) - end - - def build_event_channels - supported_events.reduce([]) do |channels, event| - channels << { type: 'text', name: event_channel_name(event), placeholder: "#general" } - end - end - - def event_channel_name(event) - "#{event}_channel" - end - - def project_name - project.name_with_namespace.gsub(/\s/, '') - end - - def project_url - project.web_url - end - - def is_update?(data) - data[:object_attributes][:action] == 'update' - end - - def should_build_be_notified?(data) - case data[:commit][:status] - when 'success' - !notify_only_broken_builds? - when 'failed' - true - else - false - end - end - - def should_pipeline_be_notified?(data) - case data[:object_attributes][:status] - when 'success' - !notify_only_broken_pipelines? - when 'failed' - true - else - false - end + def default_channel_placeholder + "#general" end end - -require "slack_service/issue_message" -require "slack_service/push_message" -require "slack_service/merge_message" -require "slack_service/note_message" -require "slack_service/build_message" -require "slack_service/pipeline_message" -require "slack_service/wiki_page_message" diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb new file mode 100644 index 00000000000..c34991e4262 --- /dev/null +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -0,0 +1,28 @@ +class SlackSlashCommandsService < ChatSlashCommandsService + include TriggersHelper + + def title + 'Slack Command' + end + + def description + "Perform common operations on GitLab in Slack" + end + + def self.to_param + 'slack_slash_commands' + end + + def trigger(params) + # Format messages to be Slack-compatible + super.tap do |result| + result[:text] = format(result[:text]) if result.is_a?(Hash) + end + end + + private + + def format(text) + Slack::Notifier::LinkFormatter.format(text) if text + end +end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index a4a967c9bc9..cbaffb8ce48 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -1,4 +1,6 @@ class TeamcityService < CiService + include ReactiveService + prop_accessor :teamcity_url, :build_type, :username, :password validates :teamcity_url, presence: true, url: true, if: :activated? @@ -41,14 +43,10 @@ class TeamcityService < CiService 'requests build, that setting is in the vsc root advanced settings.' end - def to_param + def self.to_param 'teamcity' end - def supported_events - %w(push) - end - def fields [ { type: 'text', name: 'teamcity_url', @@ -61,43 +59,18 @@ class TeamcityService < CiService ] end - def build_info(sha) - @response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}") - end - def build_page(sha, ref) - build_info(sha) if @response.nil? || !@response.code - - if @response.code != 200 - # If actual build link can't be determined, - # send user to build summary page. - build_url("viewLog.html?buildTypeId=#{build_type}") - else - # If actual build link is available, go to build result page. - built_id = @response['build']['id'] - build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}") - end + with_reactive_cache(sha, ref) {|cached| cached[:build_page] } end def commit_status(sha, ref) - build_info(sha) if @response.nil? || !@response.code - return :error unless @response.code == 200 || @response.code == 404 + with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } + end - status = if @response.code == 404 - 'Pending' - else - @response['build']['status'] - end + def calculate_reactive_cache(sha, ref) + response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}") - if status.include?('SUCCESS') - 'success' - elsif status.include?('FAILURE') - 'failed' - elsif status.include?('Pending') - 'pending' - else - :error - end + { build_page: read_build_page(response), commit_status: read_commit_status(response) } end def execute(data) @@ -122,6 +95,40 @@ class TeamcityService < CiService private + def read_build_page(response) + if response.code != 200 + # If actual build link can't be determined, + # send user to build summary page. + build_url("viewLog.html?buildTypeId=#{build_type}") + else + # If actual build link is available, go to build result page. + built_id = response['build']['id'] + build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}") + end + end + + def read_commit_status(response) + return :error unless response.code == 200 || response.code == 404 + + status = if response.code == 404 + 'Pending' + else + response['build']['status'] + end + + return :error unless status.present? + + if status.include?('SUCCESS') + 'success' + elsif status.include?('FAILURE') + 'failed' + elsif status.include?('Pending') + 'pending' + else + :error + end + end + def build_url(path) URI.join("#{teamcity_url}/", path).to_s end diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb new file mode 100644 index 00000000000..06abd406523 --- /dev/null +++ b/app/models/project_statistics.rb @@ -0,0 +1,44 @@ +class ProjectStatistics < ActiveRecord::Base + belongs_to :project + belongs_to :namespace + + before_save :update_storage_size + + STORAGE_COLUMNS = [:repository_size, :lfs_objects_size, :build_artifacts_size] + STATISTICS_COLUMNS = [:commit_count] + STORAGE_COLUMNS + + def total_repository_size + repository_size + lfs_objects_size + end + + def refresh!(only: nil) + STATISTICS_COLUMNS.each do |column, generator| + if only.blank? || only.include?(column) + public_send("update_#{column}") + end + end + + save! + end + + def update_commit_count + 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 * 1.megabyte + end + + def update_lfs_objects_size + self.lfs_objects_size = project.lfs_objects.sum(:size) + end + + def update_build_artifacts_size + self.build_artifacts_size = project.builds.sum(:artifacts_size) + end + + def update_storage_size + self.storage_size = STORAGE_COLUMNS.sum(&method(:read_attribute)) + end +end diff --git a/app/models/project_team.rb b/app/models/project_team.rb index a6e911df9bd..8a53e974b6f 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -21,6 +21,22 @@ class ProjectTeam end end + def add_guest(user, current_user: nil) + self << [user, :guest, current_user] + end + + def add_reporter(user, current_user: nil) + self << [user, :reporter, current_user] + end + + def add_developer(user, current_user: nil) + self << [user, :developer, current_user] + end + + def add_master(user, current_user: nil) + self << [user, :master, current_user] + end + def find_member(user_id) member = project.members.find_by(user_id: user_id) @@ -64,19 +80,19 @@ class ProjectTeam alias_method :users, :members def guests - @guests ||= fetch_members(:guests) + @guests ||= fetch_members(Gitlab::Access::GUEST) end def reporters - @reporters ||= fetch_members(:reporters) + @reporters ||= fetch_members(Gitlab::Access::REPORTER) end def developers - @developers ||= fetch_members(:developers) + @developers ||= fetch_members(Gitlab::Access::DEVELOPER) end def masters - @masters ||= fetch_members(:masters) + @masters ||= fetch_members(Gitlab::Access::MASTER) end def import(source_project, current_user = nil) @@ -125,8 +141,12 @@ class ProjectTeam max_member_access(user.id) == Gitlab::Access::MASTER end - def member?(user, min_member_access = Gitlab::Access::GUEST) - max_member_access(user.id) >= min_member_access + # Checks if `user` is authorized for this project, with at least the + # `min_access_level` (if given). + def member?(user, min_access_level = Gitlab::Access::GUEST) + return false unless user + + user.authorized_project?(project, min_access_level) end def human_max_access(user_id) @@ -149,112 +169,29 @@ class ProjectTeam # Lookup only the IDs we need user_ids = user_ids - access.keys + users_access = project.project_authorizations. + where(user: user_ids). + group(:user_id). + maximum(:access_level) - if user_ids.present? - user_ids.each { |id| access[id] = Gitlab::Access::NO_ACCESS } - - member_access = project.members.access_for_user_ids(user_ids) - merge_max!(access, member_access) - - if group - group_access = group.members.access_for_user_ids(user_ids) - merge_max!(access, group_access) - end - - # Each group produces a list of maximum access level per user. We take the - # max of the values produced by each group. - if project_shared_with_group? - project.project_group_links.each do |group_link| - invited_access = max_invited_level_for_users(group_link, user_ids) - merge_max!(access, invited_access) - end - end - end - + access.merge!(users_access) access end def max_member_access(user_id) - max_member_access_for_user_ids([user_id])[user_id] + max_member_access_for_user_ids([user_id])[user_id] || Gitlab::Access::NO_ACCESS end private - # For a given group, return the maximum access level for the user. This is the min of - # the invited access level of the group and the access level of the user within the group. - # For example, if the group has been given DEVELOPER access but the member has MASTER access, - # the user should receive only DEVELOPER access. - def max_invited_level_for_users(group_link, user_ids) - invited_group = group_link.group - capped_access_level = group_link.group_access - access = invited_group.group_members.access_for_user_ids(user_ids) - - # If the user is not in the list, assume he/she does not have access - missing_users = user_ids - access.keys - missing_users.each { |id| access[id] = Gitlab::Access::NO_ACCESS } - - # Cap the maximum access by the invited level access - access.each { |key, value| access[key] = [value, capped_access_level].min } - end - def fetch_members(level = nil) - project_members = project.members - group_members = group ? group.members : [] - - if level - project_members = project_members.public_send(level) - group_members = group_members.public_send(level) if group - end - - user_ids = project_members.pluck(:user_id) - - invited_members = fetch_invited_members(level) - user_ids.push(*invited_members.map(&:user_id)) if invited_members.any? + members = project.authorized_users + members = members.where(project_authorizations: { access_level: level }) if level - user_ids.push(*group_members.pluck(:user_id)) if group - - User.where(id: user_ids) + members end def group project.group end - - def merge_max!(first_hash, second_hash) - first_hash.merge!(second_hash) { |_key, old, new| old > new ? old : new } - end - - def project_shared_with_group? - project.invited_groups.any? && project.allowed_to_share_with_group? - end - - def fetch_invited_members(level = nil) - invited_members = [] - - return invited_members unless project_shared_with_group? - - project.project_group_links.includes(group: [:group_members]).each do |link| - invited_group_members = link.group.members - - if level - numeric_level = GroupMember.access_level_roles[level.to_s.singularize.titleize] - - # If we're asked for a level that's higher than the group's access, - # there's nothing left to do - next if numeric_level > link.group_access - - # Make sure we include everyone _above_ the requested level as well - invited_group_members = - if numeric_level == link.group_access - invited_group_members.where("access_level >= ?", link.group_access) - else - invited_group_members.public_send(level) - end - end - - invited_members << invited_group_members - end - - invited_members.flatten.compact - end end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 46f70da2452..9db96347322 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -127,7 +127,7 @@ class ProjectWiki end def search_files(query) - repository.search_files(query, default_branch) + repository.search_files_by_content(query, default_branch) end def repository diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index 806b3ccd275..771e3376613 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -1,9 +1,6 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base include ProtectedBranchAccess - belongs_to :protected_branch - delegate :project, to: :protected_branch - validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER, Gitlab::Access::DEVELOPER] } @@ -13,10 +10,4 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base Gitlab::Access::DEVELOPER => "Developers + Masters" }.with_indifferent_access end - - def check_access(user) - return true if user.is_admin? - - project.team.max_member_access(user.id) >= access_level - end end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index 92e9c51d883..14610cb42b7 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -1,9 +1,6 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base include ProtectedBranchAccess - belongs_to :protected_branch - delegate :project, to: :protected_branch - validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS] } @@ -18,8 +15,7 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base def check_access(user) return false if access_level == Gitlab::Access::NO_ACCESS - return true if user.is_admin? - project.team.max_member_access(user.id) >= access_level + super end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 7d06ce1e85b..43dba86e5ed 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1,28 +1,56 @@ require 'securerandom' class Repository - class CommitError < StandardError; end - - # Files to use as a project avatar in case no avatar was uploaded via the web - # UI. - AVATAR_FILES = %w{logo.png logo.jpg logo.gif} - include Gitlab::ShellAdapter attr_accessor :path_with_namespace, :project - def self.storages - Gitlab.config.repositories.storages - end + class CommitError < StandardError; end - def self.remove_storage_from_path(repo_path) - storages.find do |_, storage_path| - if repo_path.start_with?(storage_path) - return repo_path.sub(storage_path, '') - end + # Methods that cache data from the Git repository. + # + # Each entry in this Array should have a corresponding method with the exact + # same name. The cache key used by those methods must also match method's + # name. + # + # For example, for entry `:readme` there's a method called `readme` which + # stores its data in the `readme` cache key. + CACHED_METHODS = %i(size commit_count readme version contribution_guide + changelog license_blob license_key gitignore koding_yml + gitlab_ci_yml branch_names tag_names branch_count + tag_count avatar exists? empty? root_ref) + + # Certain method caches should be refreshed when certain types of files are + # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to + # the corresponding methods to call for refreshing caches. + METHOD_CACHES_FOR_FILE_TYPES = { + readme: :readme, + changelog: :changelog, + license: %i(license_blob license_key), + contributing: :contribution_guide, + version: :version, + gitignore: :gitignore, + koding: :koding_yml, + gitlab_ci: :gitlab_ci_yml, + avatar: :avatar + } + + # Wraps around the given method and caches its output in Redis and an instance + # variable. + # + # This only works for methods that do not take any arguments. + def self.cache_method(name, fallback: nil) + original = :"_uncached_#{name}" + + alias_method(original, name) + + define_method(name) do + cache_method_output(name, fallback: fallback) { __send__(original) } end + end - repo_path + def self.storages + Gitlab.config.repositories.storages end def initialize(path_with_namespace, project) @@ -47,24 +75,6 @@ class Repository ) end - def exists? - return @exists unless @exists.nil? - - @exists = cache.fetch(:exists?) do - begin - raw_repository && raw_repository.rugged ? true : false - rescue Gitlab::Git::Repository::NoRepository - false - end - end - end - - def empty? - return @empty unless @empty.nil? - - @empty = cache.fetch(:empty?) { raw_repository.empty? } - end - # # Git repository can contains some hidden refs like: # /refs/notes/* @@ -75,24 +85,22 @@ class Repository # This method return true if repository contains some content visible in project page. # def has_visible_content? - return @has_visible_content unless @has_visible_content.nil? - - @has_visible_content = cache.fetch(:has_visible_content?) do - branch_count > 0 - end + branch_count > 0 end def commit(ref = 'HEAD') return nil unless exists? + commit = if ref.is_a?(Gitlab::Git::Commit) ref else Gitlab::Git::Commit.find(raw_repository, ref) end + commit = ::Commit.new(commit, @project) if commit commit - rescue Rugged::OdbError + rescue Rugged::OdbError, Rugged::TreeError nil end @@ -184,8 +192,9 @@ class Repository options = { message: message, tagger: user_to_committer(user) } if message - GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do - rugged.tags.create(tag_name, target, options) + GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do |service| + raw_tag = rugged.tags.create(tag_name, target, options) + service.newrev = raw_tag.target_id end find_tag(tag_name) @@ -222,16 +231,14 @@ class Repository branch_names + tag_names end - def branch_names - @branch_names ||= cache.fetch(:branch_names) { branches.map(&:name) } - end - def branch_exists?(branch_name) branch_names.include?(branch_name) end def ref_exists?(ref) rugged.references.exist?(ref) + rescue Rugged::ReferenceError + false end def update_ref!(name, newrev, oldrev) @@ -239,7 +246,7 @@ class Repository # offer 'compare and swap' ref updates. Without compare-and-swap we can # (and have!) accidentally reset the ref to an earlier state, clobbering # commits. See also https://github.com/libgit2/libgit2/issues/1534. - command = %w[git update-ref --stdin -z] + command = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z) _, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin| stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00") end @@ -270,39 +277,7 @@ class Repository end def kept_around?(sha) - begin - ref_exists?(keep_around_ref_name(sha)) - rescue Rugged::ReferenceError - false - end - end - - def tag_names - cache.fetch(:tag_names) { raw_repository.tag_names } - end - - def commit_count - cache.fetch(:commit_count) do - begin - raw_repository.commit_count(self.root_ref) - rescue - 0 - end - end - end - - def branch_count - @branch_count ||= cache.fetch(:branch_count) { branches.size } - end - - def tag_count - @tag_count ||= cache.fetch(:tag_count) { raw_repository.rugged.tags.count } - end - - # Return repo size in megabytes - # Cached in redis - def size - cache.fetch(:size) { raw_repository.size } + ref_exists?(keep_around_ref_name(sha)) end def diverging_commit_counts(branch) @@ -320,48 +295,55 @@ class Repository end end - # Keys for data that can be affected for any commit push. - def cache_keys - %i(size commit_count - readme version contribution_guide changelog - license_blob license_key gitignore koding_yml) + def expire_tags_cache + expire_method_caches(%i(tag_names tag_count)) + @tags = nil end - # Keys for data on branch/tag operations. - def cache_keys_for_branches_and_tags - %i(branch_names tag_names branch_count tag_count) + def expire_branches_cache + expire_method_caches(%i(branch_names branch_count)) + @local_branches = nil end - def build_cache - (cache_keys + cache_keys_for_branches_and_tags).each do |key| - unless cache.exist?(key) - send(key) - end - end + def expire_statistics_caches + expire_method_caches(%i(size commit_count)) end - def expire_tags_cache - cache.expire(:tag_names) - @tags = nil + def expire_all_method_caches + expire_method_caches(CACHED_METHODS) end - def expire_branches_cache - cache.expire(:branch_names) - @branch_names = nil - @local_branches = nil + # Expires the caches of a specific set of methods + def expire_method_caches(methods) + methods.each do |key| + cache.expire(key) + + ivar = cache_instance_variable_name(key) + + remove_instance_variable(ivar) if instance_variable_defined?(ivar) + end end - def expire_cache(branch_name = nil, revision = nil) - cache_keys.each do |key| - cache.expire(key) + def expire_avatar_cache + expire_method_caches(%i(avatar)) + end + + # Refreshes the method caches of this repository. + # + # types - An Array of file types (e.g. `:readme`) used to refresh extra + # caches. + def refresh_method_caches(types) + to_refresh = [] + + types.each do |type| + methods = METHOD_CACHES_FOR_FILE_TYPES[type.to_sym] + + to_refresh.concat(Array(methods)) if methods end - expire_branch_cache(branch_name) - expire_avatar_cache(branch_name, revision) + expire_method_caches(to_refresh) - # This ensures this particular cache is flushed after the first commit to a - # new repository. - expire_emptiness_caches if empty? + to_refresh.each { |method| send(method) } end def expire_branch_cache(branch_name = nil) @@ -380,68 +362,32 @@ class Repository end def expire_root_ref_cache - cache.expire(:root_ref) - @root_ref = nil + expire_method_caches(%i(root_ref)) end # Expires the cache(s) used to determine if a repository is empty or not. def expire_emptiness_caches - cache.expire(:empty?) - @empty = nil - - expire_has_visible_content_cache - end + return unless empty? - def expire_has_visible_content_cache - cache.expire(:has_visible_content?) - @has_visible_content = nil - end - - def expire_branch_count_cache - cache.expire(:branch_count) - @branch_count = nil - end - - def expire_tag_count_cache - cache.expire(:tag_count) - @tag_count = nil + expire_method_caches(%i(empty?)) end def lookup_cache @lookup_cache ||= {} end - def expire_avatar_cache(branch_name = nil, revision = nil) - # Avatars are pulled from the default branch, thus if somebody pushes to a - # different branch there's no need to expire anything. - return if branch_name && branch_name != root_ref - - # We don't want to flush the cache if the commit didn't actually make any - # changes to any of the possible avatar files. - if revision && commit = self.commit(revision) - return unless commit.raw_diffs(deltas_only: true). - any? { |diff| AVATAR_FILES.include?(diff.new_path) } - end - - cache.expire(:avatar) - - @avatar = nil - end - def expire_exists_cache - cache.expire(:exists?) - @exists = nil + expire_method_caches(%i(exists?)) end # expire cache that doesn't depend on repository data (when expiring) def expire_content_cache expire_tags_cache - expire_tag_count_cache expire_branches_cache - expire_branch_count_cache expire_root_ref_cache expire_emptiness_caches expire_exists_cache + expire_statistics_caches end # Runs code after a repository has been created. @@ -456,9 +402,8 @@ class Repository # Runs code just before a repository is deleted. def before_delete expire_exists_cache - - expire_cache if exists? - + expire_all_method_caches + expire_branch_cache if exists? expire_content_cache repository_event(:remove_repository) @@ -475,9 +420,9 @@ class Repository # Runs code before pushing (= creating or removing) a tag. def before_push_tag - expire_cache + expire_statistics_caches + expire_emptiness_caches expire_tags_cache - expire_tag_count_cache repository_event(:push_tag) end @@ -485,7 +430,7 @@ class Repository # Runs code before removing a tag. def before_remove_tag expire_tags_cache - expire_tag_count_cache + expire_statistics_caches repository_event(:remove_tag) end @@ -494,15 +439,22 @@ class Repository expire_content_cache end + # Runs code after the HEAD of a repository is changed. + def after_change_head + expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys) + end + # Runs code after a repository has been forked/imported. def after_import expire_content_cache - build_cache + expire_tags_cache + expire_branches_cache end # Runs code after a new commit has been pushed. - def after_push_commit(branch_name, revision) - expire_cache(branch_name, revision) + def after_push_commit(branch_name) + expire_statistics_caches + expire_branch_cache(branch_name) repository_event(:push_commit, branch: branch_name) end @@ -510,8 +462,6 @@ class Repository # Runs code after a new branch has been created. def after_create_branch expire_branches_cache - expire_has_visible_content_cache - expire_branch_count_cache repository_event(:push_branch) end @@ -525,8 +475,6 @@ class Repository # Runs code after an existing branch has been removed. def after_remove_branch - expire_has_visible_content_cache - expire_branch_count_cache expire_branches_cache end @@ -553,86 +501,127 @@ class Repository Gitlab::Git::Blob.raw(self, oid) end + def root_ref + if raw_repository + raw_repository.root_ref + else + # When the repo does not exist we raise this error so no data is cached. + raise Rugged::ReferenceError + end + end + cache_method :root_ref + + def exists? + refs_directory_exists? + end + cache_method :exists? + + def empty? + raw_repository.empty? + end + cache_method :empty? + + # The size of this repository in megabytes. + def size + exists? ? raw_repository.size : 0.0 + end + cache_method :size, fallback: 0.0 + + def commit_count + root_ref ? raw_repository.commit_count(root_ref) : 0 + end + cache_method :commit_count, fallback: 0 + + def branch_names + branches.map(&:name) + end + cache_method :branch_names, fallback: [] + + def tag_names + raw_repository.tag_names + end + cache_method :tag_names, fallback: [] + + def branch_count + branches.size + end + cache_method :branch_count, fallback: 0 + + def tag_count + raw_repository.rugged.tags.count + end + cache_method :tag_count, fallback: 0 + + def avatar + if tree = file_on_head(:avatar) + tree.path + end + end + cache_method :avatar + def readme - cache.fetch(:readme) { tree(:head).readme } + if head = tree(:head) + head.readme + end end + cache_method :readme def version - cache.fetch(:version) do - tree(:head).blobs.find do |file| - file.name.casecmp('version').zero? - end - end + file_on_head(:version) end + cache_method :version def contribution_guide - cache.fetch(:contribution_guide) do - tree(:head).blobs.find do |file| - file.contributing? - end - end + file_on_head(:contributing) end + cache_method :contribution_guide def changelog - cache.fetch(:changelog) do - file_on_head(/\A(changelog|history|changes|news)/i) - end + file_on_head(:changelog) end + cache_method :changelog def license_blob - return nil unless head_exists? - - cache.fetch(:license_blob) do - file_on_head(/\A(licen[sc]e|copying)(\..+|\z)/i) - end + file_on_head(:license) end + cache_method :license_blob def license_key - return nil unless head_exists? + return unless exists? - cache.fetch(:license_key) do - Licensee.license(path).try(:key) - end + Licensee.license(path).try(:key) end + cache_method :license_key def gitignore - return nil if !exists? || empty? - - cache.fetch(:gitignore) do - file_on_head(/\A\.gitignore\z/) - end + file_on_head(:gitignore) end + cache_method :gitignore def koding_yml - return nil unless head_exists? - - cache.fetch(:koding_yml) do - file_on_head(/\A\.koding\.yml\z/) - end + file_on_head(:koding) end + cache_method :koding_yml def gitlab_ci_yml - return nil unless head_exists? - - @gitlab_ci_yml ||= tree(:head).blobs.find do |file| - file.name == '.gitlab-ci.yml' - end - rescue Rugged::ReferenceError - # For unknow reason spinach scenario "Scenario: I change project path" - # lead to "Reference 'HEAD' not found" exception from Repository#empty? - nil + file_on_head(:gitlab_ci) end + cache_method :gitlab_ci_yml def head_commit @head_commit ||= commit(self.root_ref) end def head_tree - @head_tree ||= Tree.new(self, head_commit.sha, nil) + if head_commit + @head_tree ||= Tree.new(self, head_commit.sha, nil) + end end - def tree(sha = :head, path = nil) + def tree(sha = :head, path = nil, recursive: false) if sha == :head + return unless head_commit + if path.nil? return head_tree else @@ -640,7 +629,7 @@ class Repository end end - Tree.new(self, sha, path) + Tree.new(self, sha, path, recursive: recursive) end def blob_at_branch(branch_name, path) @@ -670,11 +659,19 @@ class Repository end def last_commit_for_path(sha, path) - args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path}) - sha = Gitlab::Popen.popen(args, path_to_repo).first.strip + sha = last_commit_id_for_path(sha, path) commit(sha) end + def last_commit_id_for_path(sha, path) + key = path.blank? ? "last_commit_id_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}" + + cache.fetch(key) do + args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path}) + Gitlab::Popen.popen(args, path_to_repo).first.strip + end + end + def next_branch(name, opts = {}) branch_ids = self.branch_names.map do |n| next 1 if n == name @@ -782,10 +779,6 @@ class Repository @tags ||= raw_repository.tags end - def root_ref - @root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref } - end - def commit_dir(user, path, message, branch, author_email: nil, author_name: nil) update_branch_with_hooks(user, branch) do |ref| options = { @@ -970,7 +963,7 @@ class Repository update_branch_with_hooks(user, base_branch) do committer = user_to_committer(user) source_sha = Rugged::Commit.create(rugged, - message: commit.revert_message, + message: commit.revert_message(user), author: committer, committer: committer, tree: revert_tree_id, @@ -1063,16 +1056,25 @@ class Repository merge_base(ancestor_id, descendant_id) == ancestor_id end - def search_files(query, ref) - unless exists? && has_visible_content? && query.present? - return [] - end + def empty_repo? + !exists? || !has_visible_content? + end + + def search_files_by_content(query, ref) + return [] if empty_repo? || query.blank? offset = 2 args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/) end + def search_files_by_name(query, ref) + return [] if empty_repo? || query.blank? + + args = %W(#{Gitlab.config.git.bin_path} ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)}) + Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip) + end + def fetch_ref(source_path, source_ref, target_ref) args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) Gitlab::Popen.popen(args, path_to_repo) @@ -1134,28 +1136,55 @@ class Repository end end - def avatar - return nil unless exists? + # Caches the supplied block both in a cache and in an instance variable. + # + # The cache key and instance variable are named the same way as the value of + # the `key` argument. + # + # This method will return `nil` if the corresponding instance variable is also + # set to `nil`. This ensures we don't keep yielding the block when it returns + # `nil`. + # + # key - The name of the key to cache the data in. + # fallback - A value to fall back to in the event of a Git error. + def cache_method_output(key, fallback: nil, &block) + ivar = cache_instance_variable_name(key) - @avatar ||= cache.fetch(:avatar) do - AVATAR_FILES.find do |file| - blob_at_branch(root_ref, file) + if instance_variable_defined?(ivar) + instance_variable_get(ivar) + else + begin + instance_variable_set(ivar, cache.fetch(key, &block)) + rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository + # if e.g. HEAD or the entire repository doesn't exist we want to + # gracefully handle this and not cache anything. + fallback end end end - private + def cache_instance_variable_name(key) + :"@#{key.to_s.tr('?!', '')}" + end - def cache - @cache ||= RepositoryCache.new(path_with_namespace, @project.id) + def file_on_head(type) + if head = tree(:head) + head.blobs.find do |file| + Gitlab::FileDetector.type_of(file.name) == type + end + end end - def head_exists? - exists? && !empty? && !rugged.head_unborn? + private + + def refs_directory_exists? + return false unless path_with_namespace + + File.exist?(File.join(path_to_repo, 'refs')) end - def file_on_head(regex) - tree(:head).blobs.find { |file| file.name =~ regex } + def cache + @cache ||= RepositoryCache.new(path_with_namespace, @project.id) end def tags_sorted_by_committed_date diff --git a/app/models/route.rb b/app/models/route.rb new file mode 100644 index 00000000000..caf596efa79 --- /dev/null +++ b/app/models/route.rb @@ -0,0 +1,22 @@ +class Route < ActiveRecord::Base + belongs_to :source, polymorphic: true + + validates :source, presence: true + + validates :path, + length: { within: 1..255 }, + presence: true, + uniqueness: { case_sensitive: false } + + after_update :rename_children, if: :path_changed? + + def rename_children + # We update each row separately because MySQL does not have regexp_replace. + # rubocop:disable Rails/FindEach + Route.where('path LIKE ?', "#{path_was}/%").each do |route| + # Note that update column skips validation and callbacks. + # We need this to avoid recursive call of rename_children method + route.update_column(:path, route.path.sub(path_was, path)) + end + end +end diff --git a/app/models/service.rb b/app/models/service.rb index 625fbc48302..043be222f3a 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -8,6 +8,7 @@ class Service < ActiveRecord::Base default_value_for :push_events, true default_value_for :issues_events, true default_value_for :confidential_issues_events, true + default_value_for :commit_events, true default_value_for :merge_requests_events, true default_value_for :tag_push_events, true default_value_for :note_events, true @@ -75,6 +76,11 @@ class Service < ActiveRecord::Base def to_param # implement inside child + self.class.to_param + end + + def self.to_param + raise NotImplementedError end def fields @@ -91,7 +97,11 @@ class Service < ActiveRecord::Base end def event_names - supported_events.map { |event| "#{event}_events" } + self.class.event_names + end + + def self.event_names + self.supported_events.map { |event| "#{event}_events" } end def event_field(event) @@ -103,6 +113,10 @@ class Service < ActiveRecord::Base end def supported_events + self.class.supported_events + end + + def self.supported_events %w(push tag_push issue confidential_issue merge_request wiki_page) end @@ -202,7 +216,6 @@ class Service < ActiveRecord::Base bamboo buildkite builds_email - pipelines_email bugzilla campfire custom_issue_tracker @@ -214,19 +227,24 @@ class Service < ActiveRecord::Base hipchat irker jira + kubernetes + mattermost_slash_commands + mattermost + pipelines_email pivotaltracker pushover redmine + slack_slash_commands slack teamcity ] end - def self.create_from_template(project_id, template) + def self.build_from_template(project_id, template) service = template.dup service.template = false service.project_id = project_id - service if service.save + service end private diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 2373b445009..771a7350556 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -6,6 +6,7 @@ class Snippet < ActiveRecord::Base include Referable include Sortable include Awardable + include Mentionable cache_markdown_field :title, pipeline: :single_line cache_markdown_field :content @@ -26,9 +27,9 @@ class Snippet < ActiveRecord::Base delegate :name, :email, to: :author, prefix: true, allow_nil: true validates :author, presence: true - validates :title, presence: true, length: { within: 0..255 } + validates :title, presence: true, length: { maximum: 255 } validates :file_name, - length: { within: 0..255 }, + length: { maximum: 255 }, format: { with: Gitlab::Regex.file_name_regex, message: Gitlab::Regex.file_name_regex_message } @@ -63,14 +64,14 @@ class Snippet < ActiveRecord::Base @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/) end - def to_reference(from_project = nil) + def to_reference(from_project = nil, full: false) reference = "#{self.class.reference_prefix}#{id}" - if cross_project_reference?(from_project) - reference = project.to_reference + reference + if project.present? + "#{project.to_reference(from_project, full: full)}#{reference}" + else + reference end - - reference end def self.content_types @@ -93,6 +94,10 @@ class Snippet < ActiveRecord::Base 0 end + def file_name + super.to_s + end + # alias for compatibility with blobs and highlighting def path file_name diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 3b8aa1eb866..17869c8bac2 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -1,8 +1,9 @@ class Subscription < ActiveRecord::Base belongs_to :user + belongs_to :project belongs_to :subscribable, polymorphic: true - validates :user_id, - uniqueness: { scope: [:subscribable_id, :subscribable_type] }, - presence: true + validates :user, :subscribable, presence: true + + validates :project_id, uniqueness: { scope: [:subscribable_id, :subscribable_type, :user_id] } end diff --git a/app/models/timelog.rb b/app/models/timelog.rb new file mode 100644 index 00000000000..f768c4e3da5 --- /dev/null +++ b/app/models/timelog.rb @@ -0,0 +1,6 @@ +class Timelog < ActiveRecord::Base + validates :time_spent, :user, presence: true + + belongs_to :trackable, polymorphic: true + belongs_to :user +end diff --git a/app/models/todo.rb b/app/models/todo.rb index f5ade1cc293..4c99aa0d3be 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -6,13 +6,15 @@ class Todo < ActiveRecord::Base BUILD_FAILED = 3 MARKED = 4 APPROVAL_REQUIRED = 5 # This is an EE-only feature + UNMERGEABLE = 6 ACTION_NAMES = { ASSIGNED => :assigned, MENTIONED => :mentioned, BUILD_FAILED => :build_failed, MARKED => :marked, - APPROVAL_REQUIRED => :approval_required + APPROVAL_REQUIRED => :approval_required, + UNMERGEABLE => :unmergeable } belongs_to :author, class_name: "User" @@ -66,6 +68,10 @@ class Todo < ActiveRecord::Base end end + def unmergeable? + action == UNMERGEABLE + end + def build_failed? action == BUILD_FAILED end diff --git a/app/models/tree.rb b/app/models/tree.rb index 7c4ed6e393b..fe148b0ec65 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -3,21 +3,24 @@ class Tree attr_accessor :repository, :sha, :path, :entries - def initialize(repository, sha, path = '/') + def initialize(repository, sha, path = '/', recursive: false) path = '/' if path.blank? @repository = repository @sha = sha @path = path + @recursive = recursive git_repo = @repository.raw_repository - @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path) + @entries = get_entries(git_repo, @sha, @path, recursive: @recursive) end def readme return @readme if defined?(@readme) - available_readmes = blobs.select(&:readme?) + available_readmes = blobs.select do |blob| + Gitlab::FileDetector.type_of(blob.name) == :readme + end previewable_readmes = available_readmes.select do |blob| previewable?(blob.name) @@ -58,4 +61,21 @@ class Tree def sorted_entries trees + blobs + submodules end + + private + + def get_entries(git_repo, sha, path, recursive: false) + current_path_entries = Gitlab::Git::Tree.where(git_repo, sha, path) + ordered_entries = [] + + current_path_entries.each do |entry| + ordered_entries << entry + + if recursive && entry.dir? + ordered_entries.concat(get_entries(git_repo, sha, entry.path, recursive: true)) + end + end + + ordered_entries + end end diff --git a/app/models/user.rb b/app/models/user.rb index 3813df6684e..06dd98a3188 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -56,6 +56,7 @@ class User < ActiveRecord::Base has_many :personal_access_tokens, dependent: :destroy has_many :identities, dependent: :destroy, autosave: true has_many :u2f_registrations, dependent: :destroy + has_many :chat_names, dependent: :destroy # Groups has_many :members, dependent: :destroy @@ -72,6 +73,8 @@ class User < ActiveRecord::Base has_many :created_projects, foreign_key: :creator_id, class_name: 'Project' has_many :users_star_projects, dependent: :destroy has_many :starred_projects, through: :users_star_projects, source: :project + has_many :project_authorizations + has_many :authorized_projects, through: :project_authorizations, source: :project has_many :snippets, dependent: :destroy, foreign_key: :author_id has_many :issues, dependent: :destroy, foreign_key: :author_id @@ -96,6 +99,7 @@ class User < ActiveRecord::Base # # Note: devise :validatable above adds validations for :email and :password validates :name, presence: true + validates_confirmation_of :email validates :notification_email, presence: true validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email } validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true @@ -173,8 +177,10 @@ class User < ActiveRecord::Base scope :external, -> { where(external: true) } scope :active, -> { with_state(:active) } scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all } - scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') } + scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') } scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) } + scope :order_recent_sign_in, -> { reorder(last_sign_in_at: :desc) } + scope :order_oldest_sign_in, -> { reorder(last_sign_in_at: :asc) } def self.with_two_factor joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id"). @@ -202,8 +208,8 @@ class User < ActiveRecord::Base def sort(method) case method.to_s - when 'recent_sign_in' then reorder(last_sign_in_at: :desc) - when 'oldest_sign_in' then reorder(last_sign_in_at: :asc) + when 'recent_sign_in' then order_recent_sign_in + when 'oldest_sign_in' then order_oldest_sign_in else order_by(method) end @@ -226,19 +232,19 @@ class User < ActiveRecord::Base def filter(filter_name) case filter_name when 'admins' - self.admins + admins when 'blocked' - self.blocked + blocked when 'two_factor_disabled' - self.without_two_factor + without_two_factor when 'two_factor_enabled' - self.with_two_factor + with_two_factor when 'wop' - self.without_projects + without_projects when 'external' - self.external + external else - self.active + active end end @@ -288,8 +294,12 @@ class User < ActiveRecord::Base end end + def find_by_username(username) + iwhere(username: username).take + end + def find_by_username!(username) - find_by!('lower(username) = ?', username.downcase) + iwhere(username: username).take! end def find_by_personal_access_token(token_string) @@ -297,19 +307,11 @@ class User < ActiveRecord::Base personal_access_token.user if personal_access_token end - def by_username_or_id(name_or_id) - find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i) - end - # Returns a user for the given SSH key. def find_by_ssh_key_id(key_id) find_by(id: Key.unscoped.select(:user_id).where(id: key_id)) end - def build_user(attrs = {}) - User.new(attrs) - end - def reference_prefix '@' end @@ -331,12 +333,12 @@ class User < ActiveRecord::Base username end - def to_reference(_from_project = nil, _target_project = nil) + def to_reference(_from_project = nil, target_project: nil, full: nil) "#{self.class.reference_prefix}#{username}" end def generate_password - if self.force_random_password + if force_random_password self.password = self.password_confirmation = Devise.friendly_token.first(Devise.password_length.min) end end @@ -377,56 +379,55 @@ class User < ActiveRecord::Base end def two_factor_otp_enabled? - self.otp_required_for_login? + otp_required_for_login? end def two_factor_u2f_enabled? - self.u2f_registrations.exists? + u2f_registrations.exists? end def namespace_uniq # Return early if username already failed the first uniqueness validation - return if self.errors.key?(:username) && - self.errors[:username].include?('has already been taken') + return if errors.key?(:username) && + errors[:username].include?('has already been taken') - namespace_name = self.username - existing_namespace = Namespace.by_path(namespace_name) - if existing_namespace && existing_namespace != self.namespace - self.errors.add(:username, 'has already been taken') + existing_namespace = Namespace.by_path(username) + if existing_namespace && existing_namespace != namespace + errors.add(:username, 'has already been taken') end end def avatar_type - unless self.avatar.image? - self.errors.add :avatar, "only images allowed" + unless avatar.image? + errors.add :avatar, "only images allowed" end end def unique_email - if !self.emails.exists?(email: self.email) && Email.exists?(email: self.email) - self.errors.add(:email, 'has already been taken') + if !emails.exists?(email: email) && Email.exists?(email: email) + errors.add(:email, 'has already been taken') end end def owns_notification_email - return if self.temp_oauth_email? + return if temp_oauth_email? - self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email) + errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email) end def owns_public_email - return if self.public_email.blank? + return if public_email.blank? - self.errors.add(:public_email, "is not an email you own") unless self.all_emails.include?(self.public_email) + errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email) end def update_emails_with_primary_email - primary_email_record = self.emails.find_by(email: self.email) + primary_email_record = emails.find_by(email: email) if primary_email_record primary_email_record.destroy - self.emails.create(email: self.email_was) + emails.create(email: email_was) - self.update_secondary_emails! + update_secondary_emails! end end @@ -438,11 +439,32 @@ class User < ActiveRecord::Base Group.where("namespaces.id IN (#{union.to_sql})") end - # Returns projects user is authorized to access. - # - # If you change the logic of this method, please also update `Project#authorized_for_user` + def refresh_authorized_projects + Users::RefreshAuthorizedProjectsService.new(self).execute + end + + def remove_project_authorizations(project_ids) + project_authorizations.where(project_id: project_ids).delete_all + end + + def set_authorized_projects_column + unless authorized_projects_populated + update_column(:authorized_projects_populated, true) + end + end + def authorized_projects(min_access_level = nil) - Project.where("projects.id IN (#{projects_union(min_access_level).to_sql})") + refresh_authorized_projects unless authorized_projects_populated + + # We're overriding an association, so explicitly call super with no arguments or it would be passed as `force_reload` to the association + projects = super() + projects = projects.where('project_authorizations.access_level >= ?', min_access_level) if min_access_level + + projects + end + + def authorized_project?(project, min_access_level = nil) + authorized_projects(min_access_level).exists?({ id: project.id }) end # Returns the projects this user has reporter (or greater) access to, limited @@ -456,8 +478,9 @@ class User < ActiveRecord::Base end def viewable_starred_projects - starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (#{projects_union.to_sql})", - [Project::PUBLIC, Project::INTERNAL]) + starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (?)", + [Project::PUBLIC, Project::INTERNAL], + authorized_projects.select(:project_id)) end def owned_projects @@ -478,7 +501,7 @@ class User < ActiveRecord::Base end def require_ssh_key? - keys.count == 0 + keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh') end def require_password? @@ -580,7 +603,7 @@ class User < ActiveRecord::Base end def project_deploy_keys - DeployKey.unscoped.in_projects(self.authorized_projects.pluck(:id)).distinct(:id) + DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id) end def accessible_deploy_keys @@ -596,38 +619,38 @@ class User < ActiveRecord::Base end def sanitize_attrs - %w(name username skype linkedin twitter).each do |attr| - value = self.send(attr) - self.send("#{attr}=", Sanitize.clean(value)) if value.present? + %w[name username skype linkedin twitter].each do |attr| + value = public_send(attr) + public_send("#{attr}=", Sanitize.clean(value)) if value.present? end end def set_notification_email - if self.notification_email.blank? || !self.all_emails.include?(self.notification_email) - self.notification_email = self.email + if notification_email.blank? || !all_emails.include?(notification_email) + self.notification_email = email end end def set_public_email - if self.public_email.blank? || !self.all_emails.include?(self.public_email) + if public_email.blank? || !all_emails.include?(public_email) self.public_email = '' end end def update_secondary_emails! - self.set_notification_email - self.set_public_email - self.save if self.notification_email_changed? || self.public_email_changed? + set_notification_email + set_public_email + save if notification_email_changed? || public_email_changed? end def set_projects_limit # `User.select(:id)` raises # `ActiveModel::MissingAttributeError: missing attribute: projects_limit` # without this safeguard! - return unless self.has_attribute?(:projects_limit) + return unless has_attribute?(:projects_limit) connection_default_value_defined = new_record? && !projects_limit_changed? - return unless self.projects_limit.nil? || connection_default_value_defined + return unless projects_limit.nil? || connection_default_value_defined self.projects_limit = current_application_settings.default_projects_limit end @@ -657,7 +680,7 @@ class User < ActiveRecord::Base def with_defaults User.defaults.each do |k, v| - self.send("#{k}=", v) + public_send("#{k}=", v) end self @@ -668,20 +691,6 @@ class User < ActiveRecord::Base project.project_member(self) end - # Reset project events cache related to this user - # - # Since we do cache @event we need to reset cache in special cases: - # * when the user changes their avatar - # Events cache stored like events/23-20130109142513. - # The cache key includes updated_at timestamp. - # Thus it will automatically generate a new fragment - # when the event is updated because the key changes. - def reset_events_cache - Event.where(author_id: self.id). - order('id DESC').limit(1000). - update_all(updated_at: Time.now) - end - def full_website_url return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\// @@ -710,8 +719,8 @@ class User < ActiveRecord::Base def all_emails all_emails = [] - all_emails << self.email unless self.temp_oauth_email? - all_emails.concat(self.emails.map(&:email)) + all_emails << email unless temp_oauth_email? + all_emails.concat(emails.map(&:email)) all_emails end @@ -725,21 +734,21 @@ class User < ActiveRecord::Base def ensure_namespace_correct # Ensure user has namespace - self.create_namespace!(path: self.username, name: self.username) unless self.namespace + create_namespace!(path: username, name: username) unless namespace - if self.username_changed? - self.namespace.update_attributes(path: self.username, name: self.username) + if username_changed? + namespace.update_attributes(path: username, name: username) end end def post_create_hook - log_info("User \"#{self.name}\" (#{self.email}) was created") - notification_service.new_user(self, @reset_token) if self.created_by_id + log_info("User \"#{name}\" (#{email}) was created") + notification_service.new_user(self, @reset_token) if created_by_id system_hook_service.execute_hooks_for(self, :create) end def post_destroy_hook - log_info("User \"#{self.name}\" (#{self.email}) was removed") + log_info("User \"#{name}\" (#{email}) was removed") system_hook_service.execute_hooks_for(self, :destroy) end @@ -783,7 +792,7 @@ class User < ActiveRecord::Base end def oauth_authorized_tokens - Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil) + Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil) end # Returns the projects a user contributed to in the last year. @@ -887,20 +896,6 @@ class User < ActiveRecord::Base private - def projects_union(min_access_level = nil) - relations = [personal_projects.select(:id), - groups_projects.select(:id), - projects.select(:id), - groups.joins(:shared_projects).select(:project_id)] - - if min_access_level - scope = { access_level: Gitlab::Access.all_values.select { |access| access >= min_access_level } } - relations = [relations.shift] + relations.map { |relation| relation.where(members: scope) } - end - - Gitlab::SQL::Union.new(relations) - end - def ci_projects_union scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] } groups = groups_projects.where(members: scope) @@ -916,7 +911,7 @@ class User < ActiveRecord::Base end def ensure_external_user_rights - return unless self.external? + return unless external? self.can_create_group = false self.projects_limit = 0 @@ -928,7 +923,7 @@ class User < ActiveRecord::Base if current_application_settings.domain_blacklist_enabled? blocked_domains = current_application_settings.domain_blacklist - if domain_matches?(blocked_domains, self.email) + if domain_matches?(blocked_domains, email) error = 'is not from an allowed domain.' valid = false end @@ -936,7 +931,7 @@ class User < ActiveRecord::Base allowed_domains = current_application_settings.domain_whitelist unless allowed_domains.blank? - if domain_matches?(allowed_domains, self.email) + if domain_matches?(allowed_domains, email) valid = true else error = "domain is not authorized for sign-up" @@ -944,7 +939,7 @@ class User < ActiveRecord::Base end end - self.errors.add(:email, error) unless valid + errors.add(:email, error) unless valid valid 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/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 8b25332b73c..7b1752df0e1 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -1,6 +1,8 @@ module Ci class BuildPolicy < CommitStatusPolicy def rules + can! :read_build if @subject.project.public_builds? + super # If we can't read build we should also not have that diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb index 62335527654..5a3fe814b77 100644 --- a/app/policies/group_member_policy.rb +++ b/app/policies/group_member_policy.rb @@ -15,5 +15,11 @@ class GroupMemberPolicy < BasePolicy elsif @user == target_user can! :destroy_group_member end + + additional_rules! + end + + def additional_rules! + # This is meant to be overriden in EE end end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index b65fb68cd88..0be6e113655 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -4,7 +4,7 @@ class GroupPolicy < BasePolicy return unless @user globally_viewable = @subject.public? || (@subject.internal? && !@user.external?) - member = @subject.users.include?(@user) + member = @subject.users_with_parents.include?(@user) owner = @user.admin? || @subject.has_owner?(@user) master = owner || @subject.has_master?(@user) @@ -33,6 +33,8 @@ class GroupPolicy < BasePolicy if globally_viewable && @subject.request_access_enabled && !member can! :request_access end + + additional_rules!(master) end def can_read_group? @@ -43,4 +45,8 @@ class GroupPolicy < BasePolicy GroupProjectsFinder.new(@subject).execute(@user).any? end + + def additional_rules!(master) + # This is meant to be overriden in EE + end end diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb index 83847466ee2..5326061bd07 100644 --- a/app/policies/note_policy.rb +++ b/app/policies/note_policy.rb @@ -12,7 +12,7 @@ class NotePolicy < BasePolicy end if @subject.for_merge_request? && - @subject.noteable.author == @user + @subject.noteable.author == @user can! :resolve_note end end diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb index 46c5aa1a5be..d3913986cd8 100644 --- a/app/policies/personal_snippet_policy.rb +++ b/app/policies/personal_snippet_policy.rb @@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy if @subject.author == @user can! :read_personal_snippet can! :update_personal_snippet + can! :destroy_personal_snippet can! :admin_personal_snippet end + unless @user.external? + can! :create_personal_snippet + end + if @subject.internal? && !@user.external? can! :read_personal_snippet end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 1ee31023e26..71ef8901932 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -3,7 +3,7 @@ class ProjectPolicy < BasePolicy team_access!(user) owner = project.owner == user || - (project.group && project.group.has_owner?(user)) + (project.group && project.group.has_owner?(user)) owner_access! if user.admin? || owner team_member_owner_access! if owner @@ -12,11 +12,8 @@ class ProjectPolicy < BasePolicy guest_access! public_access! - # Allow to read builds for internal projects - can! :read_build if project.public_builds? - if project.request_access_enabled && - !(owner || user.admin? || project.team.member?(user) || project_group_member?(user)) + !(owner || user.admin? || project.team.member?(user) || project_group_member?(user)) can! :request_access end end @@ -46,10 +43,16 @@ class ProjectPolicy < BasePolicy can! :create_note can! :upload_file can! :read_cycle_analytics + + if project.public_builds? + can! :read_pipeline + can! :read_build + end end def reporter_access! can! :download_code + can! :download_wiki_code can! :fork_project can! :create_project_snippet can! :update_issue @@ -168,9 +171,7 @@ class ProjectPolicy < BasePolicy def disabled_features! repository_enabled = project.feature_available?(:repository, user) - unless project.feature_available?(:issues, user) - cannot!(*named_abilities(:issue)) - end + block_issues_abilities unless project.feature_available?(:merge_requests, user) && repository_enabled cannot!(*named_abilities(:merge_request)) @@ -187,6 +188,7 @@ class ProjectPolicy < BasePolicy unless project.feature_available?(:wiki, user) || project.has_external_wiki? cannot!(*named_abilities(:wiki)) + cannot!(:download_wiki_code) end unless project.feature_available?(:builds, user) && repository_enabled @@ -226,6 +228,7 @@ class ProjectPolicy < BasePolicy can! :read_commit_status can! :read_container_image can! :download_code + can! :download_wiki_code can! :read_cycle_analytics # NOTE: may be overridden by IssuePolicy @@ -239,10 +242,19 @@ class ProjectPolicy < BasePolicy def project_group_member?(user) project.group && - ( - project.group.members.exists?(user_id: user.id) || - project.group.requesters.exists?(user_id: user.id) - ) + ( + project.group.members_with_parents.exists?(user_id: user.id) || + project.group.requesters.exists?(user_id: user.id) + ) + end + + def block_issues_abilities + unless project.feature_available?(:issues, user) + cannot! :read_issue if project.default_issues_tracker? + cannot! :create_issue + cannot! :update_issue + cannot! :admin_issue + end end def named_abilities(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_build_entity.rb b/app/serializers/analytics_build_entity.rb new file mode 100644 index 00000000000..a0db5b8f0f4 --- /dev/null +++ b/app/serializers/analytics_build_entity.rb @@ -0,0 +1,40 @@ +class AnalyticsBuildEntity < Grape::Entity + include RequestAwareEntity + include EntityDateHelper + + expose :name + expose :id + expose :ref, as: :branch + expose :short_sha + expose :author, using: UserEntity + + expose :started_at, as: :date do |build| + interval_in_words(build[:started_at]) + end + + expose :duration, as: :total_time do |build| + build.duration ? distance_of_time_as_hash(build.duration.to_f) : {} + end + + expose :branch do + expose :ref, as: :name + + expose :url do |build| + url_to(:namespace_project_tree, build, build.ref) + end + end + + expose :url do |build| + url_to(:namespace_project_build, build) + end + + expose :commit_url do |build| + url_to(:namespace_project_commit, build, build.sha) + end + + private + + def url_to(route, build, id = nil) + public_send("#{route}_url", build.project.namespace, build.project, id || build) + end +end diff --git a/app/serializers/analytics_build_serializer.rb b/app/serializers/analytics_build_serializer.rb new file mode 100644 index 00000000000..f172d67d356 --- /dev/null +++ b/app/serializers/analytics_build_serializer.rb @@ -0,0 +1,3 @@ +class AnalyticsBuildSerializer < BaseSerializer + entity AnalyticsBuildEntity +end diff --git a/app/serializers/analytics_commit_entity.rb b/app/serializers/analytics_commit_entity.rb new file mode 100644 index 00000000000..402cecbfd08 --- /dev/null +++ b/app/serializers/analytics_commit_entity.rb @@ -0,0 +1,13 @@ +class AnalyticsCommitEntity < CommitEntity + include EntityDateHelper + + expose :short_id, as: :short_sha + + expose :total_time do |commit| + distance_of_time_as_hash(request.total_time.to_f) + end + + unexpose :author_name + unexpose :author_email + unexpose :message +end diff --git a/app/serializers/analytics_commit_serializer.rb b/app/serializers/analytics_commit_serializer.rb new file mode 100644 index 00000000000..cdbfecf2b70 --- /dev/null +++ b/app/serializers/analytics_commit_serializer.rb @@ -0,0 +1,3 @@ +class AnalyticsCommitSerializer < BaseSerializer + entity AnalyticsCommitEntity +end diff --git a/app/serializers/analytics_generic_serializer.rb b/app/serializers/analytics_generic_serializer.rb new file mode 100644 index 00000000000..9f4859e8410 --- /dev/null +++ b/app/serializers/analytics_generic_serializer.rb @@ -0,0 +1,7 @@ +class AnalyticsGenericSerializer < BaseSerializer + def represent(resource, opts = {}) + resource.symbolize_keys! + + super(resource, opts) + end +end diff --git a/app/serializers/analytics_issue_entity.rb b/app/serializers/analytics_issue_entity.rb new file mode 100644 index 00000000000..44c50f18613 --- /dev/null +++ b/app/serializers/analytics_issue_entity.rb @@ -0,0 +1,29 @@ +class AnalyticsIssueEntity < Grape::Entity + include RequestAwareEntity + include EntityDateHelper + + expose :title + expose :author, using: UserEntity + + expose :iid do |object| + object[:iid].to_s + end + + expose :total_time do |object| + distance_of_time_as_hash(object[:total_time].to_f) + end + + expose(:created_at) do |object| + interval_in_words(object[:created_at]) + end + + expose :url do |object| + url_to(:namespace_project_issue, id: object[:iid].to_s) + end + + private + + def url_to(route, id) + public_send("#{route}_url", request.project.namespace, request.project, id) + end +end diff --git a/app/serializers/analytics_issue_serializer.rb b/app/serializers/analytics_issue_serializer.rb new file mode 100644 index 00000000000..4fb3e8f1bb4 --- /dev/null +++ b/app/serializers/analytics_issue_serializer.rb @@ -0,0 +1,3 @@ +class AnalyticsIssueSerializer < AnalyticsGenericSerializer + entity AnalyticsIssueEntity +end diff --git a/app/serializers/analytics_merge_request_entity.rb b/app/serializers/analytics_merge_request_entity.rb new file mode 100644 index 00000000000..888265eaa38 --- /dev/null +++ b/app/serializers/analytics_merge_request_entity.rb @@ -0,0 +1,7 @@ +class AnalyticsMergeRequestEntity < AnalyticsIssueEntity + expose :state + + expose :url do |object| + url_to(:namespace_project_merge_request, id: object[:iid].to_s) + end +end diff --git a/app/serializers/analytics_merge_request_serializer.rb b/app/serializers/analytics_merge_request_serializer.rb new file mode 100644 index 00000000000..4622a1dd855 --- /dev/null +++ b/app/serializers/analytics_merge_request_serializer.rb @@ -0,0 +1,3 @@ +class AnalyticsMergeRequestSerializer < AnalyticsGenericSerializer + entity AnalyticsMergeRequestEntity +end diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb new file mode 100644 index 00000000000..a559d0850c4 --- /dev/null +++ b/app/serializers/analytics_stage_entity.rb @@ -0,0 +1,10 @@ +class AnalyticsStageEntity < Grape::Entity + include EntityDateHelper + + expose :title + expose :description + + expose :median, as: :value do |stage| + stage.median && !stage.median.zero? ? distance_of_time_in_words(stage.median) : nil + end +end diff --git a/app/serializers/analytics_stage_serializer.rb b/app/serializers/analytics_stage_serializer.rb new file mode 100644 index 00000000000..613cf6874d8 --- /dev/null +++ b/app/serializers/analytics_stage_serializer.rb @@ -0,0 +1,3 @@ +class AnalyticsStageSerializer < BaseSerializer + entity AnalyticsStageEntity +end diff --git a/app/serializers/analytics_summary_entity.rb b/app/serializers/analytics_summary_entity.rb new file mode 100644 index 00000000000..91803ec07f5 --- /dev/null +++ b/app/serializers/analytics_summary_entity.rb @@ -0,0 +1,7 @@ +class AnalyticsSummaryEntity < Grape::Entity + expose :value, safe: true + + expose :title do |object| + object.title.pluralize(object.value) + end +end diff --git a/app/serializers/analytics_summary_serializer.rb b/app/serializers/analytics_summary_serializer.rb new file mode 100644 index 00000000000..c87a24aa47c --- /dev/null +++ b/app/serializers/analytics_summary_serializer.rb @@ -0,0 +1,3 @@ +class AnalyticsSummarySerializer < BaseSerializer + entity AnalyticsSummaryEntity +end diff --git a/app/serializers/build_action_entity.rb b/app/serializers/build_action_entity.rb new file mode 100644 index 00000000000..184f5fd4b52 --- /dev/null +++ b/app/serializers/build_action_entity.rb @@ -0,0 +1,14 @@ +class BuildActionEntity < Grape::Entity + include RequestAwareEntity + + expose :name do |build| + build.name + end + + expose :path do |build| + play_namespace_project_build_path( + build.project.namespace, + build.project, + build) + end +end diff --git a/app/serializers/build_artifact_entity.rb b/app/serializers/build_artifact_entity.rb new file mode 100644 index 00000000000..8b643d8e783 --- /dev/null +++ b/app/serializers/build_artifact_entity.rb @@ -0,0 +1,14 @@ +class BuildArtifactEntity < Grape::Entity + include RequestAwareEntity + + expose :name do |build| + build.name + end + + expose :path do |build| + download_namespace_project_build_artifacts_path( + build.project.namespace, + build.project, + build) + end +end diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb index 3d9ac66de0e..b5384e6462b 100644 --- a/app/serializers/build_entity.rb +++ b/app/serializers/build_entity.rb @@ -4,21 +4,24 @@ class BuildEntity < Grape::Entity expose :id expose :name - expose :build_url do |build| - url_to(:namespace_project_build, build) + expose :build_path do |build| + path_to(:namespace_project_build, build) end - expose :retry_url do |build| - url_to(:retry_namespace_project_build, build) + expose :retry_path do |build| + path_to(:retry_namespace_project_build, build) end - expose :play_url, if: ->(build, _) { build.manual? } do |build| - url_to(:play_namespace_project_build, build) + expose :play_path, if: ->(build, _) { build.manual? } do |build| + path_to(:play_namespace_project_build, build) end + expose :created_at + expose :updated_at + private - def url_to(route, build) - send("#{route}_url", build.project.namespace, build.project, build) + def path_to(route, build) + send("#{route}_path", build.project.namespace, build.project, build) end end diff --git a/app/serializers/commit_entity.rb b/app/serializers/commit_entity.rb index f7eba6fc1e3..31763955f97 100644 --- a/app/serializers/commit_entity.rb +++ b/app/serializers/commit_entity.rb @@ -3,10 +3,21 @@ class CommitEntity < API::Entities::RepoCommit expose :author, using: UserEntity + expose :author_gravatar_url do |commit| + GravatarService.new.execute(commit.author_email) + end + expose :commit_url do |commit| - namespace_project_tree_url( + namespace_project_commit_url( + request.project.namespace, + request.project, + commit) + end + + expose :commit_path do |commit| + namespace_project_commit_path( request.project.namespace, request.project, - id: commit.id) + commit) end end diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb index ad6fc8d665b..d610fbe0c8a 100644 --- a/app/serializers/deployment_entity.rb +++ b/app/serializers/deployment_entity.rb @@ -10,8 +10,8 @@ class DeploymentEntity < Grape::Entity deployment.ref end - expose :ref_url do |deployment| - namespace_project_tree_url( + expose :ref_path do |deployment| + namespace_project_tree_path( deployment.project.namespace, deployment.project, id: deployment.ref) diff --git a/app/serializers/entity_date_helper.rb b/app/serializers/entity_date_helper.rb new file mode 100644 index 00000000000..9607ad55a8b --- /dev/null +++ b/app/serializers/entity_date_helper.rb @@ -0,0 +1,37 @@ +module EntityDateHelper + include ActionView::Helpers::DateHelper + + def interval_in_words(diff) + return 'Not started' unless diff + + "#{distance_of_time_in_words(Time.now, diff)} ago" + end + + # Converts seconds into a hash such as: + # { days: 1, hours: 3, mins: 42, seconds: 40 } + # + # It returns 0 seconds for zero or negative numbers + # It rounds to nearest time unit and does not return zero + # i.e { min: 1 } instead of { mins: 1, seconds: 0 } + def distance_of_time_as_hash(diff) + diff = diff.abs.floor + + return { seconds: 0 } if diff == 0 + + mins = (diff / 60).floor + seconds = diff % 60 + hours = (mins / 60).floor + mins = mins % 60 + days = (hours / 24).floor + hours = hours % 24 + + duration_hash = {} + + duration_hash[:days] = days if days > 0 + duration_hash[:hours] = hours if hours > 0 + duration_hash[:mins] = mins if mins > 0 + duration_hash[:seconds] = seconds if seconds > 0 + + duration_hash + end +end diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index bfccfd8bb7c..7279de59aa8 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -9,12 +9,27 @@ class EnvironmentEntity < Grape::Entity expose :last_deployment, using: DeploymentEntity expose :can_run_stop_action? - expose :environment_url do |environment| - namespace_project_environment_url( + expose :environment_path do |environment| + namespace_project_environment_path( environment.project.namespace, environment.project, environment) end + expose :stop_path do |environment| + stop_namespace_project_environment_path( + environment.project.namespace, + environment.project, + environment) + end + + expose :terminal_path, if: ->(environment, _) { environment.has_terminals? } do |environment| + can?(request.user, :admin_environment, environment.project) && + terminal_namespace_project_environment_path( + environment.project.namespace, + environment.project, + environment) + end + expose :created_at, :updated_at end diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb new file mode 100644 index 00000000000..29aecb50849 --- /dev/null +++ b/app/serializers/issuable_entity.rb @@ -0,0 +1,20 @@ +class IssuableEntity < Grape::Entity + expose :id + expose :iid + expose :assignee_id + expose :author_id + expose :description + expose :lock_version + expose :milestone_id + expose :position + expose :state + expose :title + expose :updated_by_id + expose :created_at + expose :updated_at + expose :deleted_at + expose :time_estimate + expose :total_time_spent + expose :human_time_estimate + expose :human_total_time_spent +end diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb new file mode 100644 index 00000000000..6429159ebe1 --- /dev/null +++ b/app/serializers/issue_entity.rb @@ -0,0 +1,9 @@ +class IssueEntity < IssuableEntity + expose :branch_name + expose :confidential + expose :due_date + expose :moved_to_id + expose :project_id + expose :milestone, using: API::Entities::Milestone + expose :labels, using: LabelEntity +end diff --git a/app/serializers/issue_serializer.rb b/app/serializers/issue_serializer.rb new file mode 100644 index 00000000000..4fff54a9126 --- /dev/null +++ b/app/serializers/issue_serializer.rb @@ -0,0 +1,3 @@ +class IssueSerializer < BaseSerializer + entity IssueEntity +end diff --git a/app/serializers/label_entity.rb b/app/serializers/label_entity.rb new file mode 100644 index 00000000000..304fd9de08f --- /dev/null +++ b/app/serializers/label_entity.rb @@ -0,0 +1,11 @@ +class LabelEntity < Grape::Entity + expose :id + expose :title + expose :color + expose :description + expose :group_id + expose :project_id + expose :template + expose :created_at + expose :updated_at +end diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb new file mode 100644 index 00000000000..7445298c714 --- /dev/null +++ b/app/serializers/merge_request_entity.rb @@ -0,0 +1,14 @@ +class MergeRequestEntity < IssuableEntity + expose :in_progress_merge_commit_sha + expose :locked_at + expose :merge_commit_sha + expose :merge_error + expose :merge_params + expose :merge_status + expose :merge_user_id + expose :merge_when_build_succeeds + expose :source_branch + expose :source_project_id + expose :target_branch + expose :target_project_id +end diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb new file mode 100644 index 00000000000..aa6e00dfcb4 --- /dev/null +++ b/app/serializers/merge_request_serializer.rb @@ -0,0 +1,3 @@ +class MergeRequestSerializer < BaseSerializer + entity MergeRequestEntity +end diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb new file mode 100644 index 00000000000..61f0f11d7d2 --- /dev/null +++ b/app/serializers/pipeline_entity.rb @@ -0,0 +1,85 @@ +class PipelineEntity < Grape::Entity + include RequestAwareEntity + + expose :id + expose :user, using: UserEntity + + expose :path do |pipeline| + namespace_project_pipeline_path( + pipeline.project.namespace, + pipeline.project, + pipeline) + end + + expose :details do + expose :status do |pipeline, options| + StatusEntity.represent( + pipeline.detailed_status(request.user), + options) + end + + expose :duration + expose :finished_at + expose :stages, using: StageEntity + expose :artifacts, using: BuildArtifactEntity + expose :manual_actions, using: BuildActionEntity + end + + expose :flags do + expose :latest?, as: :latest + expose :triggered?, as: :triggered + expose :stuck?, as: :stuck + expose :has_yaml_errors?, as: :yaml_errors + expose :can_retry?, as: :retryable + expose :can_cancel?, as: :cancelable + end + + expose :ref do + expose :name do |pipeline| + pipeline.ref + end + + expose :path do |pipeline| + if pipeline.ref + namespace_project_tree_path( + pipeline.project.namespace, + pipeline.project, + id: pipeline.ref) + end + end + + expose :tag?, as: :tag + expose :branch?, as: :branch + end + + expose :commit, using: CommitEntity + expose :yaml_errors, if: ->(pipeline, _) { pipeline.has_yaml_errors? } + + expose :retry_path, if: proc { can_retry? } do |pipeline| + retry_namespace_project_pipeline_path(pipeline.project.namespace, + pipeline.project, + pipeline.id) + end + + expose :cancel_path, if: proc { can_cancel? } do |pipeline| + cancel_namespace_project_pipeline_path(pipeline.project.namespace, + pipeline.project, + pipeline.id) + end + + expose :created_at, :updated_at + + private + + alias_method :pipeline, :object + + def can_retry? + pipeline.retryable? && + can?(request.user, :update_pipeline, pipeline) + end + + def can_cancel? + pipeline.cancelable? && + can?(request.user, :update_pipeline, pipeline) + end +end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb new file mode 100644 index 00000000000..cfa86cc2553 --- /dev/null +++ b/app/serializers/pipeline_serializer.rb @@ -0,0 +1,40 @@ +class PipelineSerializer < BaseSerializer + entity PipelineEntity + class InvalidResourceError < StandardError; end + include API::Helpers::Pagination + Struct.new('Pagination', :request, :response) + + def represent(resource, opts = {}) + if paginated? + raise InvalidResourceError unless resource.respond_to?(:page) + + super(paginate(resource.includes(project: :namespace)), opts) + else + super(resource, opts) + end + end + + def paginated? + defined?(@pagination) + end + + def with_pagination(request, response) + tap { @pagination = Struct::Pagination.new(request, response) } + end + + private + + # Methods needed by `API::Helpers::Pagination` + # + def params + @pagination.request.query_parameters + end + + def request + @pagination.request + end + + def header(header, value) + @pagination.response.headers[header] = value + end +end diff --git a/app/serializers/request_aware_entity.rb b/app/serializers/request_aware_entity.rb index ff8c1142abc..3039014aaaa 100644 --- a/app/serializers/request_aware_entity.rb +++ b/app/serializers/request_aware_entity.rb @@ -2,10 +2,11 @@ module RequestAwareEntity extend ActiveSupport::Concern included do - include Gitlab::Routing.url_helpers + include Gitlab::Routing + include Gitlab::Allowable end def request - @options.fetch(:request) + options.fetch(:request) end end diff --git a/app/serializers/stage_entity.rb b/app/serializers/stage_entity.rb new file mode 100644 index 00000000000..7a047bdc712 --- /dev/null +++ b/app/serializers/stage_entity.rb @@ -0,0 +1,38 @@ +class StageEntity < Grape::Entity + include RequestAwareEntity + + expose :name + + expose :title do |stage| + "#{stage.name}: #{detailed_status.label}" + end + + expose :detailed_status, + as: :status, + with: StatusEntity + + expose :path do |stage| + namespace_project_pipeline_path( + stage.pipeline.project.namespace, + stage.pipeline.project, + stage.pipeline, + anchor: stage.name) + end + + expose :dropdown_path do |stage| + stage_namespace_project_pipeline_path( + stage.pipeline.project.namespace, + stage.pipeline.project, + stage.pipeline, + stage: stage.name, + format: :json) + end + + private + + alias_method :stage, :object + + def detailed_status + stage.detailed_status(request.user) + end +end diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb new file mode 100644 index 00000000000..47066bebfb1 --- /dev/null +++ b/app/serializers/status_entity.rb @@ -0,0 +1,8 @@ +class StatusEntity < Grape::Entity + include RequestAwareEntity + + expose :icon, :text, :label, :group + + expose :has_details?, as: :has_details + expose :details_path +end diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb new file mode 100644 index 00000000000..ddaaed90e5b --- /dev/null +++ b/app/services/access_token_validation_service.rb @@ -0,0 +1,32 @@ +AccessTokenValidationService = Struct.new(:token) do + # Results: + VALID = :valid + EXPIRED = :expired + REVOKED = :revoked + INSUFFICIENT_SCOPE = :insufficient_scope + + def validate(scopes: []) + if token.expired? + return EXPIRED + + elsif token.revoked? + return REVOKED + + elsif !self.include_any_scope?(scopes) + return INSUFFICIENT_SCOPE + + else + return VALID + end + end + + # True if the token's scope contains any of the passed scopes. + def include_any_scope?(scopes) + if scopes.blank? + true + else + # Check whether the token is allowed access to any of the required scopes. + Set.new(scopes).intersection(Set.new(token.scopes)).present? + end + end +end diff --git a/app/services/after_branch_delete_service.rb b/app/services/after_branch_delete_service.rb new file mode 100644 index 00000000000..227e9ea9c6d --- /dev/null +++ b/app/services/after_branch_delete_service.rb @@ -0,0 +1,21 @@ +## +# Branch can be deleted either by DeleteBranchService +# or by GitPushService. +# +class AfterBranchDeleteService < BaseService + attr_reader :branch_name + + def execute(branch_name) + @branch_name = branch_name + + stop_environments + end + + private + + def stop_environments + Ci::StopEnvironmentsService + .new(project, current_user) + .execute(branch_name) + end +end diff --git a/app/services/chat_names/authorize_user_service.rb b/app/services/chat_names/authorize_user_service.rb new file mode 100644 index 00000000000..321bf3a9205 --- /dev/null +++ b/app/services/chat_names/authorize_user_service.rb @@ -0,0 +1,38 @@ +module ChatNames + class AuthorizeUserService + include Gitlab::Routing.url_helpers + + def initialize(service, params) + @service = service + @params = params + end + + def execute + return unless chat_name_params.values.all?(&:present?) + + token = request_token + + new_profile_chat_name_url(token: token) if token + end + + private + + def request_token + chat_name_token.store!(chat_name_params) + end + + def chat_name_token + Gitlab::ChatNameToken.new + end + + def chat_name_params + { + service_id: @service.id, + team_id: @params[:team_id], + team_domain: @params[:team_domain], + chat_id: @params[:user_id], + chat_name: @params[:user_name] + } + end + end +end diff --git a/app/services/chat_names/find_user_service.rb b/app/services/chat_names/find_user_service.rb new file mode 100644 index 00000000000..4f5c5567b42 --- /dev/null +++ b/app/services/chat_names/find_user_service.rb @@ -0,0 +1,26 @@ +module ChatNames + class FindUserService + def initialize(service, params) + @service = service + @params = params + end + + def execute + chat_name = find_chat_name + return unless chat_name + + chat_name.touch(:last_used_at) + chat_name.user + end + + private + + def find_chat_name + ChatName.find_by( + service: @service, + team_id: @params[:team_id], + chat_id: @params[:user_id] + ) + end + end +end diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb index 005014fa1de..b7da3f8e7eb 100644 --- a/app/services/ci/create_pipeline_builds_service.rb +++ b/app/services/ci/create_pipeline_builds_service.rb @@ -10,18 +10,29 @@ module Ci end end + def project + pipeline.project + end + private def create_build(build_attributes) build_attributes = build_attributes.merge( pipeline: pipeline, - project: pipeline.project, + project: project, ref: pipeline.ref, tag: pipeline.tag, user: current_user, trigger_request: trigger_request ) - pipeline.builds.create(build_attributes) + build = pipeline.builds.create(build_attributes) + + # Create the environment before the build starts. This sets its slug and + # makes it available as an environment variable + project.environments.find_or_create_by(name: build.expanded_environment_name) if + build.has_environment? + + build end def new_builds diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index cde856b0186..e3bc9847200 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -45,9 +45,15 @@ module Ci return error('No builds for this pipeline.') end - pipeline.save - pipeline.process! - pipeline + Ci::Pipeline.transaction do + pipeline.save + + Ci::CreatePipelineBuildsService + .new(project, current_user) + .execute(pipeline) + end + + pipeline.tap(&:process!) end private diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index 75d847d5bee..240ddabec36 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -1,13 +1,13 @@ module Ci class ImageForBuildService def execute(project, opts) - sha = opts[:sha] || ref_sha(project, opts[:ref]) - + ref = opts[:ref] + sha = opts[:sha] || ref_sha(project, ref) pipelines = project.pipelines.where(sha: sha) - pipelines = pipelines.where(ref: opts[:ref]) if opts[:ref] - image_name = image_for_status(pipelines.status) + image_name = image_for_status(pipelines.latest_status(ref)) image_path = Rails.root.join('public/ci', image_name) + OpenStruct.new(path: image_path, name: image_name) end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 8face432d97..79eb97b7b55 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -5,10 +5,7 @@ module Ci def execute(pipeline) @pipeline = pipeline - # This method will ensure that our pipeline does have all builds for all stages created - if created_builds.empty? - create_builds! - end + ensure_created_builds! # TODO, remove me in 9.0 new_builds = stage_indexes_of_created_builds.map do |index| @@ -22,10 +19,6 @@ module Ci private - def create_builds! - Ci::CreatePipelineBuildsService.new(project, current_user).execute(pipeline) - end - def process_stage(index) current_status = status_for_prior_stages(index) @@ -51,11 +44,11 @@ module Ci def valid_statuses_for_when(value) case value when 'on_success' - %w[success] + %w[success skipped] when 'on_failure' %w[failed] when 'always' - %w[success failed] + %w[success failed skipped] else [] end @@ -76,5 +69,18 @@ module Ci def created_builds pipeline.builds.created end + + # This method is DEPRECATED and should be removed in 9.0. + # + # We need it to maintain backwards compatibility with previous versions + # when builds were not created within one transaction with the pipeline. + # + def ensure_created_builds! + return if created_builds.any? + + Ci::CreatePipelineBuildsService + .new(project, current_user) + .execute(pipeline) + end end end diff --git a/app/services/ci/stop_environments_service.rb b/app/services/ci/stop_environments_service.rb new file mode 100644 index 00000000000..cf590459cb2 --- /dev/null +++ b/app/services/ci/stop_environments_service.rb @@ -0,0 +1,29 @@ +module Ci + class StopEnvironmentsService < BaseService + attr_reader :ref + + def execute(branch_name) + @ref = branch_name + + return unless has_ref? + + environments.each do |environment| + next unless environment.stoppable? + next unless can?(current_user, :create_deployment, project) + + environment.stop!(current_user) + end + end + + private + + def has_ref? + @ref.present? + end + + def environments + @environments ||= project + .environments_recently_updated_on_branch(@ref) + end + end +end diff --git a/app/services/ci/update_build_queue_service.rb b/app/services/ci/update_build_queue_service.rb new file mode 100644 index 00000000000..152c8ae5006 --- /dev/null +++ b/app/services/ci/update_build_queue_service.rb @@ -0,0 +1,19 @@ +module Ci + class UpdateBuildQueueService + def execute(build) + build.project.runners.each do |runner| + if runner.can_pick?(build) + runner.tick_runner_queue + end + end + + return unless build.project.shared_runners_enabled? + + Ci::Runner.shared.each do |runner| + if runner.can_pick?(build) + runner.tick_runner_queue + end + end + end + end +end diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 1c82599c579..4d410f66c55 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -34,8 +34,8 @@ module Commits repository.public_send(action, current_user, @commit, into, tree_id) success else - error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title} automatically. - It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content." + error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. + A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content." raise ChangeError, error_msg end end diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index 757fc35a78f..e004a303496 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -1,5 +1,3 @@ -require_relative 'base_service' - class CreateBranchService < BaseService def execute(branch_name, ref, source_project: @project) valid_branch = Gitlab::GitRefValidator.validate(branch_name) diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb index 8ae15ad32f4..47f9b2c621c 100644 --- a/app/services/create_deployment_service.rb +++ b/app/services/create_deployment_service.rb @@ -1,5 +1,3 @@ -require_relative 'base_service' - class CreateDeploymentService < BaseService def execute(deployable = nil) return unless executable? diff --git a/app/services/create_release_service.rb b/app/services/create_release_service.rb index d6d4afcf29a..54ff1f74126 100644 --- a/app/services/create_release_service.rb +++ b/app/services/create_release_service.rb @@ -1,5 +1,3 @@ -require_relative 'base_service' - class CreateReleaseService < BaseService def execute(tag_name, release_description) repository = project.repository diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb index c0e7ecf6a96..fe9353afeb8 100644 --- a/app/services/create_tag_service.rb +++ b/app/services/create_tag_service.rb @@ -1,5 +1,3 @@ -require_relative 'base_service' - class CreateTagService < BaseService def execute(tag_name, target, message, release_description = nil) valid_tag = Gitlab::GitRefValidator.validate(tag_name) diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index 3e5dd4ebb86..11a045f4c31 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -1,5 +1,3 @@ -require_relative 'base_service' - class DeleteBranchService < BaseService def execute(branch_name) repository = project.repository diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb new file mode 100644 index 00000000000..1b5623baebe --- /dev/null +++ b/app/services/delete_merged_branches_service.rb @@ -0,0 +1,16 @@ +class DeleteMergedBranchesService < BaseService + def async_execute + DeleteMergedBranchesWorker.perform_async(project.id, current_user.id) + end + + def execute + raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project) + + branches = project.repository.branch_names + branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) } + + branches.each do |branch| + DeleteBranchService.new(project, current_user).execute(branch) + end + end +end diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb index d824406cb49..a44dee14a0f 100644 --- a/app/services/delete_tag_service.rb +++ b/app/services/delete_tag_service.rb @@ -1,5 +1,3 @@ -require_relative 'base_service' - class DeleteTagService < BaseService def execute(tag_name) repository = project.repository diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb index 0081364b8aa..2316c57bf1e 100644 --- a/app/services/destroy_group_service.rb +++ b/app/services/destroy_group_service.rb @@ -6,12 +6,10 @@ class DestroyGroupService end def async_execute - group.transaction do - # Soft delete via paranoia gem - group.destroy - job_id = GroupDestroyWorker.perform_async(group.id, current_user.id) - Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}") - end + # Soft delete via paranoia gem + group.destroy + job_id = GroupDestroyWorker.perform_async(group.id, current_user.id) + Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}") end def execute @@ -22,6 +20,10 @@ class DestroyGroupService ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute end + group.children.each do |group| + DestroyGroupService.new(group, current_user).async_execute + end + group.really_destroy! end end diff --git a/app/services/discussions/base_service.rb b/app/services/discussions/base_service.rb new file mode 100644 index 00000000000..e4dfe6e71bb --- /dev/null +++ b/app/services/discussions/base_service.rb @@ -0,0 +1,4 @@ +module Discussions + class BaseService < ::BaseService + end +end diff --git a/app/services/discussions/resolve_service.rb b/app/services/discussions/resolve_service.rb new file mode 100644 index 00000000000..0437195f588 --- /dev/null +++ b/app/services/discussions/resolve_service.rb @@ -0,0 +1,24 @@ +module Discussions + class ResolveService < Discussions::BaseService + def execute(one_or_more_discussions) + Array(one_or_more_discussions).each { |discussion| resolve_discussion(discussion) } + end + + def resolve_discussion(discussion) + return unless discussion.can_resolve?(current_user) + + discussion.resolve!(current_user) + + MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request) + SystemNoteService.discussion_continued_in_issue(discussion, project, current_user, follow_up_issue) if follow_up_issue + end + + def merge_request + params[:merge_request] + end + + def follow_up_issue + params[:follow_up_issue] + end + end +end diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb index d00d78cee7e..e5b4d60e467 100644 --- a/app/services/files/create_dir_service.rb +++ b/app/services/files/create_dir_service.rb @@ -1,5 +1,3 @@ -require_relative "base_service" - module Files class CreateDirService < Files::BaseService def commit diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index bf127843d55..b23576b9a28 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -1,5 +1,3 @@ -require_relative "base_service" - module Files class CreateService < Files::BaseService def commit diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb index 8b27ad51789..4f7e7a5baaa 100644 --- a/app/services/files/delete_service.rb +++ b/app/services/files/delete_service.rb @@ -1,5 +1,3 @@ -require_relative "base_service" - module Files class DeleteService < Files::BaseService def commit diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index d28912e1301..54446e90007 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -1,5 +1,3 @@ -require_relative "base_service" - module Files class MultiService < Files::BaseService class FileChangedError < StandardError; end diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index c17fdb8d1f1..47a18e3e132 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -1,5 +1,3 @@ -require_relative "base_service" - module Files class UpdateService < Files::BaseService class FileChangedError < StandardError; end diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb index 172bd85dade..6cd3908d43a 100644 --- a/app/services/git_hooks_service.rb +++ b/app/services/git_hooks_service.rb @@ -1,6 +1,8 @@ class GitHooksService PreReceiveError = Class.new(StandardError) + attr_accessor :oldrev, :newrev, :ref + def execute(user, repo_path, oldrev, newrev, ref) @repo_path = repo_path @user = Gitlab::GlId.gl_id(user) @@ -16,7 +18,7 @@ class GitHooksService end end - yield + yield self run_hook('post-receive') end @@ -25,6 +27,6 @@ class GitHooksService def run_hook(name) hook = Gitlab::Git::Hook.new(name, @repo_path) - hook.trigger(@user, @oldrev, @newrev, @ref) + hook.trigger(@user, oldrev, newrev, ref) end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index de313095bed..dbe2fda27b5 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -3,6 +3,9 @@ class GitPushService < BaseService include Gitlab::CurrentSettings include Gitlab::Access + # The N most recent commits to process in a single push payload. + PROCESS_COMMIT_LIMIT = 100 + # This method will be called after each git update # and only if the provided user and project are present in GitLab. # @@ -18,7 +21,7 @@ class GitPushService < BaseService # def execute @project.repository.after_create if @project.empty_repo? - @project.repository.after_push_commit(branch_name, params[:newrev]) + @project.repository.after_push_commit(branch_name) if push_remove_branch? @project.repository.after_remove_branch @@ -49,27 +52,63 @@ class GitPushService < BaseService update_gitattributes if is_default_branch? end - # Update merge requests that may be affected by this push. A new branch - # could cause the last commit of a merge request to change. - update_merge_requests - + execute_related_hooks perform_housekeeping + + update_caches end def update_gitattributes @project.repository.copy_gitattributes(params[:ref]) end + def update_caches + if is_default_branch? + paths = Set.new + + @push_commits.each do |commit| + commit.raw_diffs(deltas_only: true).each do |diff| + paths << diff.new_path + end + end + + types = Gitlab::FileDetector.types_in_paths(paths.to_a) + else + types = [] + end + + ProjectCacheWorker.perform_async(@project.id, types, [:commit_count, :repository_size]) + end + + # Schedules processing of commit messages. + def process_commit_messages + default = is_default_branch? + + push_commits.last(PROCESS_COMMIT_LIMIT).each do |commit| + ProcessCommitWorker. + perform_async(project.id, current_user.id, commit.to_hash, default) + end + end + protected - def update_merge_requests - UpdateMergeRequestsWorker.perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref]) + def execute_related_hooks + # Update merge requests that may be affected by this push. A new branch + # could cause the last commit of a merge request to change. + # + UpdateMergeRequestsWorker + .perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref]) EventCreateService.new.push(@project, current_user, build_push_data) @project.execute_hooks(build_push_data.dup, :push_hooks) @project.execute_services(build_push_data.dup, :push_hooks) Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute - ProjectCacheWorker.perform_async(@project.id) + + if push_remove_branch? + AfterBranchDeleteService + .new(project, current_user) + .execute(branch_name) + end end def perform_housekeeping @@ -102,17 +141,6 @@ class GitPushService < BaseService end end - # Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched, - # close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables. - def process_commit_messages - default = is_default_branch? - - @push_commits.each do |commit| - ProcessCommitWorker. - perform_async(project.id, current_user.id, commit.id, default) - end - end - def build_push_data @push_data ||= Gitlab::DataBuilder::Push.build( @project, diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 20a4445bddf..96432837481 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -12,7 +12,7 @@ class GitTagPushService < BaseService project.execute_hooks(@push_data.dup, :tag_push_hooks) project.execute_services(@push_data.dup, :tag_push_hooks) Ci::CreatePipelineService.new(project, current_user, @push_data).execute - ProjectCacheWorker.perform_async(project.id) + ProjectCacheWorker.perform_async(project.id, [], [:commit_count, :repository_size]) true end diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index 2bccd584dde..febeb661fb5 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -12,6 +12,13 @@ module Groups return @group end + if @group.parent && !can?(current_user, :admin_group, @group.parent) + @group.parent = nil + @group.errors.add(:parent_id, 'manage access required to create subgroup') + + return @group + end + @group.name ||= @group.path.dup @group.save @group.add_owner(current_user) diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index 99ad12b1003..4e878ec556a 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -5,7 +5,7 @@ module Groups new_visibility = params[:visibility_level] if new_visibility && new_visibility.to_i != group.visibility_level unless can?(current_user, :change_visibility_level, group) && - Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) + Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) deny_visibility_level(group, new_visibility) return group @@ -14,7 +14,13 @@ module Groups group.assign_attributes(params) - group.save + begin + group.save + rescue Gitlab::UpdatePathError => e + group.errors.add(:base, e.message) + + false + end end end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index bb92cd80cc9..5f3ced49665 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -36,14 +36,18 @@ class IssuableBaseService < BaseService end end - def filter_params(issuable_ability_name = :issue) - filter_assignee - filter_milestone - filter_labels + def create_time_estimate_note(issuable) + SystemNoteService.change_time_estimate(issuable, issuable.project, current_user) + end - ability = :"admin_#{issuable_ability_name}" + def create_time_spent_note(issuable) + SystemNoteService.change_time_spent(issuable, issuable.project, current_user) + end - unless can?(current_user, ability, project) + def filter_params(issuable) + ability_name = :"admin_#{issuable.to_ability_name}" + + unless can?(current_user, ability_name, project) params.delete(:milestone_id) params.delete(:labels) params.delete(:add_label_ids) @@ -52,14 +56,35 @@ class IssuableBaseService < BaseService params.delete(:assignee_id) params.delete(:due_date) end + + filter_assignee(issuable) + filter_milestone + filter_labels end - def filter_assignee - if params[:assignee_id] == IssuableFinder::NONE - params[:assignee_id] = '' + def filter_assignee(issuable) + return unless params[:assignee_id].present? + + assignee_id = params[:assignee_id] + + if assignee_id.to_s == IssuableFinder::NONE + params[:assignee_id] = "" + else + params.delete(:assignee_id) unless assignee_can_read?(issuable, assignee_id) end end + def assignee_can_read?(issuable, assignee_id) + new_assignee = User.find_by_id(assignee_id) + + return false unless new_assignee.present? + + ability_name = :"read_#{issuable.to_ability_name}" + resource = issuable.persisted? ? issuable : project + + can?(new_assignee, ability_name, resource) + end + def filter_milestone milestone_id = params[:milestone_id] return unless milestone_id @@ -85,14 +110,15 @@ class IssuableBaseService < BaseService def find_or_create_label_ids labels = params.delete(:labels) + return unless labels - params[:label_ids] = labels.split(',').map do |label_name| + params[:label_ids] = labels.split(",").map do |label_name| service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip) label = service.execute - label.id - end + label.try(:id) + end.compact end def process_label_ids(attributes, existing_label_ids: nil) @@ -119,9 +145,10 @@ class IssuableBaseService < BaseService def merge_slash_commands_into_params!(issuable) description, command_params = SlashCommands::InterpretService.new(project, current_user). - execute(params[:description], issuable) + execute(params[:description], issuable) - params[:description] = description + # Avoid a description already set on an issuable to be overwritten by a nil + params[:description] = description if params.has_key?(:description) params.merge!(command_params) end @@ -136,10 +163,11 @@ class IssuableBaseService < BaseService def create(issuable) merge_slash_commands_into_params!(issuable) - filter_params + filter_params(issuable) params.delete(:state_event) params[:author] ||= current_user + label_ids = process_label_ids(params) issuable.assign_attributes(params) @@ -177,15 +205,14 @@ class IssuableBaseService < BaseService change_state(issuable) change_subscription(issuable) change_todo(issuable) - filter_params + filter_params(issuable) old_labels = issuable.labels.to_a old_mentioned_users = issuable.mentioned_users.to_a - params[:label_ids] = process_label_ids(params, existing_label_ids: issuable.label_ids) + label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids) + params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids) if params.present? && update_issuable(issuable, params) - issuable.reset_events_cache - # We do not touch as it will affect a update on updated_at field ActiveRecord::Base.no_touching do handle_common_system_notes(issuable, old_labels: old_labels) @@ -200,6 +227,10 @@ class IssuableBaseService < BaseService issuable end + def labels_changing?(old_label_ids, new_label_ids) + old_label_ids.sort != new_label_ids.sort + end + def change_state(issuable) case params.delete(:state_event) when 'reopen' @@ -212,9 +243,9 @@ class IssuableBaseService < BaseService def change_subscription(issuable) case params.delete(:subscription_event) when 'subscribe' - issuable.subscribe(current_user) + issuable.subscribe(current_user, project) when 'unsubscribe' - issuable.unsubscribe(current_user) + issuable.unsubscribe(current_user, project) end end @@ -249,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/issues/base_service.rb b/app/services/issues/base_service.rb index 9ea3ce084ba..35af867a098 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -1,5 +1,13 @@ module Issues class BaseService < ::IssuableBaseService + attr_reader :merge_request_for_resolving_discussions + + def initialize(*args) + super + + @merge_request_for_resolving_discussions ||= params.delete(:merge_request_for_resolving_discussions) + end + def hook_data(issue, action) issue_data = issue.to_hook_data(current_user) issue_url = Gitlab::UrlBuilder.build(issue) @@ -9,10 +17,6 @@ module Issues private - def filter_params - super(:issue) - end - def execute_hooks(issue, action = 'open') issue_data = hook_data(issue, action) hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb new file mode 100644 index 00000000000..a63982f60c8 --- /dev/null +++ b/app/services/issues/build_service.rb @@ -0,0 +1,50 @@ +module Issues + class BuildService < Issues::BaseService + def execute + @issue = project.issues.new(issue_params) + end + + def issue_params_with_info_from_merge_request + return {} unless merge_request_for_resolving_discussions + + { title: title_from_merge_request, description: description_from_merge_request } + end + + def title_from_merge_request + "Follow-up from \"#{merge_request_for_resolving_discussions.title}\"" + end + + def description_from_merge_request + if merge_request_for_resolving_discussions.resolvable_discussions.empty? + return "There are no unresolved discussions. "\ + "Review the conversation in #{merge_request_for_resolving_discussions.to_reference}" + end + + description = "The following discussions from #{merge_request_for_resolving_discussions.to_reference} should be addressed:" + [description, *items_for_discussions].join("\n\n") + end + + def items_for_discussions + merge_request_for_resolving_discussions.resolvable_discussions.map { |discussion| item_for_discussion(discussion) } + end + + def item_for_discussion(discussion) + first_note = discussion.first_note_to_resolve + other_note_count = discussion.notes.size - 1 + creation_time = first_note.created_at.to_s(:medium) + note_url = Gitlab::UrlBuilder.build(first_note) + + discussion_info = "- [ ] #{first_note.author.to_reference} commented in a discussion on [#{creation_time}](#{note_url}): " + discussion_info << " (+#{other_note_count} #{'comment'.pluralize(other_note_count)})" if other_note_count > 0 + + note_without_block_quotes = Banzai::Filter::BlockquoteFenceFilter.new(first_note.note).call + quote = ">>>\n#{note_without_block_quotes}\n>>>" + + [discussion_info, quote].join("\n\n") + end + + def issue_params + @issue_params ||= issue_params_with_info_from_merge_request.merge(params.slice(:title, :description)) + end + end +end diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index ab4c51386a4..f1030912c68 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -17,7 +17,7 @@ module Issues # allowed to close the given issue. def close_issue(issue, commit: nil, notifications: true, system_note: true) if project.jira_tracker? && project.jira_service.active - project.jira_service.execute(commit, issue) + project.jira_service.close_issue(commit, issue) todo_service.close_issue(issue, current_user) return issue end diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index ea1690f3e38..d2eb46ac41b 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -4,7 +4,8 @@ module Issues @request = params.delete(:request) @api = params.delete(:api) - @issue = project.issues.new + issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions) + @issue = BuildService.new(project, current_user, issue_attributes).execute create(@issue) end @@ -18,6 +19,17 @@ module Issues notification_service.new_issue(issuable, current_user) todo_service.new_issue(issuable, current_user) user_agent_detail_service.create + + if merge_request_for_resolving_discussions.try(:discussions_can_be_resolved_by?, current_user) + resolve_discussions_in_merge_request(issuable) + end + end + + def resolve_discussions_in_merge_request(issue) + Discussions::ResolveService.new(project, current_user, + merge_request: merge_request_for_resolving_discussions, + follow_up_issue: issue). + execute(merge_request_for_resolving_discussions.resolvable_discussions) end private diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index a2111b3806b..78cbf94ec69 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -10,7 +10,7 @@ module Issues end if issue.previous_changes.include?('title') || - issue.previous_changes.include?('description') + issue.previous_changes.include?('description') todo_service.update_issue(issue, current_user) end diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb index d622f9edd33..cf4f7606c94 100644 --- a/app/services/labels/find_or_create_service.rb +++ b/app/services/labels/find_or_create_service.rb @@ -22,9 +22,14 @@ module Labels ).execute(skip_authorization: skip_authorization) end + # Only creates the label if current_user can do so, if the label does not exist + # and the user can not create the label, nil is returned def find_or_create_label new_label = available_labels.find_by(title: title) - new_label ||= project.labels.create(params) + + if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project)) + new_label = project.labels.create(params) + end new_label end diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb index d572a928a42..12a8415d9a5 100644 --- a/app/services/merge_requests/add_todo_when_build_fails_service.rb +++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb @@ -1,13 +1,18 @@ module MergeRequests class AddTodoWhenBuildFailsService < MergeRequests::BaseService # Adds a todo to the parent merge_request when a CI build fails + # def execute(commit_status) + return if commit_status.allow_failure? + commit_status_merge_requests(commit_status) do |merge_request| todo_service.merge_request_build_failed(merge_request) end end - # Closes any pending build failed todos for the parent MRs when a build is retried + # Closes any pending build failed todos for the parent MRs when a + # build is retried + # def close(commit_status) commit_status_merge_requests(commit_status) do |merge_request| todo_service.merge_request_build_retried(merge_request) diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 58f69a41e14..5a53b973059 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -38,24 +38,18 @@ module MergeRequests private - def filter_params - super(:merge_request) - end - - def merge_requests_for(branch) - origin_merge_requests = @project.origin_merge_requests - .opened.where(source_branch: branch).to_a - - fork_merge_requests = @project.fork_merge_requests - .opened.where(source_branch: branch).to_a - - (origin_merge_requests + fork_merge_requests) - .uniq.select(&:source_project) + # Returns all origin and fork merge requests from `@project` satisfying passed arguments. + def merge_requests_for(source_branch, mr_states: [:opened]) + MergeRequest + .with_state(mr_states) + .where(source_branch: source_branch, source_project_id: @project.id) + .preload(:source_project) # we don't need a #includes since we're just preloading for the #select + .select(&:source_project) end def pipeline_merge_requests(pipeline) merge_requests_for(pipeline.ref).each do |merge_request| - next unless pipeline == merge_request.pipeline + next unless pipeline == merge_request.head_pipeline yield merge_request end @@ -63,7 +57,7 @@ module MergeRequests def commit_status_merge_requests(commit_status) merge_requests_for(commit_status.ref).each do |merge_request| - pipeline = merge_request.pipeline + pipeline = merge_request.head_pipeline next unless pipeline next unless pipeline.sha == commit_status.sha diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index f415244068b..6a7393a9921 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -42,17 +42,17 @@ module MergeRequests end if merge_request.source_project == merge_request.target_project && - merge_request.target_branch == merge_request.source_branch + merge_request.target_branch == merge_request.source_branch messages << 'You must select different branches' end # See if source and target branches exist - unless merge_request.source_project.commit(merge_request.source_branch) + if merge_request.source_branch.present? && !merge_request.source_project.commit(merge_request.source_branch) messages << "Source branch \"#{merge_request.source_branch}\" does not exist" end - unless merge_request.target_project.commit(merge_request.target_branch) + if merge_request.target_branch.present? && !merge_request.target_project.commit(merge_request.target_branch) messages << "Target branch \"#{merge_request.target_branch}\" does not exist" end @@ -81,7 +81,7 @@ module MergeRequests commit = commits.first merge_request.title = commit.title merge_request.description ||= commit.description.try(:strip) - elsif iid && (issue = merge_request.target_project.get_issue(iid)) && !issue.try(:confidential?) + elsif iid && issue = merge_request.target_project.get_issue(iid, current_user) case issue when Issue merge_request.title = "Resolve \"#{issue.title}\"" diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb index dc159de0058..5616edf8b4a 100644 --- a/app/services/merge_requests/merge_when_build_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb @@ -1,5 +1,5 @@ module MergeRequests - class MergeWhenBuildSucceedsService < MergeRequests::BaseService + class MergeWhenPipelineSucceedsService < MergeRequests::BaseService # Marks the passed `merge_request` to be merged when the build succeeds or # updates the params for the automatic merge def execute(merge_request) diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 22596b4014a..b4bfb0e5e8c 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -21,6 +21,7 @@ module MergeRequests end comment_mr_with_commits + mark_mr_as_wip_from_commits execute_mr_web_hooks true @@ -41,7 +42,7 @@ module MergeRequests commit_ids.include?(merge_request.diff_head_sha) end - merge_requests.uniq.select(&:source_project).each do |merge_request| + filter_merge_requests(merge_requests).each do |merge_request| MergeRequests::PostMergeService. new(merge_request.target_project, @current_user). execute(merge_request) @@ -55,15 +56,19 @@ module MergeRequests # Refresh merge request diff if we push to source or target branch of merge request # Note: we should update merge requests from forks too def reload_merge_requests - merge_requests = @project.merge_requests.opened.by_branch(@branch_name).to_a - merge_requests += fork_merge_requests.by_branch(@branch_name).to_a - merge_requests = filter_merge_requests(merge_requests) + merge_requests = @project.merge_requests.opened. + by_source_or_target_branch(@branch_name).to_a - merge_requests.each do |merge_request| + # Fork merge requests + merge_requests += MergeRequest.opened + .where(source_branch: @branch_name, source_project: @project) + .where.not(target_project: @project).to_a + + filter_merge_requests(merge_requests).each do |merge_request| if merge_request.source_branch == @branch_name || force_push? merge_request.reload_diff else - mr_commit_ids = merge_request.commits.map(&:id) + mr_commit_ids = merge_request.commits_sha push_commit_ids = @commits.map(&:id) matches = mr_commit_ids & push_commit_ids merge_request.reload_diff if matches.any? @@ -123,7 +128,7 @@ module MergeRequests return unless @commits.present? merge_requests_for_source_branch.each do |merge_request| - mr_commit_ids = Set.new(merge_request.commits.map(&:id)) + mr_commit_ids = Set.new(merge_request.commits_sha) new_commits, existing_commits = @commits.partition do |commit| mr_commit_ids.include?(commit.id) @@ -135,6 +140,24 @@ module MergeRequests end end + def mark_mr_as_wip_from_commits + return unless @commits.present? + + merge_requests_for_source_branch.each do |merge_request| + wip_commit = @commits.detect(&:work_in_progress?) + + if wip_commit && !merge_request.work_in_progress? + merge_request.update(title: merge_request.wip_title) + SystemNoteService.add_merge_request_wip_from_commit( + merge_request, + merge_request.project, + @current_user, + wip_commit + ) + end + end + end + # Call merge request webhook with update branches def execute_mr_web_hooks merge_requests_for_source_branch.each do |merge_request| @@ -155,15 +178,7 @@ module MergeRequests end def merge_requests_for_source_branch - @source_merge_requests ||= begin - merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a - merge_requests += fork_merge_requests.where(source_branch: @branch_name).to_a - filter_merge_requests(merge_requests) - end - end - - def fork_merge_requests - @fork_merge_requests ||= @project.fork_merge_requests.opened + @source_merge_requests ||= merge_requests_for(@branch_name) end def branch_added? diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index a37cc3fdf21..3cb9aae83f6 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -1,7 +1,3 @@ -require_relative 'base_service' -require_relative 'reopen_service' -require_relative 'close_service' - module MergeRequests class UpdateService < MergeRequests::BaseService def execute(merge_request) @@ -11,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 @@ -29,7 +27,7 @@ module MergeRequests end if merge_request.previous_changes.include?('title') || - merge_request.previous_changes.include?('description') + merge_request.previous_changes.include?('description') todo_service.update_merge_request(merge_request, current_user) end @@ -73,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 723cc0e6834..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,27 +21,33 @@ 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? note.note = content end - if !only_commands && note.save + note.run_after_commit do # Finish the harder work in the background - NewNoteWorker.perform_in(2.seconds, note.id, params) + NewNoteWorker.perform_async(note.id) + end + + if !only_commands && note.save todo_service.new_note(note, current_user) end - if command_params && command_params.any? + if command_params.present? slash_commands_service.execute(command_params, note) # We must add the error after we call #save because errors are reset # when #save is called if only_commands - note.errors.add(:commands_only, 'Your commands have been executed!') + note.errors.add(:commands_only, 'Commands applied') end + + note.commands_changes = command_params.keys end note diff --git a/app/services/notes/delete_service.rb b/app/services/notes/delete_service.rb index 7f1b30ec84e..a673e8e9dde 100644 --- a/app/services/notes/delete_service.rb +++ b/app/services/notes/delete_service.rb @@ -2,7 +2,6 @@ module Notes class DeleteService < BaseService def execute(note) note.destroy - note.reset_events_cache end end end 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/notes/update_service.rb b/app/services/notes/update_service.rb index 1361b1e0300..75a4b3ed826 100644 --- a/app/services/notes/update_service.rb +++ b/app/services/notes/update_service.rb @@ -5,7 +5,6 @@ module Notes note.update_attributes(params.merge(updated_by: current_user)) note.create_new_cross_references!(current_user) - note.reset_events_cache if note.previous_changes.include?('note') TodoService.new.update_note(note, current_user) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 6697840cc26..c3b61e68eab 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -75,7 +75,7 @@ class NotificationService # * watchers of the issue's labels # def relabeled_issue(issue, added_labels, current_user) - relabeled_resource_email(issue, added_labels, current_user, :relabeled_issue_email) + relabeled_resource_email(issue, issue.project, added_labels, current_user, :relabeled_issue_email) end # When create a merge request we should send an email to: @@ -118,7 +118,7 @@ class NotificationService # * watchers of the mr's labels # def relabeled_merge_request(merge_request, added_labels, current_user) - relabeled_resource_email(merge_request, added_labels, current_user, :relabeled_merge_request_email) + relabeled_resource_email(merge_request, merge_request.target_project, added_labels, current_user, :relabeled_merge_request_email) end def close_mr(merge_request, current_user) @@ -171,7 +171,6 @@ class NotificationService return true unless note.noteable_type.present? # ignore gitlab service messages - return true if note.note.start_with?('Status changed to closed') return true if note.cross_reference? && note.system? target = note.noteable @@ -205,7 +204,7 @@ class NotificationService recipients = reject_muted_users(recipients, note.project) - recipients = add_subscribed_users(recipients, note.noteable) + recipients = add_subscribed_users(recipients, note.project, note.noteable) recipients = reject_unsubscribed_users(recipients, note.noteable) recipients = reject_users_without_access(recipients, note.noteable) @@ -393,7 +392,7 @@ class NotificationService ) end - # Build a list of users based on project notifcation settings + # Build a list of users based on project notification settings def select_project_member_setting(project, global_setting, users_global_level_watch) users = notification_settings_for(project, :watch) @@ -505,17 +504,17 @@ class NotificationService end end - def add_subscribed_users(recipients, target) + def add_subscribed_users(recipients, project, target) return recipients unless target.respond_to? :subscribers - recipients + target.subscribers + recipients + target.subscribers(project) end - def add_labels_subscribers(recipients, target, labels: nil) + def add_labels_subscribers(recipients, project, target, labels: nil) return recipients unless target.respond_to? :labels (labels || target.labels).each do |label| - recipients += label.subscribers + recipients += label.subscribers(project) end recipients @@ -571,8 +570,8 @@ class NotificationService end end - def relabeled_resource_email(target, labels, current_user, method) - recipients = build_relabeled_recipients(target, current_user, labels: labels) + def relabeled_resource_email(target, project, labels, current_user, method) + recipients = build_relabeled_recipients(target, project, current_user, labels: labels) label_names = labels.map(&:name) recipients.each do |recipient| @@ -592,7 +591,10 @@ class NotificationService custom_action = build_custom_key(action, target) recipients = target.participants(current_user) - recipients = add_project_watchers(recipients, project) + + unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) + recipients = add_project_watchers(recipients, project) + end recipients = add_custom_notifications(recipients, project, custom_action) recipients = reject_mention_users(recipients, project) @@ -608,10 +610,10 @@ class NotificationService end recipients = reject_muted_users(recipients, project) - recipients = add_subscribed_users(recipients, target) + recipients = add_subscribed_users(recipients, project, target) if [:new_issue, :new_merge_request].include?(custom_action) - recipients = add_labels_subscribers(recipients, target) + recipients = add_labels_subscribers(recipients, project, target) end recipients = reject_unsubscribed_users(recipients, target) @@ -622,8 +624,8 @@ class NotificationService recipients.uniq end - def build_relabeled_recipients(target, current_user, labels:) - recipients = add_labels_subscribers([], target, labels: labels) + def build_relabeled_recipients(target, project, current_user, labels:) + recipients = add_labels_subscribers([], project, target, labels: labels) recipients = reject_unsubscribed_users(recipients, target) recipients = reject_users_without_access(recipients, target) recipients.delete(current_user) diff --git a/app/services/oauth2/access_token_validation_service.rb b/app/services/oauth2/access_token_validation_service.rb deleted file mode 100644 index 264fdccde8f..00000000000 --- a/app/services/oauth2/access_token_validation_service.rb +++ /dev/null @@ -1,42 +0,0 @@ -module Oauth2::AccessTokenValidationService - # Results: - VALID = :valid - EXPIRED = :expired - REVOKED = :revoked - INSUFFICIENT_SCOPE = :insufficient_scope - - class << self - def validate(token, scopes: []) - if token.expired? - return EXPIRED - - elsif token.revoked? - return REVOKED - - elsif !self.sufficient_scope?(token, scopes) - return INSUFFICIENT_SCOPE - - else - return VALID - end - end - - protected - - # True if the token's scope is a superset of required scopes, - # or the required scopes is empty. - def sufficient_scope?(token, scopes) - if scopes.blank? - # if no any scopes required, the scopes of token is sufficient. - return true - else - # If there are scopes required, then check whether - # the set of authorized scopes is a superset of the set of required scopes - required_scopes = Set.new(scopes) - authorized_scopes = Set.new(token.scopes) - - return authorized_scopes >= required_scopes - end - end - end -end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 15d7918e7fd..159f46cd465 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -95,7 +95,7 @@ module Projects unless @project.gitlab_project_import? @project.create_wiki unless skip_wiki? - @project.build_missing_services + create_services_from_active_templates(@project) @project.create_labels end @@ -106,6 +106,8 @@ module Projects unless @project.group || @project.gitlab_project_import? @project.team << [current_user, :master, current_user] end + + @project.group.refresh_members_authorized_projects if @project.group end def skip_wiki? @@ -135,5 +137,12 @@ module Projects @project end + + def create_services_from_active_templates(project) + Service.where(template: true, active: true).each do |template| + service = Service.build_from_template(project.id, template) + service.save! + end + end end end diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index d7221fe993c..cd230528743 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -4,15 +4,6 @@ module Projects class Error < StandardError; end - ALLOWED_TYPES = [ - 'bitbucket', - 'fogbugz', - 'gitlab', - 'github', - 'google_code', - 'gitlab_project' - ] - def execute add_repository_to_project unless project.gitlab_project_import? @@ -64,14 +55,11 @@ module Projects end def has_importer? - ALLOWED_TYPES.include?(project.import_type) + Gitlab::ImportSources.importer_names.include?(project.import_type) end def importer - return Gitlab::ImportExport::Importer.new(project) if @project.gitlab_project_import? - - class_name = "Gitlab::#{project.import_type.camelize}Import::Importer" - class_name.constantize.new(project) + Gitlab::ImportSources.importer(project.import_type).new(project) end def unknown_url? diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb index d38328403c1..96c363c8d1a 100644 --- a/app/services/projects/participants_service.rb +++ b/app/services/projects/participants_service.rb @@ -1,7 +1,7 @@ module Projects class ParticipantsService < BaseService attr_reader :noteable - + def execute(noteable) @noteable = noteable @@ -15,7 +15,8 @@ module Projects [{ name: noteable.author.name, - username: noteable.author.username + username: noteable.author.username, + avatar_url: noteable.author.avatar_url }] end @@ -28,14 +29,14 @@ module Projects def sorted(users) users.uniq.to_a.compact.sort_by(&:username).map do |user| - { username: user.username, name: user.name } + { username: user.username, name: user.name, avatar_url: user.avatar_url } end end def groups current_user.authorized_groups.sort_by(&:path).map do |group| count = group.users.count - { username: group.path, name: group.name, count: count } + { username: group.path, name: group.name, count: count, avatar_url: group.avatar_url } end end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 28470f59807..34ec575e808 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -61,9 +61,6 @@ module Projects # Move missing group labels to project Labels::TransferService.new(current_user, old_group, project).execute - # clear project cached events - project.reset_events_cache - # Move uploads Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path) diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 921ca6748d3..842e23eb6b6 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -6,10 +6,10 @@ module Projects if new_visibility && new_visibility.to_i != project.visibility_level unless can?(current_user, :change_visibility_level, project) && - Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) + Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) deny_visibility_level(project, new_visibility) - return project + return error('Visibility level unallowed') end end @@ -23,6 +23,10 @@ module Projects if project.previous_changes.include?('path') project.rename_repo end + + success + else + error('Project could not be updated') end end end diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index 5a81194a5f4..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 @@ -193,7 +205,7 @@ module SlashCommands desc 'Subscribe' condition do issuable.persisted? && - !issuable.subscribed?(current_user) + !issuable.subscribed?(current_user, project) end command :subscribe do @updates[:subscription_event] = 'subscribe' @@ -202,7 +214,7 @@ module SlashCommands desc 'Unsubscribe' condition do issuable.persisted? && - issuable.subscribed?(current_user) + issuable.subscribed?(current_user, project) end command :unsubscribe do @updates[:subscription_event] = 'unsubscribe' @@ -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 1ce66d50368..a11bca00687 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -21,7 +21,7 @@ module SystemNoteService total_count = new_commits.length + existing_commits.length commits_text = "#{total_count} commit".pluralize(total_count) - body = "Added #{commits_text}:\n\n" + body = "added #{commits_text}\n\n" body << existing_commit_summary(noteable, existing_commits, oldrev) body << new_commit_summary(new_commits).join("\n") body << "\n\n[Compare with previous version](#{diff_comparison_url(noteable, project, oldrev)})" @@ -38,13 +38,13 @@ module SystemNoteService # # Example Note text: # - # "Assignee removed" + # "removed assignee" # - # "Reassigned to @rspeicher" + # "assigned to @rspeicher" # # Returns the created Note object def change_assignee(noteable, project, author, assignee) - body = assignee.nil? ? 'Assignee removed' : "Reassigned to #{assignee.to_reference}" + body = assignee.nil? ? 'removed assignee' : "assigned to #{assignee.to_reference}" create_note(noteable: noteable, project: project, author: author, note: body) end @@ -59,11 +59,11 @@ module SystemNoteService # # Example Note text: # - # "Added ~1 and removed ~2 ~3 labels" + # "added ~1 and removed ~2 ~3 labels" # - # "Added ~4 label" + # "added ~4 label" # - # "Removed ~5 label" + # "removed ~5 label" # # Returns the created Note object def change_label(noteable, project, author, added_labels, removed_labels) @@ -85,7 +85,6 @@ module SystemNoteService end body << ' ' << 'label'.pluralize(labels_count) - body = body.capitalize create_note(noteable: noteable, project: project, author: author, note: body) end @@ -99,14 +98,64 @@ module SystemNoteService # # Example Note text: # - # "Milestone removed" + # "removed milestone" # - # "Miletone changed to 7.11" + # "changed milestone to 7.11" # # Returns the created Note object def change_milestone(noteable, project, author, milestone) - body = 'Milestone ' - body += milestone.nil? ? 'removed' : "changed to #{milestone.to_reference(project)}" + body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project)}" + + 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 @@ -121,50 +170,64 @@ module SystemNoteService # # Example Note text: # - # "Status changed to merged" + # "merged" # - # "Status changed to closed by bc17db76" + # "closed via bc17db76" # # Returns the created Note object def change_status(noteable, project, author, status, source) - body = "Status changed to #{status}" - body << " by #{source.gfm_reference(project)}" if source + body = status.dup + body << " via #{source.gfm_reference(project)}" if source create_note(noteable: noteable, project: project, author: author, note: body) end - # Called when 'merge when build succeeds' is executed + # Called when 'merge when pipeline succeeds' is executed def merge_when_build_succeeds(noteable, project, author, last_commit) - body = "Enabled an automatic merge when the build for #{last_commit.to_reference(project)} succeeds" + body = "enabled an automatic merge when the pipeline for #{last_commit.to_reference(project)} succeeds" create_note(noteable: noteable, project: project, author: author, note: body) end - # Called when 'merge when build succeeds' is canceled + # Called when 'merge when pipeline succeeds' is canceled def cancel_merge_when_build_succeeds(noteable, project, author) - body = 'Canceled the automatic merge' + body = 'canceled the automatic merge' create_note(noteable: noteable, project: project, author: author, note: body) end def remove_merge_request_wip(noteable, project, author) - body = 'Unmarked this merge request as a Work In Progress' + body = 'unmarked as a **Work In Progress**' create_note(noteable: noteable, project: project, author: author, note: body) end def add_merge_request_wip(noteable, project, author) - body = 'Marked this merge request as a **Work In Progress**' + body = 'marked as a **Work In Progress**' + + 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" + body = "resolved all discussions" create_note(noteable: merge_request, project: project, author: author, note: body) end + def discussion_continued_in_issue(discussion, project, author, issue) + body = "Added #{issue.to_reference} to continue this discussion" + note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body) + note_attributes[:type] = note_attributes.delete(:note_type) + + create_note(note_attributes) + end + # Called when the title of a Noteable is changed # # noteable - Noteable object that responds to `title` @@ -174,7 +237,7 @@ module SystemNoteService # # Example Note text: # - # "Title changed from **Old** to **New**" + # "changed title from **Old** to **New**" # # Returns the created Note object def change_title(noteable, project, author, old_title) @@ -185,7 +248,7 @@ module SystemNoteService marked_old_title = Gitlab::Diff::InlineDiffMarker.new(old_title).mark(old_diffs, mode: :deletion, markdown: true) marked_new_title = Gitlab::Diff::InlineDiffMarker.new(new_title).mark(new_diffs, mode: :addition, markdown: true) - body = "Changed title: **#{marked_old_title}** → **#{marked_new_title}**" + body = "changed title from **#{marked_old_title}** to **#{marked_new_title}**" create_note(noteable: noteable, project: project, author: author, note: body) end @@ -197,11 +260,11 @@ module SystemNoteService # # Example Note text: # - # "Made the issue confidential" + # "made the issue confidential" # # Returns the created Note object def change_issue_confidentiality(issue, project, author) - body = issue.confidential ? 'Made the issue confidential' : 'Made the issue visible' + body = issue.confidential ? 'made the issue confidential' : 'made the issue visible to everyone' create_note(noteable: issue, project: project, author: author, note: body) end @@ -216,11 +279,11 @@ module SystemNoteService # # Example Note text: # - # "Target branch changed from `Old` to `New`" + # "changed target branch from `Old` to `New`" # # Returns the created Note object def change_branch(noteable, project, author, branch_type, old_branch, new_branch) - body = "#{branch_type} branch changed from `#{old_branch}` to `#{new_branch}`".capitalize + body = "changed #{branch_type} branch from `#{old_branch}` to `#{new_branch}`" create_note(noteable: noteable, project: project, author: author, note: body) end @@ -235,7 +298,7 @@ module SystemNoteService # # Example Note text: # - # "Restored target branch `feature`" + # "restored target branch `feature`" # # Returns the created Note object def change_branch_presence(noteable, project, author, branch_type, branch, presence) @@ -246,18 +309,18 @@ module SystemNoteService 'deleted' end - body = "#{verb} #{branch_type} branch `#{branch}`".capitalize + body = "#{verb} #{branch_type} branch `#{branch}`" create_note(noteable: noteable, project: project, author: author, note: body) end # Called when a branch is created from the 'new branch' button on a issue # Example note text: # - # "Started branch `201-issue-branch-button`" + # "created branch `201-issue-branch-button`" def new_issue_branch(issue, project, author, branch) link = url_helpers.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) - body = "Started branch [`#{branch}`](#{link})" + body = "created branch [`#{branch}`](#{link})" create_note(noteable: issue, project: project, author: author, note: body) end @@ -269,11 +332,11 @@ module SystemNoteService # # Example Note text: # - # "Mentioned in #1" + # "mentioned in #1" # - # "Mentioned in !2" + # "mentioned in !2" # - # "Mentioned in 54f7727c" + # "mentioned in 54f7727c" # # See cross_reference_note_content. # @@ -303,12 +366,12 @@ module SystemNoteService end def cross_reference?(note_text) - note_text.start_with?(cross_reference_note_prefix) + note_text =~ /\A#{cross_reference_note_prefix}/i end # Check if a cross-reference is disallowed # - # This method prevents adding a "Mentioned in !1" note on every single commit + # This method prevents adding a "mentioned in !1" note on every single commit # in a merge request. Additionally, it prevents the creation of references to # external issues (which would fail). # @@ -370,12 +433,12 @@ module SystemNoteService # # Example Note text: # - # "Soandso marked the task Whatever as completed." + # "marked the task Whatever as completed." # # Returns the created Note object def change_task_status(noteable, project, author, new_task) status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE - body = "Marked the task **#{new_task.source}** as #{status_label}" + body = "marked the task **#{new_task.source}** as #{status_label}" create_note(noteable: noteable, project: project, author: author, note: body) end @@ -388,7 +451,7 @@ module SystemNoteService # # Example Note text: # - # "Moved to some_namespace/project_new#11" + # "moved to some_namespace/project_new#11" # # Returns the created Note object def noteable_moved(noteable, project, noteable_ref, author, direction:) @@ -397,7 +460,7 @@ module SystemNoteService end cross_reference = noteable_ref.to_reference(project) - body = "Moved #{direction} #{cross_reference}" + body = "moved #{direction} #{cross_reference}" create_note(noteable: noteable, project: project, author: author, note: body) end @@ -405,10 +468,12 @@ module SystemNoteService def notes_for_mentioner(mentioner, noteable, notes) if mentioner.is_a?(Commit) - notes.where('note LIKE ?', "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}") + text = "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}" + notes.where('(note LIKE ? OR note LIKE ?)', text, text.capitalize) else gfm_reference = mentioner.gfm_reference(noteable.project) - notes.where(note: cross_reference_note_content(gfm_reference)) + text = cross_reference_note_content(gfm_reference) + notes.where(note: [text, text.capitalize]) end end @@ -417,7 +482,7 @@ module SystemNoteService end def cross_reference_note_prefix - 'Mentioned in ' + 'mentioned in ' end def cross_reference_note_content(gfm_reference) 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/update_release_service.rb b/app/services/update_release_service.rb index 0ee1ff2d7d9..b7c36651968 100644 --- a/app/services/update_release_service.rb +++ b/app/services/update_release_service.rb @@ -1,5 +1,3 @@ -require_relative 'base_service' - class UpdateReleaseService < BaseService def execute(tag_name, release_description) repository = project.repository diff --git a/app/services/user_project_access_changed_service.rb b/app/services/user_project_access_changed_service.rb new file mode 100644 index 00000000000..2469b4f0d7c --- /dev/null +++ b/app/services/user_project_access_changed_service.rb @@ -0,0 +1,9 @@ +class UserProjectAccessChangedService + def initialize(user_ids) + @user_ids = Array.wrap(user_ids) + end + + def execute + AuthorizedProjectsWorker.bulk_perform_async(@user_ids.map { |id| [id] }) + end +end diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb new file mode 100644 index 00000000000..2d211d5ebbe --- /dev/null +++ b/app/services/users/refresh_authorized_projects_service.rb @@ -0,0 +1,127 @@ +module Users + # Service for refreshing the authorized projects of a user. + # + # This particular service class can not be used to update data for the same + # user concurrently. Doing so could lead to an incorrect state. To ensure this + # doesn't happen a caller must synchronize access (e.g. using + # `Gitlab::ExclusiveLease`). + # + # Usage: + # + # user = User.find_by(username: 'alice') + # service = Users::RefreshAuthorizedProjectsService.new(some_user) + # service.execute + class RefreshAuthorizedProjectsService + attr_reader :user + + LEASE_TIMEOUT = 1.minute.to_i + + # user - The User for which to refresh the authorized projects. + def initialize(user) + @user = user + + # We need an up to date User object that has access to all relations that + # may have been created earlier. The only way to ensure this is to reload + # the User object. + user.reload + end + + 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 + + remove = current.each_with_object([]) do |(project_id, row), array| + # rows not in the new list or with a different access level should be + # removed. + if !fresh[project_id] || fresh[project_id] != row.access_level + array << row.project_id + end + end + + add = fresh.each_with_object([]) do |(project_id, level), array| + # rows not in the old list or with a different access level should be + # added. + if !current[project_id] || current[project_id].access_level != level + array << [user.id, project_id, level] + end + end + + update_authorizations(remove, add) + end + + # Updates the list of authorizations for the current user. + # + # remove - The IDs of the authorization rows to remove. + # add - Rows to insert in the form `[user id, project id, access level]` + def update_authorizations(remove = [], add = []) + return if remove.empty? && add.empty? && user.authorized_projects_populated + + User.transaction do + user.remove_project_authorizations(remove) unless remove.empty? + ProjectAuthorization.insert_authorizations(add) unless add.empty? + user.set_authorized_projects_column + end + + # Since we batch insert authorization rows, Rails' associations may get + # out of sync. As such we force a reload of the User object. + user.reload + end + + def fresh_access_levels_per_project + fresh_authorizations.each_with_object({}) do |row, hash| + hash[row.project_id] = row.access_level + end + end + + def current_authorizations_per_project + current_authorizations.each_with_object({}) do |row, hash| + hash[row.project_id] = row + end + end + + def current_authorizations + user.project_authorizations.select(:project_id, :access_level) + end + + def fresh_authorizations + ProjectAuthorization. + unscoped. + select('project_id, MAX(access_level) AS access_level'). + from("(#{project_authorizations_union.to_sql}) #{ProjectAuthorization.table_name}"). + group(:project_id) + end + + private + + # Returns a union query of projects that the user is authorized to access + def project_authorizations_union + relations = [ + user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"), + user.groups_projects.select_for_project_authorization, + user.projects.select_for_project_authorization, + user.groups.joins(:shared_projects).select_for_project_authorization + ] + + Gitlab::SQL::Union.new(relations) + end + end +end diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb index b6c52ddac7a..86f317dcd18 100644 --- a/app/uploaders/artifact_uploader.rb +++ b/app/uploaders/artifact_uploader.rb @@ -1,4 +1,4 @@ -class ArtifactUploader < CarrierWave::Uploader::Base +class ArtifactUploader < GitlabUploader storage :file attr_accessor :build, :field @@ -38,12 +38,4 @@ class ArtifactUploader < CarrierWave::Uploader::Base def exists? file.try(:exists?) end - - def move_to_cache - true - end - - def move_to_store - true - end end diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index fb3b5dfecd0..cfcb877cc3e 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -1,4 +1,4 @@ -class AttachmentUploader < CarrierWave::Uploader::Base +class AttachmentUploader < GitlabUploader include UploaderHelper storage :file diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb index 71ff14a3f20..265cea2d2c6 100644 --- a/app/uploaders/avatar_uploader.rb +++ b/app/uploaders/avatar_uploader.rb @@ -1,19 +1,24 @@ -class AvatarUploader < CarrierWave::Uploader::Base +class AvatarUploader < GitlabUploader include UploaderHelper storage :file - after :store, :reset_events_cache - def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end - def reset_events_cache(file) - model.reset_events_cache if model.is_a?(User) - end - def exists? model.avatar.file && model.avatar.file.exists? end + + # We set move_to_store and move_to_cache to 'false' to prevent stealing + # the avatar file from a project when forking it. + # https://gitlab.com/gitlab-org/gitlab-ce/issues/26158 + def move_to_store + false + end + + def move_to_cache + false + end end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 3ac6030c21c..47bef7cd1e4 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -1,4 +1,4 @@ -class FileUploader < CarrierWave::Uploader::Base +class FileUploader < GitlabUploader include UploaderHelper MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)} diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb new file mode 100644 index 00000000000..02d7c601d6c --- /dev/null +++ b/app/uploaders/gitlab_uploader.rb @@ -0,0 +1,11 @@ +class GitlabUploader < CarrierWave::Uploader::Base + # Reduce disk IO + def move_to_cache + true + end + + # Reduce disk IO + def move_to_store + true + end +end diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb index 4f356dd663e..faab539b8e0 100644 --- a/app/uploaders/lfs_object_uploader.rb +++ b/app/uploaders/lfs_object_uploader.rb @@ -1,4 +1,4 @@ -class LfsObjectUploader < CarrierWave::Uploader::Base +class LfsObjectUploader < GitlabUploader storage :file def store_dir @@ -9,14 +9,6 @@ class LfsObjectUploader < CarrierWave::Uploader::Base "#{Gitlab.config.lfs.storage_path}/tmp/cache" end - def move_to_cache - true - end - - def move_to_store - true - end - def exists? file.try(:exists?) end diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb index b10ad71d052..fbaea2744a3 100644 --- a/app/uploaders/uploader_helper.rb +++ b/app/uploaders/uploader_helper.rb @@ -1,6 +1,6 @@ # Extra methods for uploader module UploaderHelper - IMAGE_EXT = %w[png jpg jpeg gif bmp tiff] + IMAGE_EXT = %w[png jpg jpeg gif bmp tiff svg] # We recommend using the .mp4 format over .mov. Videos in .mov format can # still be used but you really need to make sure they are served with the # proper MIME type video/mp4 and not video/quicktime or your videos won't play diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb index 2821ecf0a88..eb3ed31b65b 100644 --- a/app/validators/namespace_validator.rb +++ b/app/validators/namespace_validator.rb @@ -35,8 +35,22 @@ class NamespaceValidator < ActiveModel::EachValidator users ].freeze + def self.valid?(value) + !reserved?(value) && follow_format?(value) + end + + def self.reserved?(value) + RESERVED.include?(value) + end + + def self.follow_format?(value) + value =~ Gitlab::Regex.namespace_regex + end + + delegate :reserved?, :follow_format?, to: :class + def validate_each(record, attribute, value) - unless value =~ Gitlab::Regex.namespace_regex + unless follow_format?(value) record.errors.add(attribute, Gitlab::Regex.namespace_regex_message) end @@ -44,10 +58,4 @@ class NamespaceValidator < ActiveModel::EachValidator record.errors.add(attribute, "#{value} is a reserved name") end end - - private - - def reserved?(value) - RESERVED.include?(value) - end end diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb new file mode 100644 index 00000000000..36279daa743 --- /dev/null +++ b/app/validators/project_path_validator.rb @@ -0,0 +1,37 @@ +# ProjectPathValidator +# +# Custom validator for GitLab project path values. +# +# Values are checked for formatting and exclusion from a list of reserved path +# names. +class ProjectPathValidator < ActiveModel::EachValidator + # All project routes with wildcard argument must be listed here. + # Otherwise it can lead to routing issues when route considered as project name. + # + # Example: + # /group/project/tree/deploy_keys + # + # without tree as reserved name routing can match 'group/project' as group name, + # 'tree' as project name and 'deploy_keys' as route. + # + RESERVED = (NamespaceValidator::RESERVED - + %w[dashboard help ci admin search notes services assets profile public] + + %w[tree commits wikis new edit create update logs_tree + preview blob blame raw files create_dir find_file]).freeze + + def self.valid?(value) + !reserved?(value) + end + + def self.reserved?(value) + RESERVED.include?(value) + end + + delegate :reserved?, to: :class + + def validate_each(record, attribute, value) + if reserved?(value) + record.errors.add(attribute, "#{value} is a reserved name") + end + end +end diff --git a/app/views/abuse_report_mailer/notify.html.haml b/app/views/abuse_report_mailer/notify.html.haml index 2741eb44357..d50b4071745 100644 --- a/app/views/abuse_report_mailer/notify.html.haml +++ b/app/views/abuse_report_mailer/notify.html.haml @@ -1,7 +1,7 @@ %p - #{link_to @abuse_report.user.name, user_url(@abuse_report.user)} - (@#{@abuse_report.user.username}) was reported for abuse by - #{link_to @abuse_report.reporter.name, user_url(@abuse_report.reporter)} + #{link_to @abuse_report.user.name, user_url(@abuse_report.user)} + (@#{@abuse_report.user.username}) was reported for abuse by + #{link_to @abuse_report.reporter.name, user_url(@abuse_report.reporter)} (@#{@abuse_report.reporter.username}). %blockquote diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml index 7bbc75db9ff..c4b748d0ab8 100644 --- a/app/views/admin/abuse_reports/index.html.haml +++ b/app/views/admin/abuse_reports/index.html.haml @@ -4,7 +4,7 @@ .abuse-reports - if @abuse_reports.present? .table-holder - %table.table + %table.table.responsive-table %thead.hidden-sm.hidden-xs %tr %th User @@ -13,8 +13,6 @@ %th Action = render @abuse_reports - else - .no-reports - %span.pull-left - There are no abuse reports! - .pull-left - = emoji_icon 'tada' + .empty-state + .text-center + %h4 There are no abuse reports! #{emoji_icon 'tada'} diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 450ec322f2c..558bbe07b16 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -22,9 +22,8 @@ .form-group = f.label :restricted_visibility_levels, class: 'control-label col-sm-2' .col-sm-10 - - data_attrs = { toggle: 'buttons' } - .btn-group{ data: data_attrs } - - restricted_level_checkboxes('restricted-visibility-help').each do |level| + - restricted_level_checkboxes('restricted-visibility-help').each do |level| + .checkbox = level %span.help-block#restricted-visibility-help Selected levels cannot be used by non-admin users for projects or snippets. @@ -32,10 +31,8 @@ .form-group = f.label :import_sources, class: 'control-label col-sm-2' .col-sm-10 - - data_attrs = { toggle: 'buttons' } - .btn-group{ data: data_attrs } - - import_sources_checkboxes('import-sources-help').each do |source| - = source + - import_sources_checkboxes('import-sources-help').each do |source| + .checkbox= source %span.help-block#import-sources-help Enabled sources for code import during project creation. OmniAuth must be configured for GitHub = link_to "(?)", help_page_path("integration/github") @@ -284,6 +281,31 @@ results in fewer but larger UDP packets being sent. %fieldset + %legend Background Jobs + %p + These settings require a restart to take effect. + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :sidekiq_throttling_enabled do + = f.check_box :sidekiq_throttling_enabled + Enable Sidekiq Job Throttling + .help-block + Limit the amount of resources slow running jobs are assigned. + .form-group + = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2' + .col-sm-10 + = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' } + .help-block + Choose which queues you wish to throttle. + .form-group + = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01' + .help-block + The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive. + + %fieldset %legend Spam and Anti-bot Protection .form-group .col-sm-offset-2.col-sm-10 @@ -299,7 +321,7 @@ = f.text_field :recaptcha_site_key, class: 'form-control' .help-block Generate site and private keys at - %a{ href: 'http://www.google.com/recaptcha', target: 'blank'} http://www.google.com/recaptcha + %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha .form-group = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2' @@ -320,7 +342,7 @@ = f.text_field :akismet_api_key, class: 'form-control' .help-block Generate API key at - %a{ href: 'http://www.akismet.com', target: 'blank'} http://www.akismet.com + %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com %fieldset %legend Abuse reports @@ -399,6 +421,23 @@ = link_to "Koding administration documentation", help_page_path("administration/integration/koding") %fieldset + %legend PlantUML + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :plantuml_enabled do + = f.check_box :plantuml_enabled + Enable PlantUML + .form-group + = f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080' + .help-block + Allow rendering of + = link_to "PlantUML", "http://plantuml.com" + diagrams in Asciidoc documents using an external PlantUML service. + + %fieldset %legend Usage statistics .form-group .col-sm-offset-2.col-sm-10 @@ -421,7 +460,16 @@ Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. - + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :html_emails_enabled do + = f.check_box :html_emails_enabled + Enable HTML emails + .help-block + By default GitLab sends emails in HTML and plain text formats so mail + clients can choose what format to use. Disable this option if you only + want to send emails in plain text format. %fieldset %legend Automatic Git repository housekeeping .form-group diff --git a/app/views/admin/applications/_delete_form.html.haml b/app/views/admin/applications/_delete_form.html.haml index 042971e1eed..82781f6716d 100644 --- a/app/views/admin/applications/_delete_form.html.haml +++ b/app/views/admin/applications/_delete_form.html.haml @@ -1,4 +1,4 @@ - submit_btn_css ||= 'btn btn-link btn-remove btn-sm' = form_tag admin_application_path(application) do - %input{:name => "_method", :type => "hidden", :value => "delete"}/ + %input{ :name => "_method", :type => "hidden", :value => "delete" }/ = submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml index 4aacbb8cd77..c689b26d6e6 100644 --- a/app/views/admin/applications/_form.html.haml +++ b/app/views/admin/applications/_form.html.haml @@ -18,6 +18,12 @@ Use %code= Doorkeeper.configuration.native_redirect_uri for local tests + + .form-group + = f.label :scopes, class: 'col-sm-2 control-label' + .col-sm-10 + = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes + .form-actions = f.submit 'Submit', class: "btn btn-save wide" = link_to "Cancel", admin_applications_path, class: "btn btn-default" diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml index f8cd98f0ec4..b3a3b4c1d45 100644 --- a/app/views/admin/applications/index.html.haml +++ b/app/views/admin/applications/index.html.haml @@ -15,7 +15,7 @@ %th %tbody.oauth-applications - @applications.each do |application| - %tr{:id => "application_#{application.id}"} + %tr{ :id => "application_#{application.id}" } %td= link_to application.name, admin_application_path(application) %td= application.redirect_uri %td= application.access_tokens.map(&:resource_owner_id).uniq.count diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml index 3eb9d61972b..14683cc66e9 100644 --- a/app/views/admin/applications/show.html.haml +++ b/app/views/admin/applications/show.html.haml @@ -2,8 +2,7 @@ %h3.page-title Application: #{@application.name} - -.table-holder +.table-holder.oauth-application-show %table.table %tr %td @@ -23,6 +22,9 @@ - @application.redirect_uri.split.each do |uri| %div %span.monospace= uri + + = render "shared/tokens/scopes_list", token: @application + .form-actions = link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide pull-left' = render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10' diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index 05855db963a..4f982a6e369 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -43,4 +43,4 @@ .panel.panel-default - %iframe{src: sidekiq_path, width: '100%', height: 970, style: "border: none"} + %iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: none" } diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml index c05538a393c..4f2ae081d7a 100644 --- a/app/views/admin/broadcast_messages/index.html.haml +++ b/app/views/admin/broadcast_messages/index.html.haml @@ -10,7 +10,7 @@ %br.clearfix --if @broadcast_messages.any? +- if @broadcast_messages.any? %table.table %thead %tr diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml index 26a8846b609..5e3f105d41f 100644 --- a/app/views/admin/builds/index.html.haml +++ b/app/views/admin/builds/index.html.haml @@ -14,5 +14,5 @@ .row-content-block.second-block #{(@scope || 'all').capitalize} builds - %ul.content-list.builds-content-list + %ul.content-list.builds-content-list.admin-builds-table = render "projects/builds/table", builds: @builds, admin: true diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml index ec40391a3e3..b5f96363230 100644 --- a/app/views/admin/dashboard/_head.html.haml +++ b/app/views/admin/dashboard/_head.html.haml @@ -8,7 +8,7 @@ %span Overview = nav_link(controller: [:admin, :projects]) do - = link_to admin_namespaces_projects_path, title: 'Projects' do + = link_to admin_projects_path, title: 'Projects' do %span Projects = nav_link(controller: :users) do diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 1db2150f336..5238623e936 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -113,15 +113,15 @@ %hr .row .col-sm-4 - .light-well + .light-well.well-centered %h4 Projects .data - = link_to admin_namespaces_projects_path do + = link_to admin_projects_path do %h1= number_with_delimiter(Project.cached_count) %hr = link_to('New Project', new_project_path, class: "btn btn-new") .col-sm-4 - .light-well + .light-well.well-centered %h4 Users .data = link_to admin_users_path do @@ -129,7 +129,7 @@ %hr = link_to 'New User', new_admin_user_path, class: "btn btn-new" .col-sm-4 - .light-well + .light-well.well-centered %h4 Groups .data = link_to admin_groups_path do @@ -143,7 +143,7 @@ %hr - @projects.each do |project| %p - = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated' + = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60' %span.light.pull-right #{time_ago_with_tooltip(project.created_at)} @@ -152,7 +152,7 @@ %hr - @users.each do |user| %p - = link_to [:admin, user], class: 'str-truncated' do + = link_to [:admin, user], class: 'str-truncated-60' do = user.name %span.light.pull-right #{time_ago_with_tooltip(user.created_at)} @@ -162,7 +162,7 @@ %hr - @groups.each do |group| %p - = link_to [:admin, group], class: 'str-truncated' do + = link_to [:admin, group], class: 'str-truncated-60' do = group.name %span.light.pull-right #{time_ago_with_tooltip(group.created_at)} diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 149593e7f46..7b71bb5b287 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -1,27 +1,34 @@ - page_title "Deploy Keys" -.panel.panel-default.prepend-top-default - .panel-heading - Public deploy keys (#{@deploy_keys.count}) - .controls - = link_to 'New Deploy Key', new_admin_deploy_key_path, class: "btn btn-new btn-sm" - - if @deploy_keys.any? - .table-holder - %table.table - %thead.panel-heading + +%h3.page-title.deploy-keys-title + Public deploy keys (#{@deploy_keys.count}) + .pull-right + = link_to 'New Deploy Key', new_admin_deploy_key_path, class: 'btn btn-new btn-sm btn-inverted' + +- if @deploy_keys.any? + .table-holder.deploy-keys-list + %table.table + %thead + %tr + %th.col-sm-2 Title + %th.col-sm-4 Fingerprint + %th.col-sm-2 Write access allowed + %th.col-sm-2 Added at + %th.col-sm-2 + %tbody + - @deploy_keys.each do |deploy_key| %tr - %th Title - %th Fingerprint - %th Added at - %th - %tbody - - @deploy_keys.each do |deploy_key| - %tr - %td - %strong= deploy_key.title - %td - %code.key-fingerprint= deploy_key.fingerprint - %td - %span.cgray - added #{time_ago_with_tooltip(deploy_key.created_at)} - %td - = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-sm btn-remove delete-key pull-right" + %td + %strong= deploy_key.title + %td + %code.key-fingerprint= deploy_key.fingerprint + %td + - if deploy_key.can_push? + Yes + - else + No + %td + %span.cgray + added #{time_ago_with_tooltip(deploy_key.created_at)} + %td + = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove delete-key pull-right' diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml index 5c410a695bf..a064efc231f 100644 --- a/app/views/admin/deploy_keys/new.html.haml +++ b/app/views/admin/deploy_keys/new.html.haml @@ -16,6 +16,14 @@ Paste a machine public key here. Read more about how to generate it = link_to "here", help_page_path("ssh/README") = f.text_area :key, class: "form-control thin_area", rows: 5 + .form-group + .control-label + .col-sm-10 + = f.label :can_push do + = f.check_box :can_push + %strong Write access allowed + %p.light.append-bottom-0 + Allow this key to push to repository as well? (Default only allows pull access.) .form-actions = f.submit 'Create', class: "btn-create btn" diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 817910f7ddf..589f4557b52 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -7,7 +7,7 @@ .col-sm-10 = render 'shared/choose_group_avatar_button', f: f - = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group + = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index 664bb417c6a..e3a77dfdf10 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -2,9 +2,12 @@ %li.group-row{ class: css_class } .controls - = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn' + = link_to 'Edit', admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn' = link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove' .stats + %span.badge + = storage_counter(group.storage_size) + %span = icon('bookmark') = number_with_delimiter(group.projects.count) @@ -13,14 +16,14 @@ = icon('users') = number_with_delimiter(group.users.count) - %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} + %span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group) } = visibility_level_icon(group.visibility_level, fw: false) .avatar-container.s40 = image_tag group_icon(group), class: "avatar s40 hidden-xs" .title = link_to [:admin, group], class: 'group-name' do - = group.name + = group.full_name - if group.description.present? .description diff --git a/app/views/admin/groups/edit.html.haml b/app/views/admin/groups/edit.html.haml index eb09a6328ed..c2b9807015d 100644 --- a/app/views/admin/groups/edit.html.haml +++ b/app/views/admin/groups/edit.html.haml @@ -1,4 +1,4 @@ - page_title "Edit", @group.name, "Groups" %h3.page-title Edit group: #{@group.name} %hr -= render 'form' += render 'form', visibility_level: @group.visibility_level diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 794f910a61f..07775247cfd 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -27,6 +27,8 @@ = sort_title_recently_updated = link_to admin_groups_path(sort: sort_value_oldest_updated, name: project_name) do = sort_title_oldest_updated + = link_to admin_groups_path(sort: sort_value_largest_group, name: project_name) do + = sort_title_largest_group = link_to new_admin_group_path, class: "btn btn-new" do New Group %ul.content-list diff --git a/app/views/admin/groups/new.html.haml b/app/views/admin/groups/new.html.haml index c81ee552ac3..8f9fe96249f 100644 --- a/app/views/admin/groups/new.html.haml +++ b/app/views/admin/groups/new.html.haml @@ -1,4 +1,4 @@ - page_title "New Group" %h3.page-title New group %hr -= render 'form' += render 'form', visibility_level: default_group_visibility diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 40871e32913..30b3fabdd7e 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -1,8 +1,8 @@ - page_title @group.name, "Groups" %h3.page-title - Group: #{@group.name} + Group: #{@group.full_name} - = link_to edit_admin_group_path(@group), class: "btn pull-right" do + = link_to admin_group_edit_path(@group), class: "btn pull-right" do %i.fa.fa-pencil-square-o Edit %hr @@ -39,6 +39,18 @@ = @group.created_at.to_s(:medium) %li + %span.light Storage: + %strong= storage_counter(@group.storage_size) + ( + = storage_counter(@group.repository_size) + repositories, + = storage_counter(@group.build_artifacts_size) + build artifacts, + = storage_counter(@group.lfs_objects_size) + LFS + ) + + %li %span.light Group Git LFS status: %strong = group_lfs_status(@group) @@ -55,8 +67,8 @@ %li %strong = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] - %span.label.label-gray - = repository_size(project) + %span.badge + = storage_counter(project.statistics.storage_size) %span.pull-right.light %span.monospace= project.path_with_namespace + ".git" .panel-footer @@ -73,8 +85,8 @@ %li %strong = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] - %span.label.label-gray - = repository_size(project) + %span.badge + = storage_counter(project.statistics.storage_size) %span.pull-right.light %span.monospace= project.path_with_namespace + ".git" @@ -88,10 +100,10 @@ Read more about project permissions %strong= link_to "here", help_page_path("user/permissions"), class: "vlink" - = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do + = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do %div = users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all) - %div.prepend-top-10 + .prepend-top-10 = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" %hr = button_tag 'Add users to group', class: "btn btn-create" diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index c217490963f..551edf14361 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -29,7 +29,7 @@ System hook will be triggered on set of events like creating project or adding ssh key. But you can also enable extra triggers like Push events. - %div.prepend-top-default + .prepend-top-default = f.check_box :push_events, class: 'pull-left' .prepend-left-20 = f.label :push_events, class: 'list-label' do @@ -54,7 +54,7 @@ = f.submit "Add System Hook", class: "btn btn-create" %hr --if @hooks.any? +- if @hooks.any? .panel.panel-default .panel-heading System hooks (#{@hooks.count}) @@ -70,4 +70,3 @@ - if hook.send(trigger) %span.label.label-gray= trigger.titleize %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} - diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml index be224d66855..77b174fbb27 100644 --- a/app/views/admin/labels/_label.html.haml +++ b/app/views/admin/labels/_label.html.haml @@ -1,4 +1,4 @@ -%li{id: dom_id(label)} +%li{ id: dom_id(label) } .label-row = render_colored_label(label, tooltip: false) = markdown_field(label, :description) diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index 824edd171f3..0a954c20fcd 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -8,7 +8,7 @@ %div{ class: container_class } %ul.nav-links.log-tabs - loggers.each do |klass| - %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') } + %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }> = link_to klass::file_name, "##{klass::file_name_noext}", 'data-toggle' => 'tab' .row-content-block diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index b37b8d4fee7..2e6f03fcde0 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -7,7 +7,7 @@ %div{ class: container_class } .top-area .prepend-top-default - = form_tag admin_namespaces_projects_path, method: :get do |f| + = form_tag admin_projects_path, method: :get do |f| .search-holder .search-field-holder = search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name' @@ -41,19 +41,19 @@ = button_tag "Search", class: "btn btn-primary btn-search" %ul.nav-links - - opts = params[:visibility_level].present? ? {} : { page: admin_namespaces_projects_path } + - opts = params[:visibility_level].present? ? {} : { page: admin_projects_path } = nav_link(opts) do - = link_to admin_namespaces_projects_path do + = link_to admin_projects_path do All = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do - = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do + = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do Private = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do - = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do + = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do Internal = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do - = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do + = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do Public .nav-controls @@ -69,8 +69,8 @@ .controls - if project.archived %span.label.label-warning archived - %span.label.label-gray - = repository_size(project) + %span.badge + = storage_counter(project.statistics.storage_size) = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn" = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove" .title diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 6c7c3c48604..2967da6e692 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -65,9 +65,16 @@ = @project.repository.path_to_repo %li - %span.light Size - %strong - = repository_size(@project) + %span.light Storage: + %strong= storage_counter(@project.statistics.storage_size) + ( + = storage_counter(@project.statistics.repository_size) + repository, + = storage_counter(@project.statistics.build_artifacts_size) + build artifacts, + = storage_counter(@project.statistics.lfs_objects_size) + LFS + ) %li %span.light last commit: diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml index 64893b38c58..975bd950ae1 100644 --- a/app/views/admin/runners/_runner.html.haml +++ b/app/views/admin/runners/_runner.html.haml @@ -1,4 +1,4 @@ -%tr{id: dom_id(runner)} +%tr{ id: dom_id(runner) } %td - if runner.shared? %span.label.label-success shared diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 73038164056..ca503e35623 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -91,7 +91,7 @@ %strong ##{build.id} %td.status - = ci_status_with_icon(build.status) + = render 'ci/status/badge', status: build.detailed_status(current_user) %td.status - if project diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml index cdbfc60f9a4..e5b8ebdf613 100644 --- a/app/views/admin/services/_form.html.haml +++ b/app/views/admin/services/_form.html.haml @@ -4,7 +4,8 @@ %p #{@service.description} template = form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'form-horizontal fieldset-form' } do |form| - = render 'shared/service_settings', form: form + = render 'shared/service_settings', form: form, subject: @service - .form-actions - = form.submit 'Save', class: 'btn btn-save' + .footer-block.row-content-block + .form-actions + = form.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index ce5e21e54cc..9984e733956 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -15,10 +15,8 @@ %ul.nav-links = nav_link(path: 'users#show') do = link_to "Account", admin_user_path(@user) - = nav_link(path: 'users#groups') do - = link_to "Groups", groups_admin_user_path(@user) = nav_link(path: 'users#projects') do - = link_to "Projects", projects_admin_user_path(@user) + = link_to "Groups and projects", projects_admin_user_path(@user) = nav_link(path: 'users#keys') do = link_to "SSH keys", keys_admin_user_path(@user) = nav_link(controller: :identities) do diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml index 4bf1c9cde3c..3b5c713ac2d 100644 --- a/app/views/admin/users/_user.html.haml +++ b/app/views/admin/users/_user.html.haml @@ -1,8 +1,8 @@ -%li.user-row +%li.flex-row .user-avatar = image_tag avatar_icon(user), class: "avatar", alt: '' - .user-details - .user-name + .row-main-content + .user-name.row-title.str-truncated-100 = link_to user.name, [:admin, user] - if user.blocked? %span.label.label-danger blocked @@ -12,13 +12,13 @@ %span.label.label-default External - if user == current_user %span It's you! - .user-email + .row-second-line.str-truncated-100 = mail_to user.email, user.email .controls = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn' - unless user == current_user .dropdown.inline - %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', data: { toggle: 'dropdown' } } + %a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', data: { toggle: 'dropdown' } } = icon('cog') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right diff --git a/app/views/admin/users/groups.html.haml b/app/views/admin/users/groups.html.haml deleted file mode 100644 index 8f6d13b881a..00000000000 --- a/app/views/admin/users/groups.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -- page_title "Groups", @user.name, "Users" -= render 'admin/users/head' - -- group_members = @user.group_members.includes(:source) -- if group_members.any? - .panel.panel-default - .panel-heading Groups: - %ul.well-list - - group_members.each do |group_member| - - group = group_member.group - %li.group_member - %span{class: ("list-item-name" unless group_member.owner?)} - %strong= link_to group.name, admin_group_path(group) - .pull-right - %span.light= group_member.human_access - - unless group_member.owner? - = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do - %i.fa.fa-times.fa-inverse -- else - .nothing-here-block This user has no groups. diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index d3038ae644f..4dc44225d49 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -68,7 +68,7 @@ %small.badge= number_with_delimiter(User.without_projects.count) .fade-right - %ul.users-list.content-list + %ul.flex-list.content-list - if @users.empty? %li .nothing-here-block No users found. diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml index 84b9ceb23b3..15eaf1c0e67 100644 --- a/app/views/admin/users/projects.html.haml +++ b/app/views/admin/users/projects.html.haml @@ -1,15 +1,21 @@ -- page_title "Projects", @user.name, "Users" +- page_title "Groups and projects", @user.name, "Users" = render 'admin/users/head' - if @user.groups.any? .panel.panel-default .panel-heading Group projects %ul.well-list - - @user.groups.each do |group| - %li - %strong= group.name + - @user.group_members.includes(:source).each do |group_member| + - group = group_member.group + %li.group_member + %strong= link_to group.name, admin_group_path(group) – access to #{pluralize(group.projects.count, 'project')} + .pull-right + %span.light.vertical-align-middle= group_member.human_access + - unless group_member.owner? + = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove prepend-left-10", title: 'Remove user from group' do + %i.fa.fa-times.fa-inverse .row .col-md-6 @@ -35,8 +41,8 @@ - if member.owner? %span.light Owner - else - %span.light= member.human_access + %span.light.vertical-align-middle= member.human_access - if member.respond_to? :project - = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do + = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove prepend-left-10", title: 'Remove user from project' do %i.fa.fa-times diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 76c9ed0ee8b..a71240986c9 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -40,7 +40,7 @@ %li.two-factor-status %span.light Two-factor Authentication: - %strong{class: @user.two_factor_enabled? ? 'cgreen' : 'cred'} + %strong{ class: @user.two_factor_enabled? ? 'cgreen' : 'cred' } - if @user.two_factor_enabled? Enabled = link_to 'Disable', disable_two_factor_admin_user_path(@user), data: {confirm: 'Are you sure?'}, method: :patch, class: 'btn btn-xs btn-remove pull-right', title: 'Disable Two-factor Authentication' diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index fbe3ab912b6..e3305e21e96 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -1,7 +1,9 @@ - grouped_emojis = awardable.grouped_awards(with_thumbs: inline) .awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } } - awards_sort(grouped_emojis).each do |emoji, awards| - %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } } + %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", + class: (award_state_class(awards, current_user)), + data: { placement: "bottom", title: award_user_list(awards, current_user) } } = emoji_icon(emoji, sprite: false) %span.award-control-text.js-counter = awards.count diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml index 61c7cce20b2..c91602fcff7 100644 --- a/app/views/ci/lints/_create.html.haml +++ b/app/views/ci/lints/_create.html.haml @@ -36,7 +36,7 @@ - if build[:allow_failure] %b Allowed to fail --else +- else %p %b Status: syntax is incorrect diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml index 889086c62b1..95eb9a57152 100644 --- a/app/views/ci/lints/show.html.haml +++ b/app/views/ci/lints/show.html.haml @@ -1,20 +1,25 @@ - page_title "CI Lint" - page_description "Validate your GitLab CI configuration file" +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/ace.js') %h2 Check your .gitlab-ci.yml -%hr -.row - = form_tag ci_lint_path, method: :post do - .form-group - = label_tag(:content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap') +.ci-linter + .row + = form_tag ci_lint_path, method: :post do + .form-group + .col-sm-12 + .file-holder + .file-title.clearfix + Content of .gitlab-ci.yml + #ci-editor.ci-editor #{@content} + = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true) .col-sm-12 - = text_area_tag(:content, @content, class: 'form-control span1', rows: 7, require: true) - .col-sm-12 - .pull-left.prepend-top-10 - = submit_tag('Validate', class: 'btn btn-success submit-yml') + .pull-left.prepend-top-10 + = submit_tag('Validate', class: 'btn btn-success submit-yml') -.row.prepend-top-20 - .col-sm-12 - .results - = render partial: 'create' if defined?(@status) + .row.prepend-top-20 + .col-sm-12 + .results.ci-template + = render partial: 'create' if defined?(@status) diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml new file mode 100644 index 00000000000..601fb7f0f3f --- /dev/null +++ b/app/views/ci/status/_badge.html.haml @@ -0,0 +1,11 @@ +- status = local_assigns.fetch(:status) +- css_classes = "ci-status ci-#{status.group}" + +- if status.has_details? + = link_to status.details_path, class: css_classes do + = custom_icon(status.icon) + = status.text +- else + %span{ class: css_classes } + = custom_icon(status.icon) + = status.text diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml new file mode 100644 index 00000000000..8dea3479f82 --- /dev/null +++ b/app/views/ci/status/_dropdown_graph_badge.html.haml @@ -0,0 +1,19 @@ +-# Renders the content of each li in the dropdown + +- subject = local_assigns.fetch(:subject) +- status = subject.detailed_status(current_user) +- klass = "ci-status-icon ci-status-icon-#{status.group}" +- tooltip = "#{subject.name} - #{status.label}" + +- if status.has_details? + = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip } do + %span{ class: klass }= custom_icon(status.icon) + %span.ci-build-text= subject.name +- else + .mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip } } + %span{ class: klass }= custom_icon(status.icon) + %span.ci-build-text= subject.name + +- if status.has_action? + = link_to status.action_path, class: 'ci-action-icon-wrapper js-ci-action-icon', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do + = icon(status.action_icon, class: status.action_class) diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml new file mode 100644 index 00000000000..dd2f649de9a --- /dev/null +++ b/app/views/ci/status/_graph_badge.html.haml @@ -0,0 +1,20 @@ +-# Renders the graph node with both the status icon, status name and action icon + +- subject = local_assigns.fetch(:subject) +- status = subject.detailed_status(current_user) +- klass = "ci-status-icon ci-status-icon-#{status.group}" +- tooltip = "#{subject.name} - #{status.label}" + +- if status.has_details? + = link_to status.details_path, class: 'build-content has-tooltip', data: { toggle: 'tooltip', title: tooltip } do + %span{ class: klass }= custom_icon(status.icon) + .ci-status-text= subject.name +- else + .build-content.has-tooltip{ data: { toggle: 'tooltip', title: tooltip } } + %span{ class: klass }= custom_icon(status.icon) + .ci-status-text= subject.name + +- if status.has_action? + = link_to status.action_path, class: 'ci-action-icon-container has-tooltip', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do + %i.ci-action-icon-wrapper + = icon(status.action_icon, class: status.action_class) diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml index b78e70ebc1e..02b94beee92 100644 --- a/app/views/dashboard/_activity_head.html.haml +++ b/app/views/dashboard/_activity_head.html.haml @@ -1,7 +1,7 @@ %ul.nav-links - %li{ class: ("active" unless params[:filter]) } + %li{ class: ("active" unless params[:filter]) }> = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do Your Projects - %li{ class: ("active" if params[:filter] == 'starred') } + %li{ class: ("active" if params[:filter] == 'starred') }> = link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do Starred Projects diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index f7abad54286..48b0fd504f4 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -4,13 +4,13 @@ %ul.nav-links = nav_link(page: [dashboard_projects_path, root_path]) do = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do - Your Projects + Your projects = nav_link(page: starred_dashboard_projects_path) do = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do - Starred Projects + Starred projects = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do - Explore Projects + Explore projects .nav-controls = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml index b25e8ea1f0c..02e90bbfa55 100644 --- a/app/views/dashboard/_snippets_head.html.haml +++ b/app/views/dashboard/_snippets_head.html.haml @@ -1,7 +1,13 @@ -%ul.nav-links - = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do - = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do - Your Snippets - = nav_link(page: explore_snippets_path) do - = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do - Explore Snippets +.top-area + %ul.nav-links + = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do + = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do + Your Snippets + = nav_link(page: explore_snippets_path) do + = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do + Explore Snippets + + - if current_user + .nav-controls.hidden-xs + = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do + New snippet diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml index fdea834ff45..1bbd4602ecf 100644 --- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml @@ -4,6 +4,18 @@ Welcome to GitLab %p.blank-state-text Code, test, and deploy together + +- if current_user.can_create_group? + .blank-state + .blank-state-icon + = custom_icon("group", size: 50) + %h3.blank-state-title + You can create a group for several dependent projects. + %p.blank-state-text + Groups are the best way to manage projects and members. + = link_to new_group_path, class: "btn btn-new" do + New group + .blank-state .blank-state-icon = custom_icon("project", size: 50) @@ -21,18 +33,7 @@ = link_to new_project_path, class: "btn btn-new" do New project -- if current_user.can_create_group? - .blank-state - .blank-state-icon - = custom_icon("group", size: 50) - %h3.blank-state-title - You can create a group for several dependent projects. - %p.blank-state-text - Groups are the best way to manage projects and members. - = link_to new_group_path, class: "btn btn-new" do - New group - --if publicish_project_count > 0 +- if publicish_project_count > 0 .blank-state .blank-state-icon = icon("globe") diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index b2af438ea57..85cbe0bf0e6 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -2,41 +2,11 @@ - header_title "Snippets", dashboard_snippets_path = render 'dashboard/snippets_head' += render partial: 'snippets/snippets_scope_menu', locals: { include_private: true } -.nav-block - .controls.hidden-xs - = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do - = icon('plus') - New snippet +.visible-xs + + = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do + New snippet - .nav-links.snippet-scope-menu - %li{ class: ("active" unless params[:scope]) } - = link_to dashboard_snippets_path do - All - %span.badge - = current_user.snippets.count - - %li{ class: ("active" if params[:scope] == "are_private") } - = link_to dashboard_snippets_path(scope: 'are_private') do - Private - %span.badge - = current_user.snippets.are_private.count - - %li{ class: ("active" if params[:scope] == "are_internal") } - = link_to dashboard_snippets_path(scope: 'are_internal') do - Internal - %span.badge - = current_user.snippets.are_internal.count - - %li{ class: ("active" if params[:scope] == "are_public") } - = link_to dashboard_snippets_path(scope: 'are_public') do - Public - %span.badge - = current_user.snippets.are_public.count - - .visible-xs - = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do - = icon('plus') - New snippet - -= render 'snippets/snippets' += render partial: 'snippets/snippets', locals: { link_project: true } diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index cc077fad32a..9d7bcdb9d16 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -1,9 +1,9 @@ -%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} } +%li{ class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data: { url: todo_target_path(todo) } } = author_avatar(todo, size: 40) .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/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 5b2465e25ee..f4efcfb27b2 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -5,14 +5,14 @@ .top-area %ul.nav-links - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending') - %li{class: "todos-pending #{todo_pending_active}"} + %li{ class: "todos-pending #{todo_pending_active}" }> = link_to todos_filter_path(state: 'pending') do %span To do %span.badge = number_with_delimiter(todos_pending_count) - todo_done_active = ('active' if params[:state] == 'done') - %li{class: "todos-done #{todo_done_active}"} + %li{ class: "todos-done #{todo_done_active}" }> = link_to todos_filter_path(state: 'done') do %span Done @@ -32,7 +32,7 @@ - if params[:project_id].present? = hidden_field_tag(:project_id, params[:project_id]) = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', - placeholder: 'Search projects', data: { data: todo_projects_options } }) + placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project' } }) .filter-item.inline - if params[:author_id].present? = hidden_field_tag(:author_id, params[:author_id]) @@ -42,21 +42,21 @@ - if params[:type].present? = hidden_field_tag(:type, params[:type]) = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit', - data: { data: todo_types_options } }) + data: { data: todo_types_options, default_label: 'Type' } }) .filter-item.inline.actions-filter - if params[:action_id].present? = hidden_field_tag(:action_id, params[:action_id]) = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', - data: { data: todo_actions_options }}) + data: { data: todo_actions_options, default_label: 'Action' } }) .pull-right .dropdown.inline.prepend-left-10 - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span.light - if @sort.present? = sort_options_hash[@sort] - else = sort_title_recently_created - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort %li = link_to todos_filter_path(sort: sort_value_priority) do @@ -84,7 +84,7 @@ = render "shared/empty_states/todos_all_done.svg" - if todos_filter_empty? %h4.text-center - Good job! Looks like you don't have any todos left. + = Gitlab.config.gitlab.no_todos_messages.sample %p.text-center Are you looking for things to do? Take a look at = succeed "," do diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml index 20cd7b0179d..fb70d158096 100644 --- a/app/views/devise/confirmations/almost_there.haml +++ b/app/views/devise/confirmations/almost_there.haml @@ -1,12 +1,13 @@ -.well-confirmation.text-center +.well-confirmation.text-center.append-bottom-20 %h1.prepend-top-0 Almost there... - %p.lead + %p.lead.append-bottom-20 Please check your email to confirm your account + %hr - if current_application_settings.after_sign_up_text.present? .well-confirmation.text-center = markdown_field(current_application_settings, :after_sign_up_text) -%p.confirmation-content.text-center +%p.text-center No confirmation email received? Please check your spam folder or .append-bottom-20.prepend-top-20.text-center %a.btn.btn-lg.btn-success{ href: new_user_confirmation_path } diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 21b89580818..5d359538efe 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -1,16 +1,16 @@ = form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f| - %div.form-group + .form-group = f.label "Username or email", for: :login = f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required." - %div.form-group + .form-group = f.label :password = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required." - %div.submit-container.move-submit-down - = f.submit "Sign in", class: "btn btn-save" - if devise_mapping.rememberable? .remember-me.checkbox - %label{for: "user_remember_me"} + %label{ for: "user_remember_me" } = f.check_box :remember_me %span Remember me .pull-right.forgot-password = link_to "Forgot your password?", new_password_path(resource_name) + .submit-container.move-submit-down + = f.submit "Sign in", class: "btn btn-save" diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml index a6cadbcbdff..2556cb6f59b 100644 --- a/app/views/devise/sessions/_new_crowd.html.haml +++ b/app/views/devise/sessions/_new_crowd.html.haml @@ -1,13 +1,13 @@ = form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user', class: 'gl-show-field-errors') do .form-group = label_tag :username, 'Username or email' - = text_field_tag :username, nil, {class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true } + = text_field_tag :username, nil, { class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true } .form-group = label_tag :password = password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true } - if devise_mapping.rememberable? .remember-me.checkbox - %label{for: "remember_me"} + %label{ for: "remember_me" } = check_box_tag :remember_me, '1', false, id: 'remember_me' %span Remember me = submit_tag "Sign in", class: "btn-save btn" diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index 3ab5461f929..3159d21598a 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -1,13 +1,13 @@ = form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "gl-show-field-errors") do .form-group = label_tag :username, "#{server['label']} Username" - = text_field_tag :username, nil, {class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true } + = text_field_tag :username, nil, { class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true } .form-group = label_tag :password = password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true } - if devise_mapping.rememberable? .remember-me.checkbox - %label{for: "remember_me"} + %label{ for: "remember_me" } = check_box_tag :remember_me, '1', false, id: 'remember_me' %span Remember me = submit_tag "Sign in", class: "btn-save btn" diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index fa8e7979461..af87129e49e 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -1,4 +1,5 @@ - page_title "Sign in" + %div - if form_based_providers.any? = render 'devise/shared/tabs_ldap' diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index 2cadc424668..951f03083bf 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -7,7 +7,7 @@ .login-box .login-body - if @user.two_factor_otp_enabled? - = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user gl-show-field-errors' }) do |f| + = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if @user.two_factor_u2f_enabled?}" }) do |f| - resource_params = params[resource_name].presence || params = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) %div diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 8908b64cdac..e87a16a5157 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,4 +1,4 @@ -%div.omniauth-container +.omniauth-container %p %span.light Sign in with diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml index 86edaf14e43..eddfce363a7 100644 --- a/app/views/devise/shared/_signin_box.html.haml +++ b/app/views/devise/shared/_signin_box.html.haml @@ -1,18 +1,18 @@ - if form_based_providers.any? - if crowd_enabled? - .login-box.tab-pane.active{id: "crowd", role: 'tabpanel', class: 'tab-pane'} + .login-box.tab-pane.active{ id: "crowd", role: 'tabpanel' } .login-body = render 'devise/sessions/new_crowd' - @ldap_servers.each_with_index do |server, i| - .login-box.tab-pane{id: "#{server['provider_name']}", role: 'tabpanel', class: (:active if i.zero? && !crowd_enabled?)} + .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: (:active if i.zero? && !crowd_enabled?) } .login-body = render 'devise/sessions/new_ldap', server: server - if signin_enabled? - .login-box.tab-pane{id: 'ldap-standard', role: 'tabpanel'} + .login-box.tab-pane{ id: 'ldap-standard', role: 'tabpanel' } .login-body = render 'devise/sessions/new_base' - elsif signin_enabled? - .login-box.tab-pane.active{id: 'login-pane', role: 'tabpanel'} + .login-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' } .login-body = render 'devise/sessions/new_base' diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 7c68e3266e5..01ecf237925 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -1,20 +1,23 @@ -#register-pane.login-box{ role: 'tabpanel', class: 'tab-pane' } +#register-pane.tab-pane.login-box{ role: 'tabpanel' } .login-body = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f| .devise-errors = devise_error_messages! - %div.form-group + .form-group = f.label :name = f.text_field :name, class: "form-control top", required: true, title: "This field is required." - %div.username.form-group + .username.form-group = f.label :username - = f.text_field :username, class: "form-control middle", pattern: "[a-zA-Z0-9]+", required: true, title: 'Please create a username with only alphanumeric characters.' + = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, required: true, title: 'Please create a username with only alphanumeric characters.' %p.validation-error.hide Username is already taken. %p.validation-success.hide Username is available. %p.validation-pending.hide Checking username availability... - %div.form-group + .form-group = f.label :email = f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address." + .form-group + = f.label :email_confirmation + = f.email_field :email_confirmation, class: "form-control middle", required: true, title: "Please retype the email address." .form-group.append-bottom-20#password-strength = f.label :password = f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters." diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml index 1e957f0935f..8c4ad30c832 100644 --- a/app/views/devise/shared/_tabs_ldap.html.haml +++ b/app/views/devise/shared/_tabs_ldap.html.haml @@ -3,8 +3,11 @@ %li.active = link_to "Crowd", "#crowd", 'data-toggle' => 'tab' - @ldap_servers.each_with_index do |server, i| - %li{class: (:active if i.zero? && !crowd_enabled?)} + %li{ class: (:active if i.zero? && !crowd_enabled?) } = link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab' - if signin_enabled? %li = link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab' + - if signin_enabled? && signup_enabled? + %li + = link_to 'Register', '#register-pane', 'data-toggle' => 'tab' diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml index 05246303fb6..c225d800a98 100644 --- a/app/views/devise/shared/_tabs_normal.html.haml +++ b/app/views/devise/shared/_tabs_normal.html.haml @@ -1,6 +1,6 @@ -%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist'} +%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist' } %li.active{ role: 'presentation' } - %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab'} Sign in + %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in - if signin_enabled? && signup_enabled? - %li{ role: 'presentation'} - %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab'} Register + %li{ role: 'presentation' } + %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml index 1411daeb4a6..2deadbeeceb 100644 --- a/app/views/discussions/_diff_discussion.html.haml +++ b/app/views/discussions/_diff_discussion.html.haml @@ -1,5 +1,5 @@ - expanded = local_assigns.fetch(:expanded, true) -%tr.notes_holder{class: ('hide' unless expanded)} +%tr.notes_holder{ class: ('hide' unless expanded) } %td.notes_line{ colspan: 2 } %td.notes_content .content diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml index 077e8e64e5f..2bce2780484 100644 --- a/app/views/discussions/_discussion.html.haml +++ b/app/views/discussions/_discussion.html.haml @@ -1,9 +1,6 @@ - expanded = discussion.expanded? %li.note.note-discussion.timeline-entry .timeline-entry-inner - .timeline-icon - = link_to user_path(discussion.author) do - = image_tag avatar_icon(discussion.author), class: "avatar s40" .timeline-content .discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } } .discussion-header @@ -13,9 +10,7 @@ = icon("chevron-up") - else = icon("chevron-down") - Toggle discussion - = link_to_member(@project, discussion.author, avatar: false) .inline.discussion-headline-light @@ -37,7 +32,6 @@ an outdated diff = time_ago_with_tooltip(discussion.created_at, placement: "bottom", html_class: "note-created-ago") - = render "discussions/headline", discussion: discussion .discussion-body.js-toggle-content{ class: ("hide" unless expanded) } diff --git a/app/views/discussions/_jump_to_next.html.haml b/app/views/discussions/_jump_to_next.html.haml index 7ed09dd1a98..69bd416c4de 100644 --- a/app/views/discussions/_jump_to_next.html.haml +++ b/app/views/discussions/_jump_to_next.html.haml @@ -5,5 +5,5 @@ %button.btn.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion", title: "Jump to next unresolved discussion", "aria-label" => "Jump to next unresolved discussion", - data: { container: "body" }} + data: { container: "body" } } = custom_icon("next_discussion") diff --git a/app/views/discussions/_parallel_diff_discussion.html.haml b/app/views/discussions/_parallel_diff_discussion.html.haml index f1072ce0feb..ef16b516e2c 100644 --- a/app/views/discussions/_parallel_diff_discussion.html.haml +++ b/app/views/discussions/_parallel_diff_discussion.html.haml @@ -1,9 +1,9 @@ - expanded = discussion_left.try(:expanded?) || discussion_right.try(:expanded?) -%tr.notes_holder{class: ('hide' unless expanded)} +%tr.notes_holder{ class: ('hide' unless expanded) } - if discussion_left %td.notes_line.old %td.notes_content.parallel.old - .content{class: ('hide' unless discussion_left.expanded?)} + .content{ class: ('hide' unless discussion_left.expanded?) } = render "discussions/notes", discussion: discussion_left, line_type: 'old' - else %td.notes_line.old= "" @@ -13,7 +13,7 @@ - if discussion_right %td.notes_line.new %td.notes_content.parallel.new - .content{class: ('hide' unless discussion_right.expanded?)} + .content{ class: ('hide' unless discussion_right.expanded?) } = render "discussions/notes", discussion: discussion_right, line_type: 'new' - else %td.notes_line.new= "" diff --git a/app/views/doorkeeper/applications/_delete_form.html.haml b/app/views/doorkeeper/applications/_delete_form.html.haml index 001a711b1dd..84b4ce5b606 100644 --- a/app/views/doorkeeper/applications/_delete_form.html.haml +++ b/app/views/doorkeeper/applications/_delete_form.html.haml @@ -1,6 +1,6 @@ - submit_btn_css ||= 'btn btn-link btn-remove btn-sm' = form_tag oauth_application_path(application) do - %input{:name => "_method", :type => "hidden", :value => "delete"}/ + %input{ :name => "_method", :type => "hidden", :value => "delete" }/ - if defined? small = button_tag type: "submit", class: "btn btn-transparent", data: { confirm: "Are you sure?" } do %span.sr-only diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml index 5c98265727a..b3313c7c985 100644 --- a/app/views/doorkeeper/applications/_form.html.haml +++ b/app/views/doorkeeper/applications/_form.html.haml @@ -17,5 +17,9 @@ %code= Doorkeeper.configuration.native_redirect_uri for local tests + .form-group + = f.label :scopes, class: 'label-light' + = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes + .prepend-top-default = f.submit 'Save application', class: "btn btn-create" diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml index 3998e66f40d..aa271150b07 100644 --- a/app/views/doorkeeper/applications/index.html.haml +++ b/app/views/doorkeeper/applications/index.html.haml @@ -31,7 +31,7 @@ %th.last-heading %tbody - @applications.each do |application| - %tr{id: "application_#{application.id}"} + %tr{ id: "application_#{application.id}" } %td= link_to application.name, oauth_application_path(application) %td - application.redirect_uri.split.each do |uri| @@ -63,7 +63,7 @@ %tbody - @authorized_apps.each do |app| - token = app.authorized_tokens.order('created_at desc').first - %tr{id: "application_#{app.id}"} + %tr{ id: "application_#{app.id}" } %td= app.name %td= token.created_at %td= token.scopes @@ -72,7 +72,7 @@ %tr %td Anonymous - %div.help-block + .help-block %em Authorization was granted by entering your username and password in the application. %td= token.created_at %td= token.scopes diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml index 47442b78d48..559de63d96d 100644 --- a/app/views/doorkeeper/applications/show.html.haml +++ b/app/views/doorkeeper/applications/show.html.haml @@ -2,7 +2,7 @@ %h3.page-title Application: #{@application.name} -.table-holder +.table-holder.oauth-application-show %table.table %tr %td @@ -22,6 +22,9 @@ - @application.redirect_uri.split.each do |uri| %div %span.monospace= uri + + = render "shared/tokens/scopes_list", token: @application + .form-actions = link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide pull-left' = render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10' diff --git a/app/views/doorkeeper/authorizations/error.html.haml b/app/views/doorkeeper/authorizations/error.html.haml index a4c607cea60..6117b00149f 100644 --- a/app/views/doorkeeper/authorizations/error.html.haml +++ b/app/views/doorkeeper/authorizations/error.html.haml @@ -1,3 +1,3 @@ %h3.page-title An error has occurred -%main{:role => "main"} +%main{ :role => "main" } %pre= @pre_auth.error_response.body[:error_description] diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index ce050007204..2a0e301c8dd 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -1,5 +1,5 @@ %h3.page-title Authorization required -%main{:role => "main"} +%main{ :role => "main" } %p.h4 Authorize %strong.text-info= @pre_auth.client.name diff --git a/app/views/doorkeeper/authorizations/show.html.haml b/app/views/doorkeeper/authorizations/show.html.haml index 01f9e46f142..44e868e6782 100644 --- a/app/views/doorkeeper/authorizations/show.html.haml +++ b/app/views/doorkeeper/authorizations/show.html.haml @@ -1,3 +1,3 @@ %h3.page-title Authorization code: -%main{:role => "main"} +%main{ :role => "main" } %code#authorization_code= params[:code] diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml index 9f02a8d2ed9..11c1e67878e 100644 --- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml +++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml @@ -3,7 +3,7 @@ - path = oauth_authorized_application_path(0, token_id: token) - else - path = oauth_authorized_application_path(application) - + = form_tag path do - %input{:name => "_method", :type => "hidden", :value => "delete"}/ + %input{ :name => "_method", :type => "hidden", :value => "delete" }/ = submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-remove btn-sm' diff --git a/app/views/doorkeeper/authorized_applications/index.html.haml b/app/views/doorkeeper/authorized_applications/index.html.haml index b184b9c01d4..c8a585560a2 100644 --- a/app/views/doorkeeper/authorized_applications/index.html.haml +++ b/app/views/doorkeeper/authorized_applications/index.html.haml @@ -1,6 +1,6 @@ %header.page-header %h1 Your authorized applications -%main{:role => "main"} +%main{ :role => "main" } .table-holder %table.table.table-striped %thead diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml index 790d90ad3ee..49bd9acd2db 100644 --- a/app/views/emojis/index.html.haml +++ b/app/views/emojis/index.html.haml @@ -7,5 +7,5 @@ %ul.clearfix.emoji-menu-list - emojis.each do |emoji| %li.pull-left.text-center.emoji-menu-list-item - %button.emoji-menu-btn.text-center.js-emoji-btn{type: "button"} + %button.emoji-menu-btn.text-center.js-emoji-btn{ type: "button" } = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"]) diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml index c034bbe430e..a97cbd4d4b3 100644 --- a/app/views/errors/access_denied.html.haml +++ b/app/views/errors/access_denied.html.haml @@ -1,6 +1,9 @@ -- page_title "Access Denied" -%h1 403 -%h3 Access Denied -%hr -%p You are not allowed to access this page. -%p Read more about project permissions #{link_to "here", help_page_path("user/permissions"), class: "vlink"} +- content_for(:title, 'Access Denied') +%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') } + %h1 + 403 +.container + %h3 Access Denied + %hr + %p You are not allowed to access this page. + %p Read more about project permissions #{link_to "here", help_page_path("user/permissions"), class: "vlink"} diff --git a/app/views/errors/encoding.html.haml b/app/views/errors/encoding.html.haml index 90cfbebfcc6..64f7f8e0836 100644 --- a/app/views/errors/encoding.html.haml +++ b/app/views/errors/encoding.html.haml @@ -1,5 +1,8 @@ -- page_title "Encoding Error" -%h1 500 -%h3 Encoding Error -%hr -%p Page can't be loaded because of an encoding error. +- content_for(:title, 'Encoding Error') +%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') } + %h1 + 500 +.container + %h3 Encoding Error + %hr + %p Page can't be loaded because of an encoding error. diff --git a/app/views/errors/git_not_found.html.haml b/app/views/errors/git_not_found.html.haml index ff5d4cc1506..d860957665b 100644 --- a/app/views/errors/git_not_found.html.haml +++ b/app/views/errors/git_not_found.html.haml @@ -1,7 +1,9 @@ -- page_title "Git Resource Not Found" -%h1 404 -%h3 Git Resource Not found -%hr -%p - Application can't get access to some branch or commit in your repository. It - may have been moved. +- content_for(:title, 'Git Resource Not Found') +%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') } + %h1 + 404 +.container + %h3 Git Resource Not found + %hr + %p Application can't get access to some branch or commit in your repository. It + may have been moved diff --git a/app/views/errors/not_found.html.haml b/app/views/errors/not_found.html.haml index 3756b98ebb2..a0b9a632e22 100644 --- a/app/views/errors/not_found.html.haml +++ b/app/views/errors/not_found.html.haml @@ -1,5 +1,8 @@ -- page_title "Not Found" -%h1 404 -%h3 The resource you were looking for doesn't exist. -%hr -%p You may have mistyped the address or the page may have moved. +- content_for(:title, 'Not Found') +%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') } + %h1 + 404 +.container + %h3 The resource you were looking for doesn't exist. + %hr + %p You may have mistyped the address or the page may have moved. diff --git a/app/views/errors/omniauth_error.html.haml b/app/views/errors/omniauth_error.html.haml index 3e70e98a24c..72508b91134 100644 --- a/app/views/errors/omniauth_error.html.haml +++ b/app/views/errors/omniauth_error.html.haml @@ -1,9 +1,12 @@ -- page_title "Auth Error" -%h1 422 -%h3 Sign-in using #{@provider} auth failed -%hr -%p Sign-in failed because #{@error}. -%p There are couple of steps you can take: +- content_for(:title, 'Auth Error') +%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') } + %h1 + 422 +.container + %h3 Sign-in using #{@provider} auth failed + %hr + %p Sign-in failed because #{@error}. + %p There are couple of steps you can take: %ul %li Try logging in using your email diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 5c318cd3b8b..a0bd14df209 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -3,14 +3,13 @@ .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} - = cache [event, current_application_settings, "v2.2"] do - = author_avatar(event, size: 40) + = author_avatar(event, size: 40) - - if event.created_project? - = render "events/event/created_project", event: event - - elsif event.push? - = render "events/event/push", event: event - - elsif event.commented? - = render "events/event/note", event: event - - else - = render "events/event/common", event: event + - if event.created_project? + = render "events/event/created_project", event: event + - elsif event.push? + = render "events/event/push", event: event + - elsif event.commented? + = render "events/event/note", event: event + - else + = render "events/event/common", event: event diff --git a/app/views/events/_event_issue.atom.haml b/app/views/events/_event_issue.atom.haml index 083c3936212..51585314a62 100644 --- a/app/views/events/_event_issue.atom.haml +++ b/app/views/events/_event_issue.atom.haml @@ -1,2 +1,2 @@ -%div{xmlns: "http://www.w3.org/1999/xhtml"} +%div{ xmlns: "http://www.w3.org/1999/xhtml" } = markdown(issue.description, pipeline: :atom, project: issue.project, author: issue.author) diff --git a/app/views/events/_event_merge_request.atom.haml b/app/views/events/_event_merge_request.atom.haml index d7e05600627..56fc8b86217 100644 --- a/app/views/events/_event_merge_request.atom.haml +++ b/app/views/events/_event_merge_request.atom.haml @@ -1,2 +1,2 @@ -%div{xmlns: "http://www.w3.org/1999/xhtml"} +%div{ xmlns: "http://www.w3.org/1999/xhtml" } = markdown(merge_request.description, pipeline: :atom, project: merge_request.project, author: merge_request.author) diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml index 1154f982821..6fa2f9bd4db 100644 --- a/app/views/events/_event_note.atom.haml +++ b/app/views/events/_event_note.atom.haml @@ -1,2 +1,2 @@ -%div{xmlns: "http://www.w3.org/1999/xhtml"} +%div{ xmlns: "http://www.w3.org/1999/xhtml" } = markdown(note.note, pipeline: :atom, project: note.project, author: note.author) diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml index 28bee1d0a33..f8f0bcb7608 100644 --- a/app/views/events/_event_push.atom.haml +++ b/app/views/events/_event_push.atom.haml @@ -1,4 +1,4 @@ -%div{xmlns: "http://www.w3.org/1999/xhtml"} +%div{ xmlns: "http://www.w3.org/1999/xhtml" } - event.commits.first(15).each do |commit| %p %strong= commit[:author][:name] diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index bba6e0d2c20..2fb6b5647da 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -1,6 +1,6 @@ .event-title %span.author_name= link_to_author event - %span{class: event.action_name} + %span{ class: event.action_name } - if event.target = event.action_name %strong diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index aba64dd17d0..80cf2344fe1 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -1,6 +1,6 @@ .event-title %span.author_name= link_to_author event - %span{class: event.action_name} + %span{ class: event.action_name } = event_action_name(event) - if event.project diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 44fff49d99c..64ca3c32e01 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -18,7 +18,7 @@ - few_commits.each do |commit| = render "events/commit", commit: commit, project: project, event: event - - create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) + - create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user) - if event.commits_count > 1 %li.commits-stat - if event.commits_count > 2 @@ -35,12 +35,12 @@ Compare #{from_label}...#{truncate_sha(event.commit_to)} - if create_mr - %span{"data-user-is" => event.author_id, "data-display" => "inline"} + %span or = link_to create_mr_path(project.default_branch, event.ref_name, project) do create a merge request - elsif create_mr - %li.commits-stat{"data-user-is" => event.author_id} + %li.commits-stat = link_to create_mr_path(project.default_branch, event.ref_name, project) do Create Merge Request - elsif event.rm_ref? diff --git a/app/views/explore/_head.html.haml b/app/views/explore/_head.html.haml index d8a57560788..a3b0709e261 100644 --- a/app/views/explore/_head.html.haml +++ b/app/views/explore/_head.html.haml @@ -1,5 +1,5 @@ -.explore-title - %h3 +.explore-title.text-center + %h2 Explore GitLab %p.lead Discover projects, groups and snippets. Share your projects with others diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index a1b39d9e1a0..73cf6e87eb4 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -17,13 +17,13 @@ .pull-right .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span.light - if @sort.present? = sort_options_hash[@sort] - else = sort_title_recently_created - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to explore_groups_path(sort: sort_value_recently_created) do diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index 4cff14b096b..e3088848492 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -1,13 +1,13 @@ - if current_user .dropdown - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" } = icon('globe') %span.light Visibility: - if params[:visibility_level].present? = visibility_level_label(params[:visibility_level].to_i) - else Any - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_projects_path(visibility_level: nil) do @@ -20,14 +20,14 @@ - if @tags.present? .dropdown - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" } = icon('tags') %span.light Tags: - if params[:tag].present? = params[:tag] - else Any - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_projects_path(tag: nil) do diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml index 7def9eacdc9..e5706d04736 100644 --- a/app/views/explore/snippets/index.html.haml +++ b/app/views/explore/snippets/index.html.haml @@ -6,12 +6,4 @@ - else = render 'explore/head' -.row-content-block - - if current_user - = link_to new_snippet_path, class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do - New snippet - - .oneline - Public snippets created by you and other users are listed here - -= render 'snippets/snippets' += render partial: 'snippets/snippets', locals: { link_project: true } diff --git a/app/views/groups/_group_lfs_settings.html.haml b/app/views/groups/_group_lfs_settings.html.haml index af57065f0fc..3c622ca5c3c 100644 --- a/app/views/groups/_group_lfs_settings.html.haml +++ b/app/views/groups/_group_lfs_settings.html.haml @@ -8,4 +8,4 @@ Allow projects within this group to use Git LFS = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') %br/ - %span.descr This setting can be overridden in each project.
\ No newline at end of file + %span.descr This setting can be overridden in each project. diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index ebf9aca7700..bc5d3c797ac 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -21,6 +21,7 @@ = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } = icon("search") + = render 'shared/members/sort_dropdown' .panel.panel-default .panel-heading Users with access to diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml index de8f53b6b52..9d05bff6c4e 100644 --- a/app/views/groups/group_members/update.js.haml +++ b/app/views/groups/group_members/update.js.haml @@ -1,3 +1,4 @@ :plain var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}'); $("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name')); + gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@group_member)}")); diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index dc6c1bb69de..b4aa4f24d9e 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -3,24 +3,27 @@ - if current_user = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@group.name} issues") -.top-area - = render 'shared/issuable/nav', type: :issues - .nav-controls +- if group_issues(@group).exists? + .top-area + = render 'shared/issuable/nav', type: :issues - if current_user - = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do - = icon('rss') - %span.icon-label - Subscribe - = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" + .nav-controls + = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do + = icon('rss') + %span.icon-label + Subscribe + = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" -= render 'shared/issuable/filter', type: :issues + = render 'shared/issuable/filter', type: :issues -.row-content-block.second-block - Only issues from - %strong #{@group.name} - group are listed here. - - if current_user - To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page. + .row-content-block.second-block + Only issues from the + %strong #{@group.name} + group are listed here. + - if current_user + To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page. -.prepend-top-default - = render 'shared/issues' + .prepend-top-default + = render 'shared/issues' +- else + = render 'shared/empty_states/issues', project_select_button: true diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index e6953d94531..dbbdb583a24 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -2,8 +2,9 @@ .top-area = render 'shared/issuable/nav', type: :merge_requests - .nav-controls - = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request" + - if current_user + .nav-controls + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request" = render 'shared/issuable/filter', type: :merge_requests diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index 23d438b2aa1..63cadfca530 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -34,21 +34,10 @@ = f.label :projects, "Projects", class: "control-label" .col-sm-10 = f.collection_select :project_ids, @group.projects.non_archived, :id, :name, - { selected: @group.projects.non_archived.pluck(:id) }, multiple: true, class: 'select2' + { selected: @group.projects.non_archived.pluck(:id) }, required: true, multiple: true, class: 'select2' - .col-md-6 - .form-group - = f.label :due_date, "Due Date", class: "control-label" - .col-sm-10 - = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date" + = render "shared/milestones/form_dates", f: f .form-actions = f.submit 'Create Milestone', class: "btn-create btn" = link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel" - - -:javascript - $(".datepicker").datepicker({ - dateFormat: "yy-mm-dd", - onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } - }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val())); diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index d19eaa6add9..38d63fd9acc 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -21,5 +21,5 @@ = render 'shared/group_tips' .form-actions - = f.submit 'Create group', class: "btn btn-create", tabindex: 3 + = f.submit 'Create group', class: "btn btn-create" = link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel' diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 33fee334d93..2e7e5e5c309 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -18,8 +18,8 @@ .pull-right - if project.archived %span.label.label-warning archived - %span.label.label-gray - = repository_size(project) + %span.badge + = storage_counter(project.statistics.storage_size) = link_to 'Members', namespace_project_project_members_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" = link_to 'Remove', project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-sm btn-remove" diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index b439b40a75a..d256d14609e 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -4,27 +4,25 @@ - if current_user = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") -.cover-block.groups-cover-block +.group-home-panel.text-center %div{ class: container_class } .avatar-container.s70.group-avatar = image_tag group_icon(@group), class: "avatar s70 avatar-tile" - .group-info - .cover-title - %h1 - @#{@group.path} - %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) } - = visibility_level_icon(@group.visibility_level, fw: false) - - .group-right-buttons.btn-group - - if current_user - .pull-left.append-right-10= render 'shared/members/access_request_buttons', source: @group - = render 'shared/notifications/button', notification_setting: @notification_setting - - - if @group.description.present? - .cover-desc.description - = markdown_field(@group, :description) - -%div.groups-header{ class: container_class } + %h1.group-title + @#{@group.path} + %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) } + = visibility_level_icon(@group.visibility_level, fw: false) + + - if @group.description.present? + .group-home-desc + = markdown_field(@group, :description) + + - if current_user + .group-buttons + = render 'shared/members/access_request_buttons', source: @group + = render 'shared/notifications/button', notification_setting: @notification_setting + +.groups-header{ class: container_class } .top-area %ul.nav-links %li.active @@ -34,6 +32,10 @@ %li = link_to "#shared", 'data-toggle' => 'tab' do Shared Projects + - if @nested_groups.present? + %li + = link_to "#groups", 'data-toggle' => 'tab' do + Subgroups .nav-controls = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false @@ -49,3 +51,8 @@ - if @shared_projects.present? .tab-pane#shared = render "shared_projects", projects: @shared_projects + + - if @nested_groups.present? + .tab-pane#groups + %ul.content-list + = render partial: 'shared/groups/group', collection: @nested_groups diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 65842a0479b..b74cc822295 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -1,8 +1,8 @@ -#modal-shortcuts.modal{tabindex: -1} +#modal-shortcuts.modal{ tabindex: -1 } .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h4 Keyboard Shortcuts %small @@ -82,7 +82,7 @@ .col-lg-4 %table.shortcut-mappings - %tbody{ class: 'hidden-shortcut project', style: 'display:none' } + %tbody.hidden-shortcut.project{ style: 'display:none' } %tr %th %th Global Dashboard @@ -190,7 +190,7 @@ %td New issue .col-lg-4 %table.shortcut-mappings - %tbody{ class: 'hidden-shortcut network', style: 'display:none' } + %tbody.hidden-shortcut.network{ style: 'display:none' } %tr %th %th Network Graph @@ -240,7 +240,7 @@ .key shift j %td Scroll to bottom - %tbody{ class: 'hidden-shortcut issues', style: 'display:none' } + %tbody.hidden-shortcut.issues{ style: 'display:none' } %tr %th %th Issues @@ -264,7 +264,7 @@ %td.shortcut .key l %td Change Label - %tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' } + %tbody.hidden-shortcut.merge_requests{ style: 'display:none' } %tr %th %th Merge Requests diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml index be257b51b9e..f6ebd76af9d 100644 --- a/app/views/help/show.html.haml +++ b/app/views/help/show.html.haml @@ -1,3 +1,3 @@ - page_title @path.split("/").reverse.map(&:humanize) .documentation.wiki - = markdown @markdown.gsub('$your_email', current_user.try(:email) || "email@example.com") + = markdown @markdown diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 070ed90da6d..dd1df46792b 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -182,7 +182,7 @@ .nav-controls = text_field_tag 'sample', nil, class: 'form-control' .dropdown - %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span Sort by name = icon('chevron-down') %ul.dropdown-menu @@ -205,121 +205,121 @@ %h2#buttons Buttons .example - %button.btn.btn-default{:type => "button"} Default - %button.btn.btn-gray{:type => "button"} Gray - %button.btn.btn-primary{:type => "button"} Primary - %button.btn.btn-success{:type => "button"} Success - %button.btn.btn-info{:type => "button"} Info - %button.btn.btn-warning{:type => "button"} Warning - %button.btn.btn-danger{:type => "button"} Danger - %button.btn.btn-link{:type => "button"} Link + %button.btn.btn-default{ :type => "button" } Default + %button.btn.btn-gray{ :type => "button" } Gray + %button.btn.btn-primary{ :type => "button" } Primary + %button.btn.btn-success{ :type => "button" } Success + %button.btn.btn-info{ :type => "button" } Info + %button.btn.btn-warning{ :type => "button" } Warning + %button.btn.btn-danger{ :type => "button" } Danger + %button.btn.btn-link{ :type => "button" } Link %h2#dropdowns Dropdowns .example .clearfix .dropdown.inline.pull-left - %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } Dropdown = icon('chevron-down') %ul.dropdown-menu %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option .dropdown.inline.pull-right - %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } Dropdown = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option .example %div .dropdown.inline - %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } Dropdown = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-selectable %li - %a.is-active{href: "#"} + %a.is-active{ href: "#" } Dropdown Option .example %div .dropdown.inline - %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } Dropdown = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-title %span Dropdown Title - %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } } = icon('times') .dropdown-input - %input.dropdown-input-field{type: "search", placeholder: "Filter results"} + %input.dropdown-input-field{ type: "search", placeholder: "Filter results" } = icon('search') .dropdown-content %ul %li - %a.is-active{href: "#"} + %a.is-active{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li.divider %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option .dropdown-footer %strong Tip: If an author is not a member of this project, you can still filter by his name while using the search field. .dropdown.inline - %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } Dropdown loading = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-selectable.is-loading .dropdown-title %span Dropdown Title - %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } } = icon('times') .dropdown-input - %input.dropdown-input-field{type: "search", placeholder: "Filter results"} + %input.dropdown-input-field{ type: "search", placeholder: "Filter results" } = icon('search') .dropdown-content %ul %li - %a.is-active{href: "#"} + %a.is-active{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li.divider %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option %li - %a{href: "#"} + %a{ href: "#" } Dropdown Option .dropdown-footer %strong Tip: @@ -330,21 +330,21 @@ .example %div .dropdown.inline - %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{ type: 'button', data: {toggle: 'dropdown' } } Dropdown user = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user .dropdown-title %span Dropdown Title - %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } } = icon('times') .dropdown-input - %input.dropdown-input-field{type: "search", placeholder: "Filter results"} + %input.dropdown-input-field{ type: "search", placeholder: "Filter results" } = icon('search') .dropdown-content %ul %li - %a.dropdown-menu-user-link.is-active{href: "#"} + %a.dropdown-menu-user-link.is-active{ href: "#" } = link_to_member_avatar(@user, size: 30) %strong.dropdown-menu-user-full-name = @user.name @@ -354,24 +354,24 @@ .example %div .dropdown.inline - %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } Dropdown page 2 = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user.dropdown-menu-paging.is-page-two .dropdown-page-one .dropdown-title - %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}} + %button.dropdown-title-button.dropdown-menu-back{ aria: { label: "Go back" } } = icon('arrow-left') %span Dropdown Title - %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } } = icon('times') .dropdown-input - %input.dropdown-input-field{type: "search", placeholder: "Filter results"} + %input.dropdown-input-field{ type: "search", placeholder: "Filter results" } = icon('search') .dropdown-content %ul %li - %a.dropdown-menu-user-link.is-active{href: "#"} + %a.dropdown-menu-user-link.is-active{ href: "#" } = link_to_member_avatar(@user, size: 30) %strong.dropdown-menu-user-full-name = @user.name @@ -379,13 +379,13 @@ = @user.to_reference .dropdown-page-two .dropdown-title - %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}} + %button.dropdown-title-button.dropdown-menu-back{ aria: { label: "Go back" } } = icon('arrow-left') %span Create label - %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } } = icon('times') .dropdown-input - %input.dropdown-input-field{type: "search", placeholder: "Name new label"} + %input.dropdown-input-field{ type: "search", placeholder: "Name new label" } .dropdown-content %button.btn.btn-primary Create @@ -393,16 +393,16 @@ .example %div .dropdown.inline - %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + %button#js-project-dropdown.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } Projects = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-title %span Go to project - %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } } = icon('times') .dropdown-input - %input.dropdown-input-field{type: "search", placeholder: "Filter results"} + %input.dropdown-input-field{ type: "search", placeholder: "Filter results" } = icon('search') .dropdown-content .dropdown-loading @@ -486,22 +486,22 @@ .example %form.form-horizontal .form-group - %label.col-sm-2.control-label{:for => "inputEmail3"} Email + %label.col-sm-2.control-label{ :for => "inputEmail3" } Email .col-sm-10 - %input#inputEmail3.form-control{:placeholder => "Email", :type => "email"}/ + %input#inputEmail3.form-control{ :placeholder => "Email", :type => "email" }/ .form-group - %label.col-sm-2.control-label{:for => "inputPassword3"} Password + %label.col-sm-2.control-label{ :for => "inputPassword3" } Password .col-sm-10 - %input#inputPassword3.form-control{:placeholder => "Password", :type => "password"}/ + %input#inputPassword3.form-control{ :placeholder => "Password", :type => "password" }/ .form-group .col-sm-offset-2.col-sm-10 .checkbox %label - %input{:type => "checkbox"}/ + %input{ :type => "checkbox" }/ Remember me .form-group .col-sm-offset-2.col-sm-10 - %button.btn.btn-default{:type => "submit"} Sign in + %button.btn.btn-default{ :type => "submit" } Sign in .lead Form when label rendered above input @@ -510,16 +510,16 @@ .example %form .form-group - %label{:for => "exampleInputEmail1"} Email address - %input#exampleInputEmail1.form-control{:placeholder => "Enter email", :type => "email"}/ + %label{ :for => "exampleInputEmail1" } Email address + %input#exampleInputEmail1.form-control{ :placeholder => "Enter email", :type => "email" }/ .form-group - %label{:for => "exampleInputPassword1"} Password - %input#exampleInputPassword1.form-control{:placeholder => "Password", :type => "password"}/ + %label{ :for => "exampleInputPassword1" } Password + %input#exampleInputPassword1.form-control{ :placeholder => "Password", :type => "password" }/ .checkbox %label - %input{:type => "checkbox"}/ + %input{ :type => "checkbox" }/ Remember me - %button.btn.btn-default{:type => "submit"} Sign in + %button.btn.btn-default{ :type => "submit" } Sign in %h2#file File %h4 diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml new file mode 100644 index 00000000000..864c5c0ff95 --- /dev/null +++ b/app/views/import/_githubish_status.html.haml @@ -0,0 +1,61 @@ +- provider = local_assigns.fetch(:provider) +- provider_title = Gitlab::ImportSources.title(provider) + +%p.light + Select projects you want to import. +%hr +%p + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all projects + = icon("spinner spin", class: "loading-icon") + +.table-responsive + %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col + %thead + %tr + %th= "From #{provider_title}" + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } + %td + = provider_project_link(provider, project.import_source) + %td + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name + + - @repos.each do |repo| + %tr{ id: "repo_#{repo.id}" } + %td + = provider_project_link(provider, repo.full_name) + %td.import-target + %fieldset.row + .input-group + .project-path.input-group-btn + - if current_user.can_select_namespace? + - selected = params[:namespace_id] || :current_user + - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {} + = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } + - else + = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true + %span.input-group-addon / + = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true + %td.import-actions.job-status + = button_tag class: "btn btn-import js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") + +.js-importer-status{ data: { jobs_import_path: "#{url_for([:jobs, :import, provider])}", import_path: "#{url_for([:import, provider])}" } } diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index f8b4b107513..7f1b9ee7141 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -1,5 +1,6 @@ -- page_title "Bitbucket import" -- header_title "Projects", root_path +- page_title 'Bitbucket import' +- header_title 'Projects', root_path + %h3.page-title %i.fa.fa-bitbucket Import projects from Bitbucket @@ -10,13 +11,13 @@ %hr %p - if @incompatible_repos.any? - = button_tag class: "btn btn-import btn-success js-import-all" do + = button_tag class: 'btn btn-import btn-success js-import-all' do Import all compatible projects - = icon("spinner spin", class: "loading-icon") + = icon('spinner spin', class: 'loading-icon') - else - = button_tag class: "btn btn-success js-import-all" do + = button_tag class: 'btn btn-import btn-success js-import-all' do Import all projects - = icon("spinner spin", class: "loading-icon") + = icon('spinner spin', class: 'loading-icon') .table-responsive %table.table.import-jobs @@ -30,9 +31,9 @@ %th Status %tbody - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } %td - = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank" + = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank' %td = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status @@ -47,31 +48,41 @@ = project.human_import_status_name - @repos.each do |repo| - %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"} + %tr{ id: "repo_#{repo.owner}___#{repo.slug}" } %td - = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank" + = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: "_blank" %td.import-target - = import_project_target(repo['owner'], repo['slug']) + %fieldset.row + .input-group + .project-path.input-group-btn + - if current_user.can_select_namespace? + - selected = params[:namespace_id] || :current_user + - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner, path: repo.owner) } : {} + = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } + - else + = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true + %span.input-group-addon / + = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true %td.import-actions.job-status - = button_tag class: "btn btn-import js-add-to-import" do + = button_tag class: 'btn btn-import js-add-to-import' do Import - = icon("spinner spin", class: "loading-icon") + = icon('spinner spin', class: 'loading-icon') - @incompatible_repos.each do |repo| - %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"} + %tr{ id: "repo_#{repo.owner}___#{repo.slug}" } %td - = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank" + = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank' %td.import-target %td.import-actions-job-status - = label_tag "Incompatible Project", nil, class: "label label-danger" + = label_tag 'Incompatible Project', nil, class: 'label label-danger' - if @incompatible_repos.any? %p One or more of your Bitbucket projects cannot be imported into GitLab directly because they use Subversion or Mercurial for version control, rather than Git. Please convert - = link_to "them to Git,", "https://www.atlassian.com/git/tutorials/migrating-overview" + = link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview' and go through the - = link_to "import flow", status_import_bitbucket_path, "data-no-turbolink" => "true" + = link_to 'import flow', status_import_bitbucket_path, 'data-no-turbolink' => 'true' again. .js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } } diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml index c8a6fa1aa9e..97e5e51abe0 100644 --- a/app/views/import/fogbugz/status.html.haml +++ b/app/views/import/fogbugz/status.html.haml @@ -29,7 +29,7 @@ %th Status %tbody - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } %td = project.import_source %td @@ -46,7 +46,7 @@ = project.human_import_status_name - @repos.each do |repo| - %tr{id: "repo_#{repo.id}"} + %tr{ id: "repo_#{repo.id}" } %td = repo.name %td.import-target diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml new file mode 100644 index 00000000000..02a116f996b --- /dev/null +++ b/app/views/import/gitea/new.html.haml @@ -0,0 +1,23 @@ +- page_title "Gitea Import" +- header_title "Projects", root_path + +%h3.page-title + = custom_icon('go_logo') + Import Projects from Gitea + +%p + To get started, please enter your Gitea Host URL and a + = succeed '.' do + = link_to 'Personal Access Token', 'https://github.com/gogits/go-gogs-client/wiki#access-token' + += form_tag personal_access_token_import_gitea_path, class: 'form-horizontal' do + .form-group + = label_tag :gitea_host_url, 'Gitea Host URL', class: 'control-label' + .col-sm-4 + = text_field_tag :gitea_host_url, nil, placeholder: 'https://try.gitea.io', class: 'form-control' + .form-group + = label_tag :personal_access_token, 'Personal Access Token', class: 'control-label' + .col-sm-4 + = text_field_tag :personal_access_token, nil, class: 'form-control' + .form-actions + = submit_tag 'List Your Gitea Repositories', class: 'btn btn-create' diff --git a/app/views/import/gitea/status.html.haml b/app/views/import/gitea/status.html.haml new file mode 100644 index 00000000000..589ca27e45d --- /dev/null +++ b/app/views/import/gitea/status.html.haml @@ -0,0 +1,7 @@ +- page_title "Gitea Import" +- header_title "Projects", root_path +%h3.page-title + = custom_icon('go_logo') + Import Projects from Gitea + += render 'import/githubish_status', provider: 'gitea' diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 4c721d40b55..0fe578a0036 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -1,64 +1,6 @@ -- page_title "GitHub import" +- page_title "GitHub Import" - header_title "Projects", root_path %h3.page-title - %i.fa.fa-github - Import projects from GitHub + = icon 'github', text: 'Import Projects from GitHub' -%p.light - Select projects you want to import. -%hr -%p - = button_tag class: "btn btn-import btn-success js-import-all" do - Import all projects - = icon("spinner spin", class: "loading-icon") - -.table-responsive - %table.table.import-jobs - %colgroup.import-jobs-from-col - %colgroup.import-jobs-to-col - %colgroup.import-jobs-status-col - %thead - %tr - %th From GitHub - %th To GitLab - %th Status - %tbody - - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} - %td - = github_project_link(project.import_source) - %td - = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] - %td.job-status - - if project.import_status == 'finished' - %span - %i.fa.fa-check - done - - elsif project.import_status == 'started' - %i.fa.fa-spinner.fa-spin - started - - else - = project.human_import_status_name - - - @repos.each do |repo| - %tr{id: "repo_#{repo.id}"} - %td - = github_project_link(repo.full_name) - %td.import-target - %fieldset.row - .input-group - .project-path.input-group-btn - - if current_user.can_select_namespace? - - selected = params[:namespace_id] || :current_user - - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {} - = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } - - else - = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true - %span.input-group-addon / - = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true - %td.import-actions.job-status - = button_tag class: "btn btn-import js-add-to-import" do - Import - = icon("spinner spin", class: "loading-icon") - -.js-importer-status{ data: { jobs_import_path: "#{jobs_import_github_path}", import_path: "#{import_github_path}" } } += render 'import/githubish_status', provider: 'github' diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index d31fc2e6adb..d5b88709a34 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -24,7 +24,7 @@ %th Status %tbody - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } %td = link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank" %td @@ -41,7 +41,7 @@ = project.human_import_status_name - @repos.each do |repo| - %tr{id: "repo_#{repo["id"]}"} + %tr{ id: "repo_#{repo["id"]}" } %td = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank" %td.import-target diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml index 5d2f149cd5f..336becd229e 100644 --- a/app/views/import/google_code/new.html.haml +++ b/app/views/import/google_code/new.html.haml @@ -45,7 +45,7 @@ %p Upload <code>GoogleCodeProjectHosting.json</code> here: %p - %input{type: "file", name: "dump_file", id: "dump_file"} + %input{ type: "file", name: "dump_file", id: "dump_file" } %li %p Do you want to customize how Google Code email addresses and usernames are imported into GitLab? diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml index e79f122940a..9f1507cade6 100644 --- a/app/views/import/google_code/status.html.haml +++ b/app/views/import/google_code/status.html.haml @@ -34,7 +34,7 @@ %th Status %tbody - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } %td = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank" %td @@ -51,7 +51,7 @@ = project.human_import_status_name - @repos.each do |repo| - %tr{id: "repo_#{repo.id}"} + %tr{ id: "repo_#{repo.id}" } %td = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" %td.import-target @@ -61,7 +61,7 @@ Import = icon("spinner spin", class: "loading-icon") - @incompatible_repos.each do |repo| - %tr{id: "repo_#{repo.id}"} + %tr{ id: "repo_#{repo.id}" } %td = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" %td.import-target diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml index 2fd4859c1c6..882fdf1317d 100644 --- a/app/views/invites/show.html.haml +++ b/app/views/invites/show.html.haml @@ -6,7 +6,7 @@ - if inviter = @member.created_by by = link_to inviter.name, user_url(inviter) - to join + to join - case @member.source - when Project - project = @member.source @@ -20,11 +20,18 @@ = link_to group.name, group_url(group) as #{@member.human_access}. -- if @member.source.users.include?(current_user) +- is_member = @member.source.users.include?(current_user) + +- if is_member %p However, you are already a member of this #{@member.source.is_a?(Group) ? "group" : "project"}. Sign in using a different account to accept the invitation. -- else + +- if @member.invite_email != current_user.email + %p + Note that this invitation was sent to #{mail_to @member.invite_email}, but you are signed in as #{link_to current_user.to_reference, user_url(current_user)} with email #{mail_to current_user.email}. + +- unless is_member .actions = link_to "Accept invitation", accept_invite_url(@token), method: :post, class: "btn btn-success" = link_to "Decline", decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10" diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml index 125f09777ba..c93dc7a50e8 100644 --- a/app/views/kaminari/gitlab/_next_page.html.haml +++ b/app/views/kaminari/gitlab/_next_page.html.haml @@ -6,8 +6,8 @@ -# per_page: number of items to fetch per page -# remote: data-remote - if current_page.last? - %li{ class: "next disabled" } + %li.next.disabled %span= raw(t 'views.pagination.next') - else - %li{ class: "next" } + %li.next = link_to raw(t 'views.pagination.next'), url, rel: 'next', remote: remote diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml index 750aed8f329..cefe0066a8f 100644 --- a/app/views/kaminari/gitlab/_page.html.haml +++ b/app/views/kaminari/gitlab/_page.html.haml @@ -6,5 +6,5 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li{class: "page#{' active' if page.current?}#{' sibling' if page.next? || page.prev?}"} - = link_to page, url, {remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil} +%li{ class: "page#{' active' if page.current?}#{' sibling' if page.next? || page.prev?}" } + = link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil } diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml index f5e0d2ed3f3..8fe6bd653ae 100644 --- a/app/views/kaminari/gitlab/_paginator.html.haml +++ b/app/views/kaminari/gitlab/_paginator.html.haml @@ -6,7 +6,7 @@ -# remote: data-remote -# paginator: the paginator that renders the pagination tags inside = paginator.render do - %div.gl-pagination + .gl-pagination %ul.pagination.clearfix - unless current_page.first? = first_page_tag unless total_pages < 5 # As kaminari will always show the first 5 pages @@ -19,4 +19,3 @@ = next_page_tag - unless current_page.last? = last_page_tag unless total_pages < 5 - diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml index 7edf10498a8..b7c6caf7ff4 100644 --- a/app/views/kaminari/gitlab/_prev_page.html.haml +++ b/app/views/kaminari/gitlab/_prev_page.html.haml @@ -6,8 +6,8 @@ -# per_page: number of items to fetch per page -# remote: data-remote - if current_page.first? - %li{ class: "prev disabled" } + %li.prev.disabled %span= raw(t 'views.pagination.previous') - else - %li{ class: "prev" } + %li.prev = link_to raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 757de92d6d4..3096f0ee19e 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -1,27 +1,27 @@ - page_description brand_title unless page_description - site_name = "GitLab" -%head{prefix: "og: http://ogp.me/ns#"} - %meta{charset: "utf-8"} - %meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'} +%head{ prefix: "og: http://ogp.me/ns#" } + %meta{ charset: "utf-8" } + %meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' } -# Open Graph - http://ogp.me/ - %meta{property: 'og:type', content: "object"} - %meta{property: 'og:site_name', content: site_name} - %meta{property: 'og:title', content: page_title} - %meta{property: 'og:description', content: page_description} - %meta{property: 'og:image', content: page_image} - %meta{property: 'og:url', content: request.base_url + request.fullpath} + %meta{ property: 'og:type', content: "object" } + %meta{ property: 'og:site_name', content: site_name } + %meta{ property: 'og:title', content: page_title } + %meta{ property: 'og:description', content: page_description } + %meta{ property: 'og:image', content: page_image } + %meta{ property: 'og:url', content: request.base_url + request.fullpath } -# Twitter Card - https://dev.twitter.com/cards/types/summary - %meta{property: 'twitter:card', content: "summary"} - %meta{property: 'twitter:title', content: page_title} - %meta{property: 'twitter:description', content: page_description} - %meta{property: 'twitter:image', content: page_image} + %meta{ property: 'twitter:card', content: "summary" } + %meta{ property: 'twitter:title', content: page_title } + %meta{ property: 'twitter:description', content: page_description } + %meta{ property: 'twitter:image', content: page_image } = page_card_meta_tags %title= page_title(site_name) - %meta{name: "description", content: page_description} + %meta{ name: "description", content: page_description } = favicon_link_tag 'favicon.ico' @@ -36,25 +36,23 @@ = csrf_meta_tags - unless browser.safari? - %meta{name: 'referrer', content: 'origin-when-cross-origin'} - %meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'} - %meta{name: 'theme-color', content: '#474D57'} + %meta{ name: 'referrer', content: 'origin-when-cross-origin' } + %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' } + %meta{ name: 'theme-color', content: '#474D57' } -# Apple Safari/iOS home screen icons = favicon_link_tag 'touch-icon-iphone.png', rel: 'apple-touch-icon' = favicon_link_tag 'touch-icon-ipad.png', rel: 'apple-touch-icon', sizes: '76x76' = favicon_link_tag 'touch-icon-iphone-retina.png', rel: 'apple-touch-icon', sizes: '120x120' = favicon_link_tag 'touch-icon-ipad-retina.png', rel: 'apple-touch-icon', sizes: '152x152' - %link{rel: 'mask-icon', href: image_path('logo.svg'), color: 'rgb(226, 67, 41)'} + %link{ rel: 'mask-icon', href: image_path('logo.svg'), color: 'rgb(226, 67, 41)' } -# Windows 8 pinned site tile - %meta{name: 'msapplication-TileImage', content: image_path('msapplication-tile.png')} - %meta{name: 'msapplication-TileColor', content: '#30353E'} + %meta{ name: 'msapplication-TileImage', content: image_path('msapplication-tile.png') } + %meta{ name: 'msapplication-TileColor', content: '#30353E' } = yield :meta_tags = render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id') = render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id') = render 'layouts/bootlint' if Rails.env.development? - - = render 'layouts/user_styles' diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml index e138ebab018..3daa1e90a8c 100644 --- a/app/views/layouts/_init_auto_complete.html.haml +++ b/app/views/layouts/_init_auto_complete.html.haml @@ -3,6 +3,14 @@ - if project :javascript - GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_type, type_id: params[:id])}" - GitLab.GfmAutoComplete.cachedData = undefined; - GitLab.GfmAutoComplete.setup(); + gl.GfmAutoComplete.dataSources = { + emojis: "#{emojis_namespace_project_autocomplete_sources_path(project.namespace, project)}", + members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}", + issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}", + mergeRequests: "#{merge_requests_namespace_project_autocomplete_sources_path(project.namespace, project)}", + labels: "#{labels_namespace_project_autocomplete_sources_path(project.namespace, project)}", + milestones: "#{milestones_namespace_project_autocomplete_sources_path(project.namespace, project)}", + commands: "#{commands_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}" + }; + + gl.GfmAutoComplete.setup(); diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 8aefdcb3d9b..54d02ee8e4b 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -22,9 +22,10 @@ = render "layouts/nav/#{nav}" .content-wrapper{ class: "#{layout_nav_class}" } = yield :sub_nav - = render "layouts/broadcast" - = render "layouts/flash" - = yield :flash_message + .alert-wrapper + = render "layouts/broadcast" + = render "layouts/flash" + = yield :flash_message %div{ class: "#{(container_class unless @no_container)} #{@content_class}" } - .content + .content{ id: "content-body" } = yield diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 8e65bd12c56..0e64ebd71b8 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -6,7 +6,7 @@ - group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) } - if @project && @project.persisted? - project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: namespace_project_issues_path(@project.namespace, @project), mr_path: namespace_project_merge_requests_path(@project.namespace, @project) } -.search.search-form{class: "#{'has-location-badge' if label.present?}"} +.search.search-form{ class: "#{'has-location-badge' if label.present?}" } = form_tag search_path, method: :get, class: 'navbar-form' do |f| .search-input-container - if label.present? @@ -44,4 +44,4 @@ = hidden_field_tag :snippets, true = hidden_field_tag :repository_ref, @ref = button_tag 'Go' if ENV['RAILS_ENV'] == 'test' - .search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref } + .search-autocomplete-opts.hide{ :'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref } diff --git a/app/views/layouts/_user_styles.html.haml b/app/views/layouts/_user_styles.html.haml deleted file mode 100644 index b76b3cb5510..00000000000 --- a/app/views/layouts/_user_styles.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -:css - [data-user-is] { - display: none !important; - } - - [data-user-is="#{current_user.try(:id)}"] { - display: block !important; - } - - [data-user-is="#{current_user.try(:id)}"][data-display="inline"] { - display: inline !important; - } - - [data-user-is-not] { - display: block !important; - } - - [data-user-is-not][data-display="inline"] { - display: inline !important; - } - - [data-user-is-not="#{current_user.try(:id)}"] { - display: none !important; - } diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 6c2285fa2b6..935517d4913 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en", class: "#{page_class}" } = render "layouts/head" - %body{class: "#{user_application_theme}", data: {page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}"}} + %body{ class: "#{user_application_theme}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } = Gon::Base.render_data -# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body. diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index afd9958f073..3368a9beb29 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -1,7 +1,7 @@ !!! 5 -%html{ lang: "en", class: "devise-layout-html"} +%html.devise-layout-html = render "layouts/head" - %body.ui_charcoal.login-page.application.navless{ data: { page: body_data_page }} + %body.ui_charcoal.login-page.application.navless{ data: { page: body_data_page } } .page-wrap = Gon::Base.render_data = render "layouts/header/empty" diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml index 6bd427b02ac..7466423a934 100644 --- a/app/views/layouts/devise_empty.html.haml +++ b/app/views/layouts/devise_empty.html.haml @@ -1,5 +1,5 @@ !!! 5 -%html{ lang: "en"} +%html{ lang: "en" } = render "layouts/head" %body.ui_charcoal.login-page.application.navless = Gon::Base.render_data diff --git a/app/views/layouts/devise_mailer.html.haml b/app/views/layouts/devise_mailer.html.haml index c258eafdd51..e1e1f9ae516 100644 --- a/app/views/layouts/devise_mailer.html.haml +++ b/app/views/layouts/devise_mailer.html.haml @@ -1,7 +1,7 @@ !!! 5 %html %head - %meta(content='text/html; charset=UTF-8' http-equiv='Content-Type') + %meta{ content: 'text/html; charset=UTF-8', 'http-equiv'=> 'Content-Type' } = stylesheet_link_tag 'mailers/devise' %body @@ -9,7 +9,7 @@ %tr %td %table#header - %td{valign: "top"} + %td{ valign: "top" } = image_tag('mailers/gitlab_header_logo.png', id: 'logo', alt: 'GitLab Wordmark') %table#body diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index 7fbe065df00..6d9ec043590 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -1,10 +1,59 @@ !!! 5 -%html{ lang: "en"} - = render "layouts/head" - %body{class: "#{user_application_theme} application navless"} - = Gon::Base.render_data - = render "layouts/header/empty" - .container.navless-container - = render "layouts/flash" - .error-page - = yield +%html{ lang: "en" } + %head + %meta{ :content => "width=device-width, initial-scale=1, maximum-scale=1", :name => "viewport" } + %title= yield(:title) + :css + body { + color: #666; + text-align: center; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + margin: auto; + font-size: 14px; + } + + h1 { + font-size: 56px; + line-height: 100px; + font-weight: normal; + color: #456; + } + + h2 { + font-size: 24px; + color: #666; + line-height: 1.5em; + } + + h3 { + color: #456; + font-size: 20px; + font-weight: normal; + line-height: 28px; + } + + hr { + max-width: 800px; + margin: 18px auto; + border: 0; + border-top: 1px solid #EEE; + border-bottom: 1px solid white; + } + + img { + max-width: 40vw; + display: block; + margin: 40px auto; + } + + .container { + margin: auto 20px; + } + + ul { + margin: auto; + text-align: left; + display:inline-block; + } +%body + = yield diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 7a9859262f7..9ecc0d11c95 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,10 +1,11 @@ %header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } - %div{ class: "container-fluid" } + %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content + .container-fluid .header-content %button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" } %span.sr-only Toggle navigation = icon('bars') - %button.navbar-toggle{type: 'button'} + %button.navbar-toggle{ type: 'button' } %span.sr-only Toggle navigation = icon('ellipsis-v') @@ -43,7 +44,7 @@ %li = link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username } %li - = link_to "Profile Settings", profile_path, aria: { label: "Profile Settings" } + = link_to "Settings", profile_path, aria: { label: "Settings" } %li = link_to "Help", help_path, aria: { label: "Help" } %li.divider diff --git a/app/views/layouts/nav/_admin_settings.html.haml b/app/views/layouts/nav/_admin_settings.html.haml index 38e9b80d129..9de0e12a826 100644 --- a/app/views/layouts/nav/_admin_settings.html.haml +++ b/app/views/layouts/nav/_admin_settings.html.haml @@ -1,6 +1,6 @@ .controls .dropdown.admin-settings-dropdown - %a.dropdown-new.btn.btn-default{href: '#', 'data-toggle' => 'dropdown'} + %a.dropdown-new.btn.btn-default{ href: '#', 'data-toggle' => 'dropdown' } = icon('cog') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index a0356feef95..205d23178d2 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,5 +1,4 @@ .nav-sidebar - .sidebar-header Across GitLab %ul.nav = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do @@ -26,13 +25,17 @@ = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do %span Issues - %span.count= number_with_delimiter(current_user.assigned_issues.opened.count) + %span.count= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened)) = nav_link(path: 'dashboard#merge_requests') do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do %span Merge Requests - %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count) + %span.count= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened)) = nav_link(controller: 'dashboard/snippets') do = link_to dashboard_snippets_path, title: 'Snippets' do %span Snippets + + = link_to help_path, title: 'About GitLab CE', class: 'about-gitlab' do + %span + About GitLab CE diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index f7edb47b666..f3539fd372d 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -31,7 +31,7 @@ = link_to merge_requests_group_path(@group), title: 'Merge Requests' do %span Merge Requests - - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute + - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute %span.badge.count= number_with_delimiter(merge_requests.count) = nav_link(controller: [:group_members]) do = link_to group_group_members_path(@group), title: 'Members' do diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index 75275afc0f3..30feb6813b4 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -1,26 +1,18 @@ - if current_user - can_admin_group = can?(current_user, :admin_group, @group) - can_edit = can?(current_user, :admin_group, @group) - - member = @group.members.find_by(user_id: current_user.id) - - can_leave = member && can?(current_user, :destroy_group_member, member) - - if can_admin_group || can_edit || can_leave + - if can_admin_group || can_edit .controls .dropdown.group-settings-dropdown - %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} + %a.dropdown-new.btn.btn-default#group-settings-button{ href: '#', 'data-toggle' => 'dropdown' } = icon('cog') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right - if can_admin_group = nav_link(path: 'groups#projects') do = link_to 'Projects', projects_group_path(@group), title: 'Projects' - - if can_edit || can_leave + - if can_edit && can_admin_group %li.divider - - if can_edit %li = link_to 'Edit Group', edit_group_path(@group) - - if can_leave - %li - = link_to polymorphic_path([:leave, @group, :members]), - data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do - Leave Group diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 6d514f669db..e06301bda14 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -17,6 +17,10 @@ = link_to applications_profile_path, title: 'Applications' do %span Applications + = nav_link(controller: :chat_names) do + = link_to profile_chat_names_path, title: 'Chat' do + %span + Chat = nav_link(controller: :personal_access_tokens) do = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do %span diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 99a58bbb676..a8bbd67de80 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,28 +1,19 @@ - if current_user .controls .dropdown.project-settings-dropdown - %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', 'data-toggle' => 'dropdown'} + %a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', 'data-toggle' => 'dropdown' } = icon('cog') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right - can_edit = can?(current_user, :admin_project, @project) - -# We don't use @project.team.find_member because it searches for group members too... - - member = @project.members.find_by(user_id: current_user.id) - - can_leave = member && can?(current_user, :destroy_project_member, member) = render 'layouts/nav/project_settings', can_edit: can_edit - - if can_edit || can_leave + - if can_edit %li.divider - - if can_edit - %li - = link_to edit_project_path(@project) do - Edit Project - - if can_leave - %li - = link_to polymorphic_path([:leave, @project, :members]), - data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do - Leave Project + %li + = link_to edit_project_path(@project) do + Edit Project .scrolling-tabs-container{ class: nav_control_class } .fade-left @@ -70,14 +61,14 @@ %span Issues - if @project.default_issues_tracker? - %span.badge.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count) + %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) - if project_nav_tab? :merge_requests = nav_link(controller: :merge_requests) do = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %span Merge Requests - %span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) + %span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) - if project_nav_tab? :wiki = nav_link(controller: :wikis) do diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 613b8b7d301..c6df66d2c3c 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -1,26 +1,17 @@ - if project_nav_tab? :team - = nav_link(controller: [:project_members, :teams]) do - = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do + = nav_link(controller: [:members, :teams]) do + = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do %span Members - if can_edit - - if @project.allowed_to_share_with_group? - = nav_link(controller: :group_links) do - = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do - %span - Groups = nav_link(controller: :deploy_keys) do = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do %span Deploy Keys - = nav_link(controller: :hooks) do - = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do - %span - Webhooks - = nav_link(controller: :services) do - = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do + = nav_link(controller: :integrations) do + = link_to namespace_project_settings_integrations_path(@project.namespace, @project), title: 'Integrations' do %span - Services + Integrations = nav_link(controller: :protected_branches) do = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do %span diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index 1ec4c3f0c67..76268c1b705 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -1,14 +1,14 @@ -%html{lang: "en"} +%html{ lang: "en" } %head - %meta{content: "text/html; charset=utf-8", "http-equiv" => "Content-Type"} + %meta{ content: "text/html; charset=utf-8", "http-equiv" => "Content-Type" } %title GitLab = stylesheet_link_tag 'notify' = yield :head %body - %div.content + .content = yield - %div.footer{style: "margin-top: 10px;"} + .footer{ style: "margin-top: 10px;" } %p — %br diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index b77d3402a2e..0ee8a57dbd4 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -1,5 +1,5 @@ -- page_title "Profile Settings" -- header_title "Profile Settings", profile_path unless header_title +- page_title "User Settings" +- header_title "User Settings", profile_path unless header_title - sidebar "dashboard" - nav "profile" diff --git a/app/views/notify/_note_message.text.erb b/app/views/notify/_note_message.text.erb new file mode 100644 index 00000000000..f82cbc9a3fc --- /dev/null +++ b/app/views/notify/_note_message.text.erb @@ -0,0 +1,5 @@ +<% if current_application_settings.email_author_in_body %> + <%= @note.author_name %> wrote: +<% end -%> + +<%= @note.note %> diff --git a/app/views/notify/_note_mr_or_commit_email.html.haml b/app/views/notify/_note_mr_or_commit_email.html.haml new file mode 100644 index 00000000000..edf8dfe7e9e --- /dev/null +++ b/app/views/notify/_note_mr_or_commit_email.html.haml @@ -0,0 +1,18 @@ += content_for :head do + = stylesheet_link_tag 'mailers/highlighted_diff_email' + +New comment + +- if @discussion && @discussion.diff_file + on + = link_to @note.diff_file.file_path, @target_url, class: 'details' + \: + %table + = render partial: "projects/diffs/line", + collection: @discussion.truncated_diff_lines, + as: :line, + locals: { diff_file: @note.diff_file, + plain: true, + email: true } + += render 'note_message' diff --git a/app/views/notify/_note_mr_or_commit_email.text.erb b/app/views/notify/_note_mr_or_commit_email.text.erb new file mode 100644 index 00000000000..b4fcdf6b1e9 --- /dev/null +++ b/app/views/notify/_note_mr_or_commit_email.text.erb @@ -0,0 +1,8 @@ +<% if @discussion && @discussion.diff_file -%> + on <%= @note.diff_file.file_path -%> +<% end -%>: + +<%= url %> + +<%= render 'simple_diff' if @discussion -%> +<%= render 'note_message' %> diff --git a/app/views/notify/_simple_diff.text.erb b/app/views/notify/_simple_diff.text.erb new file mode 100644 index 00000000000..c28d1cc34d3 --- /dev/null +++ b/app/views/notify/_simple_diff.text.erb @@ -0,0 +1,3 @@ +<% @discussion.truncated_diff_lines(highlight: false).each do |line| %> +> <%= line.text %> +<% end %> diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml index 4bf7c1f4d64..a744c4be9d6 100644 --- a/app/views/notify/build_fail_email.html.haml +++ b/app/views/notify/build_fail_email.html.haml @@ -1,5 +1,5 @@ - content_for :header do - %h1{style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"} + %h1{ style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" } GitLab (build failed) %h3 diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml index 252a5b7152c..8c2e6db1426 100644 --- a/app/views/notify/build_success_email.html.haml +++ b/app/views/notify/build_success_email.html.haml @@ -1,5 +1,5 @@ - content_for :header do - %h1{style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"} + %h1{ style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" } GitLab (build successful) %h3 diff --git a/app/views/notify/links/ci/builds/_build.html.haml b/app/views/notify/links/ci/builds/_build.html.haml new file mode 100644 index 00000000000..d35b3839171 --- /dev/null +++ b/app/views/notify/links/ci/builds/_build.html.haml @@ -0,0 +1,2 @@ +%a{ href: pipeline_build_url(pipeline, build), style: "color:#3777b0;text-decoration:none;" } + = build.name diff --git a/app/views/notify/links/ci/builds/_build.text.erb b/app/views/notify/links/ci/builds/_build.text.erb new file mode 100644 index 00000000000..f495a2e5486 --- /dev/null +++ b/app/views/notify/links/ci/builds/_build.text.erb @@ -0,0 +1 @@ +Build #<%= build.id %> ( <%= pipeline_build_url(pipeline, build) %> ) diff --git a/app/views/notify/links/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.html.haml new file mode 100644 index 00000000000..b6563b185b3 --- /dev/null +++ b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.html.haml @@ -0,0 +1 @@ += build.name diff --git a/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb new file mode 100644 index 00000000000..8e89c52a1f3 --- /dev/null +++ b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb @@ -0,0 +1 @@ +Build #<%= build.id %> diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml index f42b150c0d6..d1855568215 100644 --- a/app/views/notify/new_issue_email.html.haml +++ b/app/views/notify/new_issue_email.html.haml @@ -1,7 +1,7 @@ - if current_application_settings.email_author_in_body %div #{link_to @issue.author_name, user_url(@issue.author)} wrote: --if @issue.description +- if @issue.description = markdown(@issue.description, pipeline: :email, author: @issue.author) - if @issue.assignee_id.present? diff --git a/app/views/notify/new_mention_in_issue_email.html.haml b/app/views/notify/new_mention_in_issue_email.html.haml index 4f3d36bd9ca..02f21baa368 100644 --- a/app/views/notify/new_mention_in_issue_email.html.haml +++ b/app/views/notify/new_mention_in_issue_email.html.haml @@ -4,7 +4,7 @@ - if current_application_settings.email_author_in_body %div #{link_to @issue.author_name, user_url(@issue.author)} wrote: --if @issue.description +- if @issue.description = markdown(@issue.description, pipeline: :email, author: @issue.author) - if @issue.assignee_id.present? diff --git a/app/views/notify/new_mention_in_merge_request_email.html.haml b/app/views/notify/new_mention_in_merge_request_email.html.haml index 32aedb9e6b9..cbd434be02a 100644 --- a/app/views/notify/new_mention_in_merge_request_email.html.haml +++ b/app/views/notify/new_mention_in_merge_request_email.html.haml @@ -11,5 +11,5 @@ %p Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name} --if @merge_request.description +- if @merge_request.description = markdown(@merge_request.description, pipeline: :email, author: @merge_request.author) diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index 158404de396..8890b300f7d 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -8,5 +8,5 @@ %p Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name} --if @merge_request.description +- if @merge_request.description = markdown(@merge_request.description, pipeline: :email, author: @merge_request.author) diff --git a/app/views/notify/note_commit_email.html.haml b/app/views/notify/note_commit_email.html.haml index 1d961e4424c..0a650e3b2ca 100644 --- a/app/views/notify/note_commit_email.html.haml +++ b/app/views/notify/note_commit_email.html.haml @@ -1,2 +1,2 @@ -= render 'note_message' - +%p.details + = render 'note_mr_or_commit_email' diff --git a/app/views/notify/note_commit_email.text.erb b/app/views/notify/note_commit_email.text.erb index aaeaf5fdf73..6aa085a172e 100644 --- a/app/views/notify/note_commit_email.text.erb +++ b/app/views/notify/note_commit_email.text.erb @@ -1,9 +1,2 @@ -New comment for Commit <%= @commit.short_id %> - -<%= url_for(namespace_project_commit_url(@note.project.namespace, @note.project, id: @commit.id, anchor: "note_#{@note.id}")) %> - - -Author: <%= @note.author_name %> - -<%= @note.note %> - +New comment for Commit <%= @commit.short_id -%> +<%= render partial: 'note_mr_or_commit_email', locals: { url: @target_url } %> diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml index ea7e3d199fd..0a650e3b2ca 100644 --- a/app/views/notify/note_merge_request_email.html.haml +++ b/app/views/notify/note_merge_request_email.html.haml @@ -1,7 +1,2 @@ -- if @note.diff_note? && @note.diff_file - %p.details - New comment on diff for - = link_to @note.diff_file.file_path, @target_url - \: - -= render 'note_message' +%p.details + = render 'note_mr_or_commit_email' diff --git a/app/views/notify/note_merge_request_email.text.erb b/app/views/notify/note_merge_request_email.text.erb index 8cdab63829e..2ce64c494cf 100644 --- a/app/views/notify/note_merge_request_email.text.erb +++ b/app/views/notify/note_merge_request_email.text.erb @@ -1,9 +1,2 @@ -New comment for Merge Request <%= @merge_request.to_reference %> - -<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %> - - -<%= @note.author_name %> - -<%= @note.note %> - +New comment for Merge Request <%= @merge_request.to_reference -%> +<%= render partial: 'note_mr_or_commit_email', locals: { url: @target_url }%> diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index 38c852f0a3a..82c7fe229b8 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -1,9 +1,9 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -%html{lang: "en"} +%html{ lang: "en" } %head - %meta{content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/ - %meta{content: "width=device-width, initial-scale=1", name: "viewport"}/ - %meta{content: "IE=edge", "http-equiv" => "X-UA-Compatible"}/ + %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/ + %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/ + %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/ %title= message.subject :css /* CLIENT-SPECIFIC STYLES */ @@ -41,137 +41,139 @@ padding-right: 10px !important; } } - %body{style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"} - %table#body{border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;"} + %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" } %tbody %tr.line - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" } %tr.header - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"} - %img{alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55"}/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/ %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"} - %table.wrapper{border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"} - %table.content{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" } %tbody %tr.alert - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;"} - %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;"} - %img{alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13"}/ - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } + %img{ alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } Your pipeline has failed. %tr.spacer - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } %tr.section - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"} - %table.info{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"} Project - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) - %a.muted{href: namespace_url, style: "color:#333333;text-decoration:none;"} + %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } = namespace_name \/ - %a.muted{href: project_url(@project), style: "color:#333333;text-decoration:none;"} + %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" } = @project.name %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Branch - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} - %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} - %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13"}/ - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} - %a.muted{href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" } = @pipeline.ref %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Commit - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} - %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} - %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/ - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} - %a{href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } = @pipeline.short_sha - if @merge_request in - %a{href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;"} + %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" } = @merge_request.to_reference - .commit{style: "color:#5c5c5c;font-weight:300;"} + .commit{ style: "color:#5c5c5c;font-weight:300;" } = @pipeline.git_commit_message.truncate(50) %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Author - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} - %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - commit = @pipeline.commit - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} - %img.avatar{height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24"}/ - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.author - %a.muted{href: user_url(commit.author), style: "color:#333333;text-decoration:none;"} + %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } = commit.author.name - else %span = commit.author_name %tr.spacer - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } - failed = @pipeline.statuses.latest.failed %tr.pre-section - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;" } Pipeline - %a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"} + %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } = "\##{@pipeline.id}" had = failed.size failed = "#{'build'.pluralize(failed.size)}." %tr.warning - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" } Logs may contain sensitive data. Please consider before forwarding this email. %tr.section - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;"} - %table.builds{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" } + %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" } %tbody - failed.each do |build| %tr.build-state - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"} - %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;"} - %img{alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10"}/ - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;" } + %img{ alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" } = build.stage - %td{align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"} - %a{href: pipeline_build_url(@pipeline, build), style: "color:#3777b0;text-decoration:none;"} - = build.name + %td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" } + = render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %tr.build-log - %td{colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;"} - %pre{style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;"} - = build.trace_html(last_lines: 10).html_safe + - if build.has_trace? + %td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" } + %pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" } + = build.trace_html(last_lines: 10).html_safe + - else + %td{ colspan: "2" } %tr.footer - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"} - %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ %div - %a{href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;"} Manage all notifications + %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications · - %a{href: help_url, style: "color:#3777b0;text-decoration:none;"} Help + %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help %div You're receiving this email because of your account on = succeed "." do - %a{href: root_url, style: "color:#3777b0;text-decoration:none;"}= Gitlab.config.gitlab.host + %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb index 8f8084b58e1..ab91c7ef350 100644 --- a/app/views/notify/pipeline_failed_email.text.erb +++ b/app/views/notify/pipeline_failed_email.text.erb @@ -19,10 +19,12 @@ Commit Author: <%= commit.author_name %> Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>. <% failed.each do |build| -%> -Build #<%= build.id %> ( <%= pipeline_build_url(@pipeline, build) %> ) +<%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %> Stage: <%= build.stage %> Name: <%= build.name %> +<% if build.has_trace? -%> Trace: <%= build.trace_with_state(last_lines: 10)[:text] %> +<% end -%> <% end -%> diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index 697c8d19257..6dddb3b6373 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -1,9 +1,9 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -%html{lang: "en"} +%html{ lang: "en" } %head - %meta{content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/ - %meta{content: "width=device-width, initial-scale=1", name: "viewport"}/ - %meta{content: "IE=edge", "http-equiv" => "X-UA-Compatible"}/ + %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/ + %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/ + %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/ %title= message.subject :css /* CLIENT-SPECIFIC STYLES */ @@ -41,114 +41,114 @@ padding-right: 10px !important; } } - %body{style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"} - %table#body{border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;"} + %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" } %tbody %tr.line - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" } %tr.header - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"} - %img{alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55"}/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/ %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"} - %table.wrapper{border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"} - %table.content{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" } %tbody %tr.success - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;"} - %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;"} - %img{alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13"}/ - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } + %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } Your pipeline has passed. %tr.spacer - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } %tr.section - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"} - %table.info{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"} Project - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) - %a.muted{href: namespace_url, style: "color:#333333;text-decoration:none;"} + %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } = namespace_name \/ - %a.muted{href: project_url(@project), style: "color:#333333;text-decoration:none;"} + %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" } = @project.name %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Branch - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} - %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} - %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13"}/ - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} - %a.muted{href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" } = @pipeline.ref %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Commit - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} - %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} - %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/ - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} - %a{href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } = @pipeline.short_sha - if @merge_request in - %a{href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;"} + %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" } = @merge_request.to_reference - .commit{style: "color:#5c5c5c;font-weight:300;"} + .commit{ style: "color:#5c5c5c;font-weight:300;" } = @pipeline.git_commit_message.truncate(50) %tr - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Author - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} - %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - commit = @pipeline.commit - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} - %img.avatar{height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24"}/ - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.author - %a.muted{href: user_url(commit.author), style: "color:#333333;text-decoration:none;"} + %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } = commit.author.name - else %span = commit.author_name %tr.spacer - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } %tr.success-message - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;"} + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;" } - build_count = @pipeline.statuses.latest.size - - stage_count = @pipeline.stages.size + - stage_count = @pipeline.stages_count Pipeline - %a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"} + %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } = "\##{@pipeline.id}" successfully completed = "#{build_count} #{'build'.pluralize(build_count)}" in = "#{stage_count} #{'stage'.pluralize(stage_count)}." %tr.footer - %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"} - %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ %div - %a{href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;"} Manage all notifications + %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications · - %a{href: help_url, style: "color:#3777b0;text-decoration:none;"} Help + %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help %div You're receiving this email because of your account on = succeed "." do - %a{href: root_url, style: "color:#3777b0;text-decoration:none;"}= Gitlab.config.gitlab.host + %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb index ae22d474f2c..40e5e306426 100644 --- a/app/views/notify/pipeline_success_email.text.erb +++ b/app/views/notify/pipeline_success_email.text.erb @@ -16,7 +16,7 @@ Commit Author: <%= commit.author_name %> <% end -%> <% build_count = @pipeline.statuses.latest.size -%> -<% stage_count = @pipeline.stages.size -%> +<% stage_count = @pipeline.stages_count -%> Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>. You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>. diff --git a/app/views/notify/project_was_not_exported_email.text.haml b/app/views/notify/project_was_not_exported_email.text.haml index b27cb620b9e..27785165c2d 100644 --- a/app/views/notify/project_was_not_exported_email.text.haml +++ b/app/views/notify/project_was_not_exported_email.text.haml @@ -3,4 +3,4 @@ = "The errors we encountered were:" - @errors.each do |error| - #{error}
\ No newline at end of file + #{error} diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index c0c07d65daa..36858fa6f34 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -1,5 +1,5 @@ = content_for :head do - = stylesheet_link_tag 'mailers/repository_push_email' + = stylesheet_link_tag 'mailers/highlighted_diff_email' %h3 #{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name} @@ -27,9 +27,9 @@ %h4 #{pluralize @message.diffs_count, "changed file"}: %ul - - @message.diffs.each_with_index do |diff, i| + - @message.diffs.each do |diff| %li.file-stats - %a{href: "#{@message.target_url if @message.disable_diffs?}#diff-#{i}" } + %a{ href: "#{@message.target_url if @message.disable_diffs?}##{hexdigest(diff.file_path)}" } - if diff.deleted_file %span.deleted-file − @@ -52,9 +52,10 @@ %h5 The diff was not included because it is too large. - else %h4 Changes: - - diff_files.each_with_index do |diff_file, i| - %li{id: "diff-#{i}"} - %a{href: @message.target_url + "#diff-#{i}"}< + - diff_files.each do |diff_file| + - file_hash = hexdigest(diff_file.file_path) + %li{ id: file_hash } + %a{ href: @message.target_url + "##{file_hash}" }< - if diff_file.deleted_file %strong< = diff_file.old_path diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml new file mode 100644 index 00000000000..1ec1e7c70e4 --- /dev/null +++ b/app/views/profiles/chat_names/_chat_name.html.haml @@ -0,0 +1,27 @@ +- service = chat_name.service +- project = service.project +%tr + %td + %strong + - if can?(current_user, :read_project, project) + = link_to project.name_with_namespace, project_path(project) + - else + .light N/A + %td + %strong + - if can?(current_user, :admin_project, project) + = link_to service.title, edit_namespace_project_service_path(project.namespace, project, service) + - else + = service.title + %td + = chat_name.team_domain + %td + = chat_name.chat_name + %td + - if chat_name.last_used_at + = time_ago_with_tooltip(chat_name.last_used_at) + - else + Never + + %td + = link_to 'Remove', profile_chat_name_path(chat_name), method: :delete, class: 'btn btn-danger pull-right', data: { confirm: 'Are you sure you want to revoke this nickname?' } diff --git a/app/views/profiles/chat_names/index.html.haml b/app/views/profiles/chat_names/index.html.haml new file mode 100644 index 00000000000..20cc636b2da --- /dev/null +++ b/app/views/profiles/chat_names/index.html.haml @@ -0,0 +1,30 @@ +- page_title 'Chat' += render 'profiles/head' + +.row.prepend-top-default + .col-lg-3.profile-settings-sidebar + %h4.prepend-top-0 + = page_title + %p + You can see your Chat accounts. + + .col-lg-9 + %h5 Active chat names (#{@chat_names.size}) + + - if @chat_names.present? + .table-responsive + %table.table.chat-names + %thead + %tr + %th Project + %th Service + %th Team domain + %th Nickname + %th Last used + %th + %tbody + = render @chat_names + + - else + .settings-message.text-center + You don't have any active chat names. diff --git a/app/views/profiles/chat_names/new.html.haml b/app/views/profiles/chat_names/new.html.haml new file mode 100644 index 00000000000..5bf22aa94f1 --- /dev/null +++ b/app/views/profiles/chat_names/new.html.haml @@ -0,0 +1,15 @@ +%h3.page-title Authorization required +%main{ :role => "main" } + %p.h4 + Authorize + %strong.text-info= @chat_name_params[:chat_name] + to use your account? + + %hr + .actions + = form_tag profile_chat_names_path, method: :post do + = hidden_field_tag :token, @chat_name_token.token + = submit_tag "Authorize", class: "btn btn-success wide pull-left" + = form_tag deny_profile_chat_names_path, method: :delete do + = hidden_field_tag :token, @chat_name_token.token + = submit_tag "Deny", class: "btn btn-danger prepend-left-10" diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml index 3276db6692c..d2a60ac2867 100644 --- a/app/views/profiles/keys/_key.html.haml +++ b/app/views/profiles/keys/_key.html.haml @@ -6,6 +6,9 @@ = key.title .description = key.fingerprint + .last-used-at + last used: + = key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : 'n/a' .pull-right %span.key-created-at created #{time_ago_with_tooltip(key.created_at)} diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index dd7615400dc..d44603c638c 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -11,6 +11,9 @@ %li %span.light Created on: %strong= @key.created_at.to_s(:medium) + %li + %span.light Last used on: + %strong= @key.last_used_at.try(:to_s, :medium) || 'N/A' .col-md-8 %p diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index 93187873501..71b224a413b 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -17,5 +17,5 @@ %hr %h5 Your SSH keys (#{@keys.count}) - %div.append-bottom-default + .append-bottom-default = render 'key_table' diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 844fce59704..5c5e5940365 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -3,7 +3,7 @@ %div - if @user.errors.any? - %div.alert.alert-danger + .alert.alert-danger %ul - @user.errors.full_messages.each do |msg| %li= msg @@ -30,7 +30,7 @@ %br .clearfix .form-group.pull-left.global-notification-setting - = render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true + = render 'shared/notifications/button', notification_setting: @global_notification_setting .clearfix diff --git a/app/views/profiles/personal_access_tokens/_form.html.haml b/app/views/profiles/personal_access_tokens/_form.html.haml new file mode 100644 index 00000000000..3f6efa33953 --- /dev/null +++ b/app/views/profiles/personal_access_tokens/_form.html.haml @@ -0,0 +1,21 @@ +- personal_access_token = local_assigns.fetch(:personal_access_token) +- scopes = local_assigns.fetch(:scopes) + += form_for [:profile, personal_access_token], method: :post, html: { class: 'js-requires-input' } do |f| + + = form_errors(personal_access_token) + + .form-group + = f.label :name, class: 'label-light' + = f.text_field :name, class: "form-control", required: true + + .form-group + = f.label :expires_at, class: 'label-light' + = f.text_field :expires_at, class: "datepicker form-control" + + .form-group + = f.label :scopes, class: 'label-light' + = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: personal_access_token, scopes: scopes + + .prepend-top-default + = f.submit 'Create Personal Access Token', class: "btn btn-create" diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 05a2ea67aa2..60a561c9f9c 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -19,7 +19,7 @@ Your New Personal Access Token .form-group = text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control", 'aria-describedby' => "created-personal-access-token-help-block" - = clipboard_button(clipboard_text: flash[:personal_access_token]) + = clipboard_button(clipboard_text: flash[:personal_access_token], title: "Copy personal access token to clipboard", placement: "left") %span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again. %hr @@ -28,21 +28,8 @@ Add a Personal Access Token %p.profile-settings-content Pick a name for the application, and we'll give you a unique token. - = form_for [:profile, @personal_access_token], - method: :post, html: { class: 'js-requires-input' } do |f| - = form_errors(@personal_access_token) - - .form-group - = f.label :name, class: 'label-light' - = f.text_field :name, class: "form-control", required: true - - .form-group - = f.label :expires_at, class: 'label-light' - = f.text_field :expires_at, class: "datepicker form-control", required: false - - .prepend-top-default - = f.submit 'Create Personal Access Token', class: "btn btn-create" + = render "form", personal_access_token: @personal_access_token, scopes: @scopes %hr @@ -56,6 +43,7 @@ %th Name %th Created %th Expires + %th Scopes %th %tbody - @active_personal_access_tokens.each do |token| @@ -67,6 +55,7 @@ = token.expires_at.to_date.to_s(:medium) - else %span.personal-access-tokens-never-expires-label Never + %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>" %td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." } - else diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 2afa026847a..feadd863b00 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -1,7 +1,7 @@ - page_title 'Preferences' = render 'profiles/head' -= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'row prepend-top-default js-preferences-form'} do |f| += form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f| .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 Application theme @@ -10,7 +10,7 @@ .col-lg-9.application-theme - Gitlab::Themes.each do |theme| = label_tag do - .preview{class: theme.css_class} + .preview{ class: theme.css_class } = f.radio_button :theme_id, theme.id = theme.name .col-sm-12 diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 578af9fe98d..c0c82cde2f6 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -18,7 +18,8 @@ or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host} .col-lg-9 .clearfix.avatar-image.append-bottom-default - = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160' + = link_to avatar_icon(@user, 400), target: '_blank' do + = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160' %h5.prepend-top-0 Upload new avatar .prepend-top-5.append-bottom-10 @@ -30,7 +31,7 @@ The maximum file size allowed is 200KB. - if @user.avatar? %hr - = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-gray" + = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?" }, method: :delete, class: "btn btn-gray" %hr .row .col-lg-3.profile-settings-sidebar @@ -69,7 +70,7 @@ %span.help-block We also use email for avatar detection if no avatar is uploaded. .form-group = f.label :public_email, class: "label-light" - = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show on profile'}, class: "select2" + = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), { include_blank: 'Do not show on profile' }, class: "select2" %span.help-block This email will be displayed on your public profile. .form-group = f.label :skype, class: "label-light" @@ -101,14 +102,14 @@ .modal-dialog .modal-content .modal-header - %button.close{:type => "button", :'data-dismiss' => "modal"} + %button.close{ :type => "button", :'data-dismiss' => "modal" } %span × %h4.modal-title Position and size your new avatar .modal-body .profile-crop-image-container - %img.modal-profile-crop-image + %img.modal-profile-crop-image{ alt: "Avatar cropper" } .crop-controls .btn-group %button.btn.btn-primary{ data: { method: "zoom", option: "0.1" } } @@ -116,5 +117,5 @@ %button.btn.btn-primary{ data: { method: "zoom", option: "-0.1" } } %span.fa.fa-search-minus .modal-footer - %button.btn.btn-primary.js-upload-user-avatar{:type => "button"} + %button.btn.btn-primary.js-upload-user-avatar{ :type => "button" } Set new profile picture diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 03ac739ade5..558a1d56151 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -30,7 +30,7 @@ To add the entry manually, provide the following details to the application on your phone. %p.prepend-top-0.append-bottom-0 Account: - = current_user.email + = @account_string %p.prepend-top-0.append-bottom-0 Key: = current_user.otp_secret.scan(/.{4}/).join(' ') diff --git a/app/views/profiles/update_username.js.haml b/app/views/profiles/update_username.js.haml index de1337a2a24..5307e0b48cb 100644 --- a/app/views/profiles/update_username.js.haml +++ b/app/views/profiles/update_username.js.haml @@ -2,5 +2,6 @@ :plain new Flash("Username successfully changed", "notice") - else + - error = @user.errors.full_messages.first :plain - new Flash("Username change failed - #{@user.errors.full_messages.first}", "alert") + new Flash("Username change failed - #{escape_javascript error.html_safe}", "alert") diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index d011e51e696..0ea733cb978 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -9,11 +9,11 @@ = render 'shared/event_filter' - .content_list.project-activity{:"data-href" => activity_project_path(@project)} + .content_list.project-activity{ :"data-href" => activity_project_path(@project) } = spinner :javascript - var activity = new Activities(); + var activity = new gl.Activities(); $(document).on('page:restore', function (event) { activity.reloadActivities() }) diff --git a/app/views/projects/_bitbucket_import_modal.html.haml b/app/views/projects/_bitbucket_import_modal.html.haml index e74fd5b93ea..c24a496486c 100644 --- a/app/views/projects/_bitbucket_import_modal.html.haml +++ b/app/views/projects/_bitbucket_import_modal.html.haml @@ -1,8 +1,8 @@ -%div#bitbucket_import_modal.modal +#bitbucket_import_modal.modal .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h3 Import projects from Bitbucket .modal-body To enable importing projects from Bitbucket, @@ -10,4 +10,4 @@ as administrator you need to configure - else ask your GitLab administrator to configure - == #{link_to 'OAuth integration', help_page_path("integration/bitbucket")}. + = link_to 'OAuth integration', help_page_path("integration/bitbucket") diff --git a/app/views/projects/_customize_workflow.html.haml b/app/views/projects/_customize_workflow.html.haml index d2c1e943db1..e2b73cee5a9 100644 --- a/app/views/projects/_customize_workflow.html.haml +++ b/app/views/projects/_customize_workflow.html.haml @@ -1,5 +1,5 @@ .row-content-block.project-home-empty - %div.text-center{ class: container_class } + .text-center{ class: container_class } %h4 Customize your workflow! %p diff --git a/app/views/projects/_find_file_link.html.haml b/app/views/projects/_find_file_link.html.haml index 08e2fc48be7..dbb33090670 100644 --- a/app/views/projects/_find_file_link.html.haml +++ b/app/views/projects/_find_file_link.html.haml @@ -1,3 +1,3 @@ -= link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do
- = icon('search')
- %span Find File
+= link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do + = icon('search') + %span Find File diff --git a/app/views/projects/_gitlab_import_modal.html.haml b/app/views/projects/_gitlab_import_modal.html.haml index e9f39b16aa7..00aef66e1f8 100644 --- a/app/views/projects/_gitlab_import_modal.html.haml +++ b/app/views/projects/_gitlab_import_modal.html.haml @@ -1,8 +1,8 @@ -%div#gitlab_import_modal.modal +#gitlab_import_modal.modal .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h3 Import projects from GitLab.com .modal-body To enable importing projects from GitLab.com, @@ -10,4 +10,4 @@ as administrator you need to configure - else ask your GitLab administrator to configure - == #{link_to 'OAuth integration', help_page_path("integration/gitlab")}. + = link_to 'OAuth integration', help_page_path("integration/gitlab") diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 5a04c3318cf..1b9d87e9969 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -5,7 +5,7 @@ = project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile') %h1.project-title = @project.name - %span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)} + %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } = visibility_level_icon(@project.visibility_level, fw: false) .project-home-desc @@ -18,11 +18,19 @@ = link_to project_path(forked_from_project) do = forked_from_project.namespace.try(:name) - .project-repo-buttons.project-action-buttons + .project-repo-buttons .count-buttons = render 'projects/buttons/star' = render 'projects/buttons/fork' - - if @project.feature_available?(:repository, current_user) - .project-clone-holder - = render "shared/clone_panel" + %span.hidden-xs + - if @project.feature_available?(:repository, current_user) + .project-clone-holder + = render "shared/clone_panel" + + - if current_user && can?(current_user, :download_code, @project) + = render 'projects/buttons/download', project: @project, ref: @ref + = render 'projects/buttons/dropdown' + = render 'shared/notifications/button', notification_setting: @notification_setting + = render 'projects/buttons/koding' + = render 'shared/members/access_request_buttons', source: @project diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml index 7f530708947..e1fea8ccf3d 100644 --- a/app/views/projects/_last_commit.html.haml +++ b/app/views/projects/_last_commit.html.haml @@ -1,7 +1,8 @@ + - ref = local_assigns.fetch(:ref) - status = commit.status(ref) - if status - = link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do + = link_to pipelines_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do = ci_icon_for_status(status) = ci_label_for_status(status) diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 58d961d93ca..085f79de785 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -15,23 +15,23 @@ %li.pull-right .toolbar-group - = markdown_toolbar_button({icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" }) - = markdown_toolbar_button({icon: "italic fw", data: { "md-tag" => "*" }, title: "Add italic text" }) - = markdown_toolbar_button({icon: "quote-right fw", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" }) - = markdown_toolbar_button({icon: "code fw", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" }) - = markdown_toolbar_button({icon: "list-ul fw", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" }) - = markdown_toolbar_button({icon: "list-ol fw", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" }) - = markdown_toolbar_button({icon: "check-square-o fw", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" }) + = markdown_toolbar_button({ icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" }) + = markdown_toolbar_button({ icon: "italic fw", data: { "md-tag" => "*" }, title: "Add italic text" }) + = markdown_toolbar_button({ icon: "quote-right fw", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" }) + = markdown_toolbar_button({ icon: "code fw", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" }) + = markdown_toolbar_button({ icon: "list-ul fw", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" }) + = markdown_toolbar_button({ icon: "list-ol fw", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" }) + = markdown_toolbar_button({ icon: "check-square-o fw", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" }) .toolbar-group %button.toolbar-btn.js-zen-enter.has-tooltip.hidden-xs{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } } - =icon("arrows-alt fw") + = icon("arrows-alt fw") .md-write-holder = yield - .md.md-preview-holder.js-md-preview.hide{class: (preview_class if defined?(preview_class))} + .md.md-preview-holder.js-md-preview.hide{ class: (preview_class if defined?(preview_class)) } - if defined?(referenced_users) && referenced_users - %div.referenced-users.hide + .referenced-users.hide %span = icon("exclamation-triangle") You are about to add diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml new file mode 100644 index 00000000000..afe2fd7fd7b --- /dev/null +++ b/app/views/projects/_merge_request_merge_settings.html.haml @@ -0,0 +1,15 @@ +- form = local_assigns.fetch(:form) + +.form-group + .checkbox.builds-feature + = form.label :only_allow_merge_if_build_succeeds do + = form.check_box :only_allow_merge_if_build_succeeds + %strong Only allow merge requests to be merged if the build succeeds + %br + %span.descr + Builds need to be configured to enable this feature. + = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds') + .checkbox + = form.label :only_allow_merge_if_all_discussions_are_resolved do + = form.check_box :only_allow_merge_if_all_discussions_are_resolved + %strong Only allow merge requests to be merged if all discussions are resolved diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml index 6e143c4b570..818010bc7d3 100644 --- a/app/views/projects/_merge_request_settings.html.haml +++ b/app/views/projects/_merge_request_settings.html.haml @@ -1,18 +1,8 @@ -.merge-requests-feature - %fieldset.builds-feature - %hr - %h5.prepend-top-0 - Merge Requests - .form-group - .checkbox - = f.label :only_allow_merge_if_build_succeeds do - = f.check_box :only_allow_merge_if_build_succeeds - %strong Only allow merge requests to be merged if the build succeeds - %br - %span.descr - Builds need to be configured to enable this feature. - = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds') - .checkbox - = f.label :only_allow_merge_if_all_discussions_are_resolved do - = f.check_box :only_allow_merge_if_all_discussions_are_resolved - %strong Only allow merge requests to be merged if all discussions are resolved +- form = local_assigns.fetch(:form) + +%fieldset.features.merge-requests-feature.append-bottom-default + %hr + %h5.prepend-top-0 + Merge Requests + + = render 'projects/merge_request_merge_settings', form: form diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml index 369a847e7d4..b6fb08b68e9 100644 --- a/app/views/projects/_readme.html.haml +++ b/app/views/projects/_readme.html.haml @@ -18,5 +18,5 @@ distributed with computer software, forming part of its documentation. %p We recommend you to - = link_to "add a README", new_readme_path, class: 'underlined-link' + = link_to "add a README", add_special_file_path(@project, file_name: 'README.md'), class: 'underlined-link' file to the repository and GitLab will render it here instead of this message. 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/_wiki.html.haml b/app/views/projects/_wiki.html.haml index f00422dd7c0..41d42740f61 100644 --- a/app/views/projects/_wiki.html.haml +++ b/app/views/projects/_wiki.html.haml @@ -7,7 +7,7 @@ - else - can_create_wiki = can?(current_user, :create_wiki, @project) .project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] } - %div.text-center{ class: container_class } + .text-center{ class: container_class } %h4 This project does not have a wiki homepage yet - if can_create_wiki diff --git a/app/views/projects/artifacts/_tree_directory.html.haml b/app/views/projects/artifacts/_tree_directory.html.haml index def493c56f5..9e49c93388a 100644 --- a/app/views/projects/artifacts/_tree_directory.html.haml +++ b/app/views/projects/artifacts/_tree_directory.html.haml @@ -1,6 +1,6 @@ - path_to_directory = browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: directory.path) -%tr.tree-item{ 'data-link' => path_to_directory} +%tr.tree-item{ 'data-link' => path_to_directory } %td.tree-item-file-name = tree_icon('folder', '755', directory.name) %span.str-truncated diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index 539d07d634a..d0ff14e45e6 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -1,5 +1,4 @@ - page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds' -- header_title project_title(@project, "Builds", project_builds_path(@project)) .top-block.row-content-block.clearfix .pull-right @@ -9,7 +8,7 @@ Download artifacts archive .tree-holder - %div.tree-content-holder + .tree-content-holder %table.table.tree-table %thead %tr diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index cadfe5a3e30..23f54553014 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -31,12 +31,12 @@ .light = commit_author_link(commit, avatar: false) - authored + committed #{time_ago_with_tooltip(commit.committed_date)} %td.line-numbers - line_count = blame_group[:lines].count - (current_line...(current_line + line_count)).each do |i| - %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i} + %a.diff-line-num{ href: "#L#{i}", id: "L#{i}", 'data-line-number' => i } = icon("link") = i \ diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 149ee7c59d6..f75f438ee4f 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -18,11 +18,11 @@ - else = link_to title, '#' -%ul.blob-commit-info.hidden-xs +%ul.blob-commit-info.table-list.hidden-xs - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) = render blob_commit, project: @project, ref: @ref -%div#blob-content-holder.blob-content-holder +#blob-content-holder.blob-content-holder %article.file-holder .file-title = blob_icon blob.mode, blob.name diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 4a6aa92e3f3..1d058daa094 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -21,6 +21,8 @@ = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } ) .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden = dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } ) + .dockerfile-selector.js-dockerfile-selector-wrap.hidden + = dropdown_tag("Choose a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } ) = button_tag class: 'soft-wrap-toggle btn', type: 'button' do %span.no-wrap = custom_icon('icon_no_wrap') diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml index 4c356d1f07f..a486b2fe491 100644 --- a/app/views/projects/blob/_image.html.haml +++ b/app/views/projects/blob/_image.html.haml @@ -5,11 +5,11 @@ - # be wrong/strange if RawController modified the data. - blob.load_all_data!(@repository) - blob = sanitize_svg(blob) - %img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"} + %img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}", alt: "#{blob.name}" } - else .nothing-here-block The SVG could not be displayed as it is too large, you can #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank')} instead. - else - %img{src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path))} + %img{ src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path)), alt: "#{blob.name}" } diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml index 84694203d7d..7f470b890ba 100644 --- a/app/views/projects/blob/_new_dir.html.haml +++ b/app/views/projects/blob/_new_dir.html.haml @@ -2,7 +2,7 @@ .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title Create New Directory .modal-body = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-quick-submit js-requires-input' do diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml index 2e1f32fd15e..db6662a95ac 100644 --- a/app/views/projects/blob/_remove.html.haml +++ b/app/views/projects/blob/_remove.html.haml @@ -2,7 +2,7 @@ .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title Delete #{@blob.name} .modal-body diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index 57a27ec904e..61a7ffdd0ab 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -2,7 +2,7 @@ .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title #{title} .modal-body = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal' do @@ -12,7 +12,7 @@ Attach a file by drag & drop or = link_to 'click to upload', '#', class: "markdown-selector" %br - .dropzone-alerts{class: "alert alert-danger data", style: "display:none"} + .dropzone-alerts.alert.alert-danger.data{ style: "display:none" } = render 'shared/new_commit_form', placeholder: placeholder diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index a79ae53c780..538f8591f13 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -13,15 +13,15 @@ - case diff_view - when :inline %td.old_line.diff-line-num{ data: { linenumber: line_old } } - %a{href: "##{line_old}", data: { linenumber: line_old }} + %a{ href: "##{line_old}", data: { linenumber: line_old } } %td.new_line.diff-line-num{ data: { linenumber: line_new } } - %a{href: "##{line_new}", data: { linenumber: line_new }} + %a{ href: "##{line_new}", data: { linenumber: line_new } } = line_content - when :parallel - %td.old_line.diff-line-num{data: { linenumber: line_old }} + %td.old_line.diff-line-num{ data: { linenumber: line_old } } = link_to raw(line_old), "##{line_old}" = line_content - %td.new_line.diff-line-num{data: { linenumber: line_new }} + %td.new_line.diff-line-num{ data: { linenumber: line_new } } = link_to raw(line_new), "##{line_new}" = line_content diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 2a0352a71b7..a5dcd93f42e 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -27,5 +27,5 @@ = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}" = hidden_field_tag 'last_commit_sha', @last_commit_sha = hidden_field_tag 'content', '', id: "file-content" - = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] + = hidden_field_tag 'from_merge_request_iid', params[:from_merge_request_iid] = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id) diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml index 541dc96c45f..5cafb644b40 100644 --- a/app/views/projects/blob/preview.html.haml +++ b/app/views/projects/blob/preview.html.haml @@ -20,6 +20,6 @@ - else %td.old_line.diff-line-num %td.new_line.diff-line-num - %td.line_content{class: "#{line.type}"}= diff_line_content(line.text) + %td.line_content{ class: "#{line.type}" }= diff_line_content(line.text) - else .nothing-here-block No changes. diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index 0ab78a39cf9..b6738c3380f 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -5,7 +5,7 @@ %div{ class: container_class } = render 'projects/last_push' - %div#tree-holder.tree-holder + #tree-holder.tree-holder = render 'blob', blob: @blob - if can_edit_blob?(@blob) diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml new file mode 100644 index 00000000000..356bd50f7f3 --- /dev/null +++ b/app/views/projects/boards/_show.html.haml @@ -0,0 +1,28 @@ +- @no_container = true +- @content_class = "issue-boards-content" +- page_title "Boards" + +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('boards/boards_bundle.js') + = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test? + + %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board" + %script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list" + %script#js-board-list-card{ type: "text/x-template" }= render "projects/boards/components/card" + += render "projects/issues/head" + += render 'shared/issuable/filter', type: :boards + +#board-app.boards-app{ "v-cloak" => true, data: board_data } + .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" } + .boards-app-loading.text-center{ "v-if" => "loading" } + = icon("spinner spin") + %board{ "v-cloak" => true, + "v-for" => "list in state.lists", + "ref" => "board", + ":list" => "list", + ":disabled" => "disabled", + ":issue-link-base" => "issueLinkBase", + ":key" => "_uid" } + = render "projects/boards/components/sidebar" diff --git a/app/views/projects/boards/components/_blank_state.html.haml b/app/views/projects/boards/components/_blank_state.html.haml index 97eb952eff1..0af40ddf8fe 100644 --- a/app/views/projects/boards/components/_blank_state.html.haml +++ b/app/views/projects/boards/components/_blank_state.html.haml @@ -1,5 +1,5 @@ %board-blank-state{ "inline-template" => true, - "v-if" => "list.id == 'blank'" } + "v-if" => 'list.id == "blank"' } .board-blank-state %p Add the following default lists to your Issue Board with one click: diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index f7071051efc..a2e5118a9f3 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -1,80 +1,34 @@ -%board{ "inline-template" => true, - "v-cloak" => true, - "v-for" => "list in state.lists | orderBy 'position'", - "v-ref:board" => true, - ":list" => "list", - ":disabled" => "disabled", - ":issue-link-base" => "issueLinkBase", - "track-by" => "_uid" } - .board{ ":class" => "{ 'is-draggable': !list.preset }", - ":data-id" => "list.id" } - .board-inner - %header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" } - %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" } - %span.has-tooltip{ ":title" => "(list.label ? list.label.description : '')", - data: { container: "body", placement: "bottom" } } - {{ list.title }} - .board-issue-count-holder.pull-right.clearfix{ "v-if" => "list.type !== 'blank'" } - %span.board-issue-count.pull-left{ ":class" => "{ 'has-btn': list.type !== 'done' }" } - {{ list.issuesSize }} - - if can?(current_user, :admin_issue, @project) - %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button", - "@click" => "showNewIssueForm", - "v-if" => "list.type !== 'done'", - "aria-label" => "Add an issue", - "title" => "Add an issue", - data: { placement: "top", container: "body" } } - = icon("plus") - - if can?(current_user, :admin_list, @project) - %board-delete{ "inline-template" => true, - ":list" => "list", - "v-if" => "!list.preset && list.id" } - %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" } - = icon("trash") - %board-list{ "inline-template" => true, - "v-if" => "list.type !== 'blank'", - ":list" => "list", - ":issues" => "list.issues", - ":loading" => "list.loading", - ":disabled" => "disabled", - ":show-issue-form.sync" => "showIssueForm", - ":issue-link-base" => "issueLinkBase" } - .board-list-loading.text-center{ "v-if" => "loading" } - = icon("spinner spin") - - if can? current_user, :create_issue, @project - %board-new-issue{ "inline-template" => true, +.board{ ":class" => '{ "is-draggable": !list.preset }', + ":data-id" => "list.id" } + .board-inner + %header.board-header{ ":class" => '{ "has-border": list.label }', ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" } + %h3.board-title.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset) }' } + %span.has-tooltip{ ":title" => '(list.label ? list.label.description : "")', + data: { container: "body", placement: "bottom" } } + {{ list.title }} + .board-issue-count-holder.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' } + %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "done" && !disabled }' } + {{ list.issuesSize }} + - if can?(current_user, :admin_issue, @project) + %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button", + "@click" => "showNewIssueForm", + "v-if" => 'list.type !== "done"', + "aria-label" => "Add an issue", + "title" => "Add an issue", + data: { placement: "top", container: "body" } } + = icon("plus") + - if can?(current_user, :admin_list, @project) + %board-delete{ "inline-template" => true, ":list" => "list", - ":show-issue-form.sync" => "showIssueForm", - "v-show" => "list.type !== 'done' && showIssueForm" } - .card.board-new-issue-form - %form{ "@submit" => "submit($event)" } - .flash-container{ "v-if" => "error" } - .flash-alert - An error occured. Please try again. - %label.label-light{ ":for" => "list.id + '-title'" } - Title - %input.form-control{ type: "text", - "v-model" => "title", - "v-el:input" => true, - ":id" => "list.id + '-title'" } - .clearfix.prepend-top-10 - %button.btn.btn-success.pull-left{ type: "submit", - ":disabled" => "title === ''", - "v-el:submit-button" => true } - Submit issue - %button.btn.btn-default.pull-right{ type: "button", - "@click" => "cancel" } - Cancel - %ul.board-list{ "v-el:list" => true, - "v-show" => "!loading", - ":data-board" => "list.id", - ":class" => "{ 'is-smaller': showIssueForm }" } - = render "projects/boards/components/card" - %li.board-list-count.text-center{ "v-if" => "showCount" } - = icon("spinner spin", "v-show" => "list.loadingMore" ) - %span{ "v-if" => "list.issues.length === list.issuesSize" } - Showing all issues - %span{ "v-else" => true } - Showing {{ list.issues.length }} of {{ list.issuesSize }} issues - - if can?(current_user, :admin_list, @project) - = render "projects/boards/components/blank_state" + "v-if" => "!list.preset && list.id" } + %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" } + = icon("trash") + %board-list{ "v-if" => 'list.type !== "blank"', + ":list" => "list", + ":issues" => "list.issues", + ":loading" => "list.loading", + ":disabled" => "disabled", + ":issue-link-base" => "issueLinkBase", + "ref" => "board-list" } + - if can?(current_user, :admin_list, @project) + = render "projects/boards/components/blank_state" diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml new file mode 100644 index 00000000000..34fdb1f6a74 --- /dev/null +++ b/app/views/projects/boards/components/_board_list.html.haml @@ -0,0 +1,44 @@ +.board-list-component + .board-list-loading.text-center{ "v-if" => "loading" } + = icon("spinner spin") + - if can? current_user, :create_issue, @project + %board-new-issue{ "inline-template" => true, + ":list" => "list", + "v-if" => 'list.type !== "done" && showIssueForm' } + .card.board-new-issue-form + %form{ "@submit" => "submit($event)" } + .flash-container{ "v-if" => "error" } + .flash-alert + An error occured. Please try again. + %label.label-light{ ":for" => 'list.id + "-title"' } + Title + %input.form-control{ type: "text", + "v-model" => "title", + "ref" => "input", + ":id" => 'list.id + "-title"' } + .clearfix.prepend-top-10 + %button.btn.btn-success.pull-left{ type: "submit", + ":disabled" => 'title === ""', + "ref" => "submit-button" } + Submit issue + %button.btn.btn-default.pull-right{ type: "button", + "@click" => "cancel" } + Cancel + %ul.board-list{ "ref" => "list", + "v-show" => "!loading", + ":data-board" => "list.id", + ":class" => '{ "is-smaller": showIssueForm }' } + %board-card{ "v-for" => "(issue, index) in orderedIssues", + "ref" => "issue", + ":index" => "index", + ":list" => "list", + ":issue" => "issue", + ":issue-link-base" => "issueLinkBase", + ":disabled" => "disabled", + ":key" => "issue.id" } + %li.board-list-count.text-center{ "v-if" => "showCount" } + = icon("spinner spin", "v-show" => "list.loadingMore" ) + %span{ "v-if" => "list.issues.length === list.issuesSize" } + Showing all issues + %span{ "v-else" => true } + Showing {{ list.issues.length }} of {{ list.issuesSize }} issues diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml index 8fce702314c..e4c2aff46ec 100644 --- a/app/views/projects/boards/components/_card.html.haml +++ b/app/views/projects/boards/components/_card.html.haml @@ -1,36 +1,28 @@ -%board-card{ "inline-template" => true, - "v-for" => "issue in issues | orderBy 'priority'", - "v-ref:issue" => true, - ":index" => "$index", - ":list" => "list", - ":issue" => "issue", - ":issue-link-base" => "issueLinkBase", - ":disabled" => "disabled", - "track-by" => "id" } - %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }", - ":index" => "index", - "@mousedown" => "mouseDown", - "@mouseMove" => "mouseMove", - "@mouseup" => "showIssue($event)" } - %h4.card-title - = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential") - %a{ ":href" => "issueLinkBase + '/' + issue.id", - ":title" => "issue.title" } - {{ issue.title }} - .card-footer - %span.card-number{ "v-if" => "issue.id" } - = precede '#' do - {{ issue.id }} - %a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username", - ":title" => "'Assigned to ' + issue.assignee.name", - "v-if" => "issue.assignee", - data: { container: 'body' } } - %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 } - %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels", - type: "button", - "v-if" => "(!list.label || label.id !== list.label.id)", - "@click" => "filterByLabel(label, $event)", - ":style" => "{ backgroundColor: label.color, color: label.textColor }", - ":title" => "label.description", - data: { container: 'body' } } - {{ label.title }} +%li.card{ ":class" => '{ "user-can-drag": !disabled && issue.id, "is-disabled": disabled || !issue.id, "is-active": issueDetailVisible }', + ":index" => "index", + ":data-issue-id" => "issue.id", + "@mousedown" => "mouseDown", + "@mousemove" => "mouseMove", + "@mouseup" => "showIssue($event)" } + %h4.card-title + = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential") + %a{ ":href" => 'issueLinkBase + "/" + issue.id', + ":title" => "issue.title" } + {{ issue.title }} + .card-footer + %span.card-number{ "v-if" => "issue.id" } + = precede '#' do + {{ issue.id }} + %a.has-tooltip{ ":href" => "\"#{root_path}\" + issue.assignee.username", + ":title" => '"Assigned to " + issue.assignee.name', + "v-if" => "issue.assignee", + data: { container: 'body' } } + %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20, alt: "Avatar" } + %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels", + type: "button", + "v-if" => "(!list.label || label.id !== list.label.id)", + "@click" => "filterByLabel(label, $event)", + ":style" => "{ backgroundColor: label.color, color: label.textColor }", + ":title" => "label.description", + data: { container: 'body' } } + {{ label.title }} diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml index f0c0c6953e0..df7fa9ddaf2 100644 --- a/app/views/projects/boards/components/_sidebar.html.haml +++ b/app/views/projects/boards/components/_sidebar.html.haml @@ -1,23 +1,24 @@ %board-sidebar{ "inline-template" => true, - ":current-user" => "#{current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) if current_user}" } - %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" } - .issuable-sidebar - .block.issuable-sidebar-header - %span.issuable-header-text.hide-collapsed.pull-left - %strong - {{ issue.title }} - %br/ - %span - = precede "#" do - {{ issue.id }} - %a.gutter-toggle.pull-right{ role: "button", - href: "#", - "@click.prevent" => "closeSidebar", - "aria-label" => "Toggle sidebar" } - = custom_icon("icon_close", size: 15) - .js-issuable-update - = render "projects/boards/components/sidebar/assignee" - = render "projects/boards/components/sidebar/milestone" - = render "projects/boards/components/sidebar/due_date" - = render "projects/boards/components/sidebar/labels" - = render "projects/boards/components/sidebar/notifications" + ":current-user" => "#{current_user ? current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) : {}}" } + %transition{ name: "boards-sidebar-slide" } + %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" } + .issuable-sidebar + .block.issuable-sidebar-header + %span.issuable-header-text.hide-collapsed.pull-left + %strong + {{ issue.title }} + %br/ + %span + = precede "#" do + {{ issue.id }} + %a.gutter-toggle.pull-right{ role: "button", + href: "#", + "@click.prevent" => "closeSidebar", + "aria-label" => "Toggle sidebar" } + = custom_icon("icon_close", size: 15) + .js-issuable-update + = render "projects/boards/components/sidebar/assignee" + = render "projects/boards/components/sidebar/milestone" + = render "projects/boards/components/sidebar/due_date" + = render "projects/boards/components/sidebar/labels" + = render "projects/boards/components/sidebar/notifications" diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml index 604e13858d1..e75ce305440 100644 --- a/app/views/projects/boards/components/sidebar/_assignee.html.haml +++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml @@ -1,8 +1,8 @@ .block.assignee .title.hide-collapsed Assignee - = icon("spinner spin", class: "block-loading") - if can?(current_user, :admin_issue, @project) + = icon("spinner spin", class: "block-loading") = link_to "Edit", "#", class: "edit-link pull-right" .value.hide-collapsed %span.assign-yourself.no-value{ "v-if" => "!issue.assignee" } @@ -14,7 +14,7 @@ %a.author_link.bold{ ":href" => "'#{root_url}' + issue.assignee.username", "v-if" => "issue.assignee" } %img.avatar.avatar-inline.s32{ ":src" => "issue.assignee.avatar", - width: "32" } + width: "32", alt: "Avatar" } %span.author {{ issue.assignee.name }} %span.username diff --git a/app/views/projects/boards/components/sidebar/_due_date.html.haml b/app/views/projects/boards/components/sidebar/_due_date.html.haml index c7da1d0d4ac..1a3b88e28c5 100644 --- a/app/views/projects/boards/components/sidebar/_due_date.html.haml +++ b/app/views/projects/boards/components/sidebar/_due_date.html.haml @@ -1,8 +1,8 @@ .block.due_date .title Due date - = icon("spinner spin", class: "block-loading") - if can?(current_user, :admin_issue, @project) + = icon("spinner spin", class: "block-loading") = link_to "Edit", "#", class: "edit-link pull-right" .value .value-content diff --git a/app/views/projects/boards/components/sidebar/_labels.html.haml b/app/views/projects/boards/components/sidebar/_labels.html.haml index ce68e5e1998..0f0a84c156d 100644 --- a/app/views/projects/boards/components/sidebar/_labels.html.haml +++ b/app/views/projects/boards/components/sidebar/_labels.html.haml @@ -1,8 +1,8 @@ .block.labels .title Labels - = icon("spinner spin", class: "block-loading") - if can?(current_user, :admin_issue, @project) + = icon("spinner spin", class: "block-loading") = link_to "Edit", "#", class: "edit-link pull-right" .value.issuable-show-labels %span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" } diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/projects/boards/components/sidebar/_milestone.html.haml index 3cd20d1c0f7..008d1186478 100644 --- a/app/views/projects/boards/components/sidebar/_milestone.html.haml +++ b/app/views/projects/boards/components/sidebar/_milestone.html.haml @@ -1,8 +1,8 @@ .block.milestone .title Milestone - = icon("spinner spin", class: "block-loading") - if can?(current_user, :admin_issue, @project) + = icon("spinner spin", class: "block-loading") = link_to "Edit", "#", class: "edit-link pull-right" .value %span.no-value{ "v-if" => "!issue.milestone" } diff --git a/app/views/projects/boards/components/sidebar/_notifications.html.haml b/app/views/projects/boards/components/sidebar/_notifications.html.haml index 21c9563e9db..a08c7f2af09 100644 --- a/app/views/projects/boards/components/sidebar/_notifications.html.haml +++ b/app/views/projects/boards/components/sidebar/_notifications.html.haml @@ -1,11 +1,7 @@ - if current_user .block.light.subscription{ ":data-url" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '/toggle_subscription'" } - .title + %span.issuable-header-text.hide-collapsed.pull-left Notifications - %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } - {{ issue.subscribed ? 'Unsubscribe' : 'Subscribe' }} - .subscription-status{ ":data-status" => "issue.subscribed ? 'subscribed' : 'unsubscribed'" } - .unsubscribed{ "v-show" => "!issue.subscribed" } - You're not receiving notifications from this thread. - .subscribed{ "v-show" => "issue.subscribed" } - You're receiving notifications because you're subscribed to this thread. + %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } + %span + {{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}} diff --git a/app/views/projects/boards/index.html.haml b/app/views/projects/boards/index.html.haml index 29c9a43a0c1..2a5b8b1441e 100644 --- a/app/views/projects/boards/index.html.haml +++ b/app/views/projects/boards/index.html.haml @@ -1,18 +1 @@ -- @no_container = true -- @content_class = "issue-boards-content" -- page_title "Boards" - -- content_for :page_specific_javascripts do - = page_specific_javascript_tag('boards/boards_bundle.js') - = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test? - -= render "projects/issues/head" - -= render 'shared/issuable/filter', type: :boards - -#board-app.boards-app{ "v-cloak" => true, data: board_data } - .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" } - .boards-app-loading.text-center{ "v-if" => "loading" } - = icon("spinner spin") - = render "projects/boards/components/board" - = render "projects/boards/components/sidebar" += render "show" diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml index 29c9a43a0c1..2a5b8b1441e 100644 --- a/app/views/projects/boards/show.html.haml +++ b/app/views/projects/boards/show.html.haml @@ -1,18 +1 @@ -- @no_container = true -- @content_class = "issue-boards-content" -- page_title "Boards" - -- content_for :page_specific_javascripts do - = page_specific_javascript_tag('boards/boards_bundle.js') - = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test? - -= render "projects/issues/head" - -= render 'shared/issuable/filter', type: :boards - -#board-app.boards-app{ "v-cloak" => true, data: board_data } - .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" } - .boards-app-loading.text-center{ "v-if" => "loading" } - = icon("spinner spin") - = render "projects/boards/components/board" - = render "projects/boards/components/sidebar" += render "show" diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 9135cee8364..04efc2e996c 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -4,7 +4,7 @@ - number_commits_behind = diverging_commit_counts[:behind] - number_commits_ahead = diverging_commit_counts[:ahead] - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) -%li(class="js-branch-#{branch.name}") +%li{ class: "js-branch-#{branch.name}" } %div = link_to namespace_project_tree_path(@project.namespace, @project, branch.name), class: 'item-title str-truncated' do = branch.name @@ -12,12 +12,11 @@ - if branch.name == @repository.root_ref %span.label.label-primary default - elsif @repository.merged_to_root_ref? branch.name - %span.label.label-info.has-tooltip(title="Merged into #{@repository.root_ref}") + %span.label.label-info.has-tooltip{ title: "Merged into #{@repository.root_ref}" } merged - if @project.protected_branch? branch.name %span.label.label-success - %i.fa.fa-lock protected .controls.hidden-xs - if merge_project && create_mr_button?(@repository.root_ref, branch.name) diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 84f38575e84..5f8f56150f9 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -3,19 +3,20 @@ = render "projects/commits/head" %div{ class: container_class } - .top-area + .top-area.adjust .nav-text - Protected branches can be managed in project settings + Protected branches can be managed in + = link_to 'project settings', namespace_project_protected_branches_path(@project.namespace, @project) .nav-controls = form_tag(filter_branches_path, method: :get) do = search_field_tag :search, params[:search], { placeholder: 'Filter by branch name', id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false } .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span.light = projects_sort_options_hash[@sort] - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_branches_path(sort: sort_value_name) do @@ -26,6 +27,8 @@ = sort_title_oldest_updated - if can? current_user, :push_code, @project + = link_to namespace_project_merged_branches_path(@project.namespace, @project), class: 'btn btn-inverted btn-remove has-tooltip', title: "Delete all branches that are merged into '#{@project.repository.root_ref}'", method: :delete, data: { confirm: "Deleting the merged branches cannot be undone. Are you sure?", container: 'body' } do + Delete merged branches = link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do New branch diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index 5a6c8c243fa..e63bdb38bd8 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -2,7 +2,7 @@ - if @error .alert.alert-danger - %button{ type: "button", class: "close", "data-dismiss" => "alert"} × + %button.close{ type: "button", "data-dismiss" => "alert" } × = @error %h3.page-title New Branch diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index 3f2ce7377fd..b15be0d861d 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,10 +1,13 @@ .content-block.build-header .header-content - = ci_status_with_icon(@build.status) + = render 'ci/status/badge', status: @build.detailed_status(current_user) Build %strong ##{@build.id} + in pipeline + = link_to pipeline_path(@build.pipeline) do + %strong ##{@build.pipeline.id} for commit - = link_to ci_status_path(@build.pipeline) do + = link_to namespace_project_commit_path(@project.namespace, @project, @build.pipeline.sha) do %strong= @build.pipeline.short_sha from = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do @@ -14,6 +17,6 @@ = render "user" = time_ago_with_tooltip(@build.created_at) - if can?(current_user, :update_build, @build) && @build.retryable? - = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted pull-right', method: :post + = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 28f519f11b2..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 @@ -111,28 +111,29 @@ %span.label.label-primary = tag - - if @build.pipeline.stages.many? + - if @build.pipeline.stages_count > 1 .dropdown.build-dropdown .title Stage - %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span.stage-selection More - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu - @build.pipeline.stages.each do |stage| %li - %a.stage-item= stage + %a.stage-item= stage.name .builds-container - HasStatus::ORDERED_STATUSES.each do |build_status| - builds.select{|build| build.status == build_status}.each do |build| - .build-job{class: sidebar_build_class(build, @build), data: {stage: build.stage}} + .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } } = link_to namespace_project_build_path(@project.namespace, @project, build) do = icon('arrow-right') - = ci_icon_for_status(build.status) + %span{ class: "ci-status-icon-#{build.status}" } + = ci_icon_for_status(build.status) %span - if build.name = build.name - else = build.id - if build.retried? - %i.fa.fa-refresh.has-tooltip{data: { container: 'body', placement: 'bottom' }, title: 'Build was retried'} + %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Build was retried' } diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml index 36294c89fa8..028664f5bba 100644 --- a/app/views/projects/builds/_table.html.haml +++ b/app/views/projects/builds/_table.html.haml @@ -10,6 +10,7 @@ %tr %th Status %th Build + %th Pipeline - if admin %th Project %th Runner @@ -19,6 +20,6 @@ %th Coverage %th - = render partial: "projects/ci/builds/build", collection: builds, as: :build, locals: { commit_sha: true, ref: true, stage: true, allow_retry: true, coverage: admin || project.build_coverage_enabled?, admin: admin } + = render partial: "projects/ci/builds/build", collection: builds, as: :build, locals: { commit_sha: true, ref: true, pipeline_link: true, stage: true, allow_retry: true, coverage: admin || project.build_coverage_enabled?, admin: admin } = paginate builds, theme: 'gitlab' diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 06070f12bbd..c623e39b21f 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -19,5 +19,5 @@ = link_to ci_lint_path, class: 'btn btn-default' do %span CI Lint - %div.content-list.builds-content-list + .content-list.builds-content-list = render "table", builds: @builds, project: @project diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index ae7a7ecb392..c613e473e4c 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,6 +1,6 @@ - @no_container = true - page_title "#{@build.name} (##{@build.id})", "Builds" -- header_title project_title(@project, "Builds", project_builds_path(@project)) +- trace_with_state = @build.trace_with_state = render "projects/pipelines/head", build_subnav: true %div{ class: container_class } @@ -26,25 +26,54 @@ = link_to namespace_project_runners_path(@build.project.namespace, @build.project) do Runners page + - if @build.starts_environment? + .prepend-top-default + .environment-information + - if @build.outdated_deployment? + = ci_icon_for_status('success_with_warnings') + - else + = ci_icon_for_status(@build.status) + + - environment = environment_for_build(@build.project, @build) + - if @build.success? && @build.last_deployment.present? + - if @build.last_deployment.last? + This build is the most recent deployment to #{environment_link_for_build(@build.project, @build)}. + - else + This build is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}. + View the most recent deployment #{deployment_link(environment.last_deployment)}. + - elsif @build.complete? && !@build.success? + The deployment of this build to #{environment_link_for_build(@build.project, @build)} did not succeed. + - else + This build is creating a deployment to #{environment_link_for_build(@build.project, @build)} + - if environment.try(:last_deployment) + and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')} + .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 - = link_to '#build-trace', class: 'btn' do - %i.fa.fa-angle-up - = link_to '#down-build-trace', class: 'btn' do - %i.fa.fa-angle-down + %a.scroll-link.scroll-top{ href: '#up-build-trace', id: 'scroll-top', title: 'Scroll to top' } + = custom_icon('scroll_up') + = custom_icon('scroll_up_hover_active') + %a.scroll-link.scroll-bottom{ href: '#down-build-trace', id: 'scroll-bottom', title: 'Scroll to bottom' } + = custom_icon('scroll_down') + = custom_icon('scroll_down_hover_active') - if @build.active? .autoscroll-container - %button.btn.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} - Enable autoscroll + %span.status-message#autoscroll-status{ data: { state: 'disabled' } } + %span.status-text Autoscroll active + %i.status-icon + = custom_icon('scroll_down_hover_active') + #up-build-trace %pre.build-trace#build-trace %code.bash.js-build-output - = icon("refresh spin", class: "js-build-refresh") + .build-loader-animation.js-build-refresh #down-build-trace diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 7e83a88913a..324a7f8cd3f 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,42 +1,41 @@ - if !project.empty_repo? && can?(current_user, :download_code, project) - %span{class: 'hidden-xs hidden-sm download-button'} - .dropdown.inline - %button.btn{ 'data-toggle' => 'dropdown' } - = icon('download') - = icon("caret-down") - %span.sr-only - Select Archive Format - %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } - %li.dropdown-header Source code - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do - %i.fa.fa-download - %span Download zip - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.gz - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.bz2 - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar + .project-action-button.dropdown.inline + %button.btn{ 'data-toggle' => 'dropdown' } + = icon('download') + = icon("caret-down") + %span.sr-only + Select Archive Format + %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } + %li.dropdown-header Source code + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do + %i.fa.fa-download + %span Download zip + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.gz + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.bz2 + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar - - pipeline = project.pipelines.latest_successful_for(ref) - - if pipeline - - artifacts = pipeline.builds.latest.with_artifacts - - if artifacts.any? - %li.dropdown-header Artifacts - - unless pipeline.latest? - - latest_pipeline = project.pipeline_for(ref) - %li - .unclickable= ci_status_for_statuseable(latest_pipeline) - %li.dropdown-header Previous Artifacts - - artifacts.each do |job| - %li - = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do - %i.fa.fa-download - %span Download '#{job.name}' + - pipeline = project.pipelines.latest_successful_for(ref) + - if pipeline + - artifacts = pipeline.builds.latest.with_artifacts + - if artifacts.any? + %li.dropdown-header Artifacts + - unless pipeline.latest? + - latest_pipeline = project.pipeline_for(ref) + %li + .unclickable= ci_status_for_statuseable(latest_pipeline) + %li.dropdown-header Previous Artifacts + - artifacts.each do |job| + %li + = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do + %i.fa.fa-download + %span Download '#{job.name}' diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 6cd9b98a706..67de8699b2e 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -1,6 +1,6 @@ - if current_user - .dropdown.inline.project-dropdown - %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} + .project-action-button.dropdown.inline + %a.btn.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" } = icon('plus') = icon("caret-down") %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 27da86b9efe..851fe44a86d 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -8,7 +8,7 @@ = link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn' do = custom_icon('icon_fork') %span Fork - %div.count-with-arrow + .count-with-arrow %span.arrow = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count' do = @project.forks_count diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml index fdc80d44253..5d9a776da89 100644 --- a/app/views/projects/buttons/_koding.html.haml +++ b/app/views/projects/buttons/_koding.html.haml @@ -1,7 +1,3 @@ -- if koding_enabled? && current_user && can_push_branch?(@project, @project.default_branch) - - if @repository.koding_yml - = link_to koding_project_url(@project), class: 'btn', target: '_blank' do - Run in IDE (Koding) - - else - = link_to add_koding_stack_path(@project), class: 'btn' do - Set Up Koding +- if koding_enabled? && current_user && @repository.koding_yml && can_push_branch?(@project, @project.default_branch) + = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank' do + Run in IDE (Koding) diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index 12d35101770..d57eb2cbfbc 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -6,7 +6,7 @@ - else = icon('star-o') %span Star - %div.count-with-arrow + .count-with-arrow %span.arrow %span.count.star-count = @project.star_count @@ -15,7 +15,7 @@ = link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: 'You must sign in to star a project' do = icon('star') Star - %div.count-with-arrow + .count-with-arrow %span.arrow %span.count = @project.star_count diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 94632056b15..520113639b7 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -2,16 +2,14 @@ - ref = local_assigns.fetch(:ref, nil) - commit_sha = local_assigns.fetch(:commit_sha, nil) - retried = local_assigns.fetch(:retried, false) +- pipeline_link = local_assigns.fetch(:pipeline_link, false) - stage = local_assigns.fetch(:stage, false) - coverage = local_assigns.fetch(:coverage, false) - allow_retry = local_assigns.fetch(:allow_retry, false) -%tr.build.commit{class: ('retried' if retried)} +%tr.build.commit{ class: ('retried' if retried) } %td.status - - if can?(current_user, :read_build, build) - = ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build)) - - else - = ci_status_with_icon(build.status) + = render "ci/status/badge", status: build.detailed_status(current_user) %td.branch-commit - if can?(current_user, :read_build, build) @@ -51,12 +49,20 @@ - if build.manual? %span.label.label-info manual + - if pipeline_link + %td + = link_to pipeline_path(build.pipeline) do + %span.pipeline-id ##{build.pipeline.id} + %span by + - if build.pipeline.user + = user_avatar(user: build.pipeline.user, size: 20) + - else + %span.monospace API + - if admin %td - if build.project = link_to build.project.name_with_namespace, admin_namespace_project_path(build.project.namespace, build.project) - - - if admin %td - if build.try(:runner) = runner_link(build.runner) @@ -82,9 +88,8 @@ %span #{time_ago_with_tooltip(build.finished_at)} %td.coverage - - if coverage - - if build.try(:coverage) - #{build.coverage}% + - if coverage && build.try(:coverage) + #{build.coverage}% %td .pull-right @@ -96,9 +101,9 @@ = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do = icon('remove', class: 'cred') - elsif allow_retry - - if build.retryable? - = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do - = icon('repeat') - - elsif build.playable? && !admin + - if build.playable? && !admin = link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do = custom_icon('icon_play') + - elsif build.retryable? + = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do + = icon('repeat') diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml deleted file mode 100644 index 93dca81e6f9..00000000000 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- is_playable = subject.playable? && can?(current_user, :update_build, @project) -- if is_playable - = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } do - = ci_icon_for_status('play') - .ci-status-text= subject.name -- elsif can?(current_user, :read_build, @project) - = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do - %span.ci-status-icon - = ci_icon_for_status(subject.status) - .ci-status-text= subject.name -- else - %span.ci-status-icon - = ci_icon_for_status(subject.status) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 2a2d24be736..990bfbcf951 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -4,8 +4,7 @@ %tr.commit %td.commit-link - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do - = ci_status_with_icon(status) + = render 'ci/status/badge', status: pipeline.detailed_status(current_user) %td = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do @@ -41,15 +40,25 @@ - else Cant find HEAD commit for this branch - - stages_status = pipeline.statuses.latest.stages_status %td.stage-cell - - stages.each do |stage| - - status = stages_status[stage] - - tooltip = "#{stage.titleize}: #{status || 'not found'}" - - if status - .stage-container - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do - = ci_icon_for_status(status) + - pipeline.stages.each do |stage| + - if stage.status + - detailed_status = stage.detailed_status(current_user) + - icon_status = "#{detailed_status.icon}_borderless" + - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}" + + .stage-container.dropdown.js-mini-pipeline-graph + %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } } + = custom_icon(icon_status) + = icon('caret-down') + + %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container + .arrow-up + .js-builds-dropdown-list.scrollable-menu + + .js-builds-dropdown-loading.builds-dropdown-loading.hidden + %span.fa.fa-spinner.fa-spin + %td - if pipeline.duration @@ -69,18 +78,18 @@ .btn-group.inline - if actions.any? .btn-group - %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{ type: 'button', 'data-toggle' => 'dropdown' } = custom_icon('icon_play') - = icon('caret-down') + = icon('caret-down', 'aria-hidden' => 'true') %ul.dropdown-menu.dropdown-menu-align-right - actions.each do |build| %li = link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do = custom_icon('icon_play') - %span= build.name.humanize + %span= build.name - if artifacts.present? .btn-group - %a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{ type: 'button', 'data-toggle' => 'dropdown' } = icon("download") = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml deleted file mode 100644 index b7087749428..00000000000 --- a/app/views/projects/commit/_builds.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -- @ci_pipelines.each do |pipeline| - = render "pipeline", pipeline: pipeline, pipeline_details: true diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index e4cd55b9f7a..421b3db342d 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -6,29 +6,28 @@ - label = 'Cherry-pick' - target_label = 'Pick into branch' -.modal{id: "modal-#{type}-commit"} +.modal{ id: "modal-#{type}-commit" } .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × - %h3.page-title== #{label} this #{commit.change_type_title} + %a.close{ href: "#", "data-dismiss" => "modal" } × + %h3.page-title== #{label} this #{commit.change_type_title(current_user)} .modal-body - = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do + = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do .form-group.branch = label_tag 'target_branch', target_label, class: 'control-label' .col-sm-10 = hidden_field_tag :target_branch, @project.default_branch, id: 'target_branch' - = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "target_branch", selected: @project.default_branch, target_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false }}) + = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "target_branch", selected: @project.default_branch, target_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } }) - if can?(current_user, :push_code, @project) .js-create-merge-request-container .checkbox - - nonce = SecureRandom.hex - = label_tag "create_merge_request-#{nonce}" do - = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" + = label_tag do + = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: nil Start a <strong>new merge request</strong> with these changes - else - = hidden_field_tag 'create_merge_request', 1 + = hidden_field_tag 'create_merge_request', 1, id: nil .form-actions = submit_tag label, class: 'btn btn-create' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index cbfd99ca448..8aed88da38b 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -7,8 +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 - = nav_link(path: 'commit#builds') do - = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do - Builds - %span.badge= @statuses.count + %span.badge= @commit.pipelines.size diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml deleted file mode 100644 index 6bb900e3fc1..00000000000 --- a/app/views/projects/commit/_ci_stage.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -%tr - %th{colspan: 10} - %strong - %a{name: stage} - - status = statuses.latest.status - %span{class: "ci-status-link ci-status-icon-#{status}"} - = ci_icon_for_status(status) - - if stage - - = stage.titleize - = render statuses.latest_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true - = render statuses.retried_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true - %tr - %td{colspan: 10} - diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 0ebc38d16cf..08eb0c57f66 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,13 +1,8 @@ -.commit-info-row.commit-info-row-header - .commit-meta - %strong Commit - %strong.monospace.js-details-short= @commit.short_id - = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do - %span.text-expander - \... - %span.js-details-content.hide - %strong.monospace.commit-hash-full= @commit.id - = clipboard_button(clipboard_text: @commit.id) +.page-content-header + .header-main-content + %strong + = 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)} %span by @@ -19,7 +14,8 @@ %strong = commit_committer_link(@commit, avatar: true, size: 24) #{time_ago_with_tooltip(@commit.committed_date)} - .commit-action-buttons + + .header-action-buttons - if defined?(@notes_count) && @notes_count > 0 %span.btn.disabled.btn-grouped.hidden-xs.append-right-10 = icon('comment') @@ -55,8 +51,8 @@ %pre.commit-description = preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author)) -.commit-info-widget - .widget-row.branch-info +.info-well + .well-segment.branch-info .icon-container.commit-icon = custom_icon("icon_commit") %span.cgray= pluralize(@commit.parents.count, "parent") @@ -66,8 +62,8 @@ %i.fa.fa-spinner.fa-spin - if @commit.status - .widget-row.pipeline-info - .icon-container + .well-segment.pipeline-info + %div{ class: "icon-container ci-status-icon-#{@commit.status}" } = ci_icon_for_status(@commit.status) Pipeline = link_to "##{@commit.pipelines.last.id}", pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "monospace" diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index d6916fb7f1a..08d3443b3d0 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -1,10 +1,6 @@ .pipeline-graph-container .row-content-block.build-content.middle-block.pipeline-actions .pull-right - .btn.btn-grouped.btn-white.toggle-pipeline-btn - %span.toggle-btn-text Hide - %span pipeline graph - %span.caret - if can?(current_user, :update_pipeline, pipeline.project) - if pipeline.builds.latest.failed.any?(&:retryable?) = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post @@ -28,20 +24,8 @@ in = time_interval_in_words pipeline.duration - .row-content-block.build-content.middle-block.pipeline-graph.hidden - .pipeline-visualization - %ul.stage-column-list - - stages = pipeline.stages_with_latest_statuses - - stages.each do |stage, statuses| - %li.stage-column - .stage-name - %a{name: stage} - - if stage - = stage.titleize - .builds-container - %ul - = render "projects/commit/pipeline_stage", statuses: statuses - + .row-content-block.build-content.middle-block.js-pipeline-graph.hidden + = render "projects/pipelines/graph", pipeline: pipeline - if pipeline.yaml_errors.present? .bs-callout.bs-callout-danger @@ -66,5 +50,4 @@ - if pipeline.project.build_coverage_enabled? %th Coverage %th - - pipeline.statuses.relevant.stages.each do |stage| - = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage) + = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml deleted file mode 100644 index 289aa5178b1..00000000000 --- a/app/views/projects/commit/_pipeline_stage.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -- status_groups = statuses.group_by(&:group_name) -- status_groups.each do |group_name, grouped_statuses| - - if grouped_statuses.one? - - status = grouped_statuses.first - - is_playable = status.playable? && can?(current_user, :update_build, @project) - %li.build{ class: ("playable" if is_playable) } - .curve - .build-content - = render "projects/#{status.to_partial_path}_pipeline", subject: status - - else - %li.build - .curve - .dropdown.inline.build-content - = render "projects/commit/pipeline_status_group", name: group_name, subject: grouped_statuses diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml deleted file mode 100644 index 18daa2ee693..00000000000 --- a/app/views/projects/commit/_pipeline_status_group.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- group_status = CommitStatus.where(id: subject).status -%button.dropdown-menu-toggle.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } } - %span.ci-status-icon - = ci_icon_for_status(group_status) - %span.ci-status-text - = name - %span.badge= subject.size -.dropdown-menu.grouped-pipeline-dropdown - .arrow - %ul - - subject.each do |status| - %li - = render "projects/#{status.to_partial_path}_pipeline", subject: status diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 2dc91a9b762..1164627fa11 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -1,15 +1,15 @@ -%ul.content-list.pipelines +%div - if pipelines.blank? - %li + %div .nothing-here-block No pipelines to show - else - .table-holder - %table.table.ci-table - %tbody - %th Status - %th Pipeline - %th Commit - %th Stages - %th - %th - = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, show_commit: false + .table-holder.pipelines + %table.table.ci-table.js-pipeline-table + %thead + %th.pipeline-status Status + %th.pipeline-info Pipeline + %th.pipeline-commit Commit + %th.pipeline-stages Stages + %th.pipeline-date + %th.pipeline-actions + = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml deleted file mode 100644 index 077b2d2725b..00000000000 --- a/app/views/projects/commit/builds.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- @no_container = true -- page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits" -= render "projects/commits/head" - -%div{ class: container_class } - = render "commit_box" - - = render "ci_menu" - = render "builds" 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/commit/show.html.haml b/app/views/projects/commit/show.html.haml index b8c64d1f13e..7afd3d80ef5 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -8,7 +8,7 @@ - if @commit.status = render "ci_menu" - else - %div.block-connector + .block-connector = render "projects/diffs/diffs", diffs: @diffs = render "projects/notes/notes_with_form" - if can_collaborate_with_project? diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 9f80a974d64..002e3d345dc 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -6,37 +6,36 @@ - note_count = notes.user.count - cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count] -- cache_key.push(commit.status) if commit.status +- cache_key.push(commit.status(ref)) if commit.status(ref) = cache(cache_key, expires_in: 1.day) do - %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } - = author_avatar(commit, size: 36) + %li.commit.table-list-row.js-toggle-container{ id: "commit-#{commit.short_id}" } - .commit-info-block - .commit-row-title - %span.item-title - = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" - %span.commit-row-message.visible-xs-inline - · - = commit.short_id - - if commit.status(ref) - .visible-xs-inline - = render_commit_status(commit, ref: ref) - - if commit.description? - %a.text-expander.hidden-xs.js-toggle-button ... + .table-list-cell.avatar-cell.hidden-xs + = author_avatar(commit, size: 36) - .commit-actions.hidden-xs - - if commit.status(ref) - = render_commit_status(commit, ref: ref) - = clipboard_button(clipboard_text: commit.id) - = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" - = link_to_browse_code(project, commit) + .table-list-cell.commit-content + = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title" + %span.commit-row-message.visible-xs-inline + · + = commit.short_id + - if commit.status(ref) + .visible-xs-inline + = render_commit_status(commit, ref: ref) + - if commit.description? + %a.text-expander.hidden-xs.js-toggle-button ... - if commit.description? %pre.commit-row-description.js-toggle-content = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author)) - - .commit-row-info + .commiter = commit_author_link(commit, avatar: false, size: 24) - authored + committed #{time_ago_with_tooltip(commit.committed_date)} + + .table-list-cell.commit-actions.hidden-xs + - if commit.status(ref) + = render_commit_status(commit, ref: ref) + = clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard") + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" + = link_to_browse_code(project, commit) diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml index ce416caa494..64d93e4141c 100644 --- a/app/views/projects/commits/_commit_list.html.haml +++ b/app/views/projects/commits/_commit_list.html.haml @@ -1,7 +1,7 @@ - commits, hidden = limited_commits(@commits) - commits = Commit.decorate(commits, @project) -%div.panel.panel-default +.panel.panel-default .panel-heading Commits (#{@commits.count}) - if hidden > 0 @@ -11,4 +11,4 @@ %li.warning-row.unstyled #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. - else - %ul.content-list= render commits, project: @project, ref: @ref + %ul.content-list.table-list= render commits, project: @project, ref: @ref diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 48756c68941..fcc367951ad 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -4,7 +4,7 @@ - commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| %li.commit-header= "#{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}" %li.commits-row - %ul.list-unstyled.commit-list + %ul.content-list.commit-list.table-list.table-wide = render commits, project: project, ref: ref - if hidden > 0 diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 9628cbd1634..e77f23c7fd8 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -28,12 +28,12 @@ = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false } - if current_user && current_user.private_token .control - = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'btn' do + = link_to namespace_project_commits_path(@project.namespace, @project, @ref, { format: :atom, private_token: current_user.private_token }), title: "Commits Feed", class: 'btn' do = icon("rss") %ul.breadcrumb.repo-breadcrumb = commits_breadcrumbs - %div{id: dom_id(@project)} + %div{ id: dom_id(@project) } %ol#commits-list.list-unstyled.content_list = render 'commits', project: @project, ref: @ref = spinner diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index 7bde20c3286..d76d48187cd 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -2,21 +2,21 @@ .clearfix - if params[:to] && params[:from] .compare-switch-container - = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'} + = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Switch base of comparison'} .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown .input-group.inline-input-group %span.input-group-addon from = hidden_field_tag :from, params[:from] - = button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do - .dropdown-toggle-text= params[:from] || 'Select branch/tag' + = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do + .dropdown-toggle-text.str-truncated= params[:from] || 'Select branch/tag' = render "ref_dropdown" .compare-ellipsis.inline ... .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown .input-group.inline-input-group %span.input-group-addon to = hidden_field_tag :to, params[:to] - = button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do - .dropdown-toggle-text= params[:to] || 'Select branch/tag' + = button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do + .dropdown-toggle-text.str-truncated= params[:to] || 'Select branch/tag' = render "ref_dropdown" = button_tag "Compare", class: "btn btn-create commits-compare-btn" diff --git a/app/views/projects/cycle_analytics/_empty_stage.html.haml b/app/views/projects/cycle_analytics/_empty_stage.html.haml new file mode 100644 index 00000000000..c3f95860e92 --- /dev/null +++ b/app/views/projects/cycle_analytics/_empty_stage.html.haml @@ -0,0 +1,7 @@ +.empty-stage-container + .empty-stage + .icon-no-data + = custom_icon ('icon_no_data') + %h4 We don't have enough data to show this stage. + %p + {{currentStage.emptyStageText}} diff --git a/app/views/projects/cycle_analytics/_no_access.html.haml b/app/views/projects/cycle_analytics/_no_access.html.haml new file mode 100644 index 00000000000..0ffc79b3181 --- /dev/null +++ b/app/views/projects/cycle_analytics/_no_access.html.haml @@ -0,0 +1,7 @@ +.no-access-stage-container + .no-access-stage + .icon-lock + = custom_icon ('icon_lock') + %h4 You need permission. + %p + Want to see the data? Please ask administrator for access. diff --git a/app/views/projects/cycle_analytics/_overview.html.haml b/app/views/projects/cycle_analytics/_overview.html.haml new file mode 100644 index 00000000000..c8f0b547f80 --- /dev/null +++ b/app/views/projects/cycle_analytics/_overview.html.haml @@ -0,0 +1,15 @@ +.cycle-analytics-overview + .container + .row + .col-md-10.col-md-offset-1 + .row.overview-details + .col-md-6.overview-text + %h4 Introducing Cycle Analytics + %p + Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project. + To set up CA, you must first define a production environment by setting up your CI and then deploy to production. + %p + %a.btn{ href: help_page_path('user/project/cycle_analytics'), target: "_blank" } Read more + .col-md-6.overview-image + %span.overview-icon + = custom_icon ('icon_cycle_analytics_overview') diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 247d612ba6f..479ce44f378 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -1,63 +1,90 @@ - @no_container = true - page_title "Cycle Analytics" - - content_for :page_specific_javascripts do - = page_specific_javascript_tag('cycle_analytics/cycle_analytics_bundle.js') + = page_specific_javascript_tag("cycle_analytics/cycle_analytics_bundle.js") = render "projects/pipelines/head" -#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) }} - - .bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"} - = icon('times', class: 'dismiss-icon', "@click" => "dismissLanding()") - .row - .col-sm-3.col-xs-12.svg-container - = custom_icon('icon_cycle_analytics_splash') - .col-sm-8.col-xs-12.inner-content - %h4 - Introducing Cycle Analytics - %p - Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project. - - = link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn' +#cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } } + - if @cycle_analytics_no_data + .bordered-box.landing.content-block{ "v-if" => "!isOverviewDialogDismissed" } + = icon("times", class: "dismiss-icon", "@click" => "dismissOverviewDialog()") + .row + .col-sm-3.col-xs-12.svg-container + = custom_icon('icon_cycle_analytics_splash') + .col-sm-8.col-xs-12.inner-content + %h4 + Introducing Cycle Analytics + %p + Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project. + = link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn' = icon("spinner spin", "v-show" => "isLoading") - - .wrapper{"v-show" => "!isLoading && !hasError"} + .wrapper{ "v-show" => "!isLoading && !hasError" } .panel.panel-default .panel-heading Pipeline Health - .content-block .container-fluid .row - .col-sm-3.col-xs-12.column{"v-for" => "item in analytics.summary"} - %h3.header {{item.value}} - %p.text {{item.title}} - + .col-sm-3.col-xs-12.column{ "v-for" => "item in state.summary" } + %h3.header {{ item.value }} + %p.text {{ item.title }} .col-sm-3.col-xs-12.column .dropdown.inline.js-ca-dropdown - %button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"} + %button.dropdown-menu-toggle{ "data-toggle" => "dropdown", :type => "button" } %span.dropdown-label Last 30 days %i.fa.fa-chevron-down %ul.dropdown-menu.dropdown-menu-align-right %li - %a{'href' => "#", 'data-value' => '30'} + %a{ "href" => "#", "data-value" => "30" } Last 30 days %li - %a{'href' => "#", 'data-value' => '90'} + %a{ "href" => "#", "data-value" => "90" } Last 90 days - - .bordered-box - %ul.content-list - %li{"v-for" => "item in analytics.stats"} - .container-fluid - .row - .col-xs-8.title-col - %p.title - {{item.title}} - %p.text - {{item.description}} - .col-xs-4.value-col - %span - {{item.value}} + .stage-panel-container + .panel.panel-default.stage-panel + .panel-heading + %nav.col-headers + %ul + %li.stage-header + %span.stage-name + Stage + %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The phase of the development lifecycle.", "aria-hidden" => "true" } + %li.median-header + %span.stage-name + Median + %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.", "aria-hidden" => "true" } + %li.event-header + %span.stage-name + {{ currentStage ? currentStage.legend : 'Related Issues' }} + %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The collection of events added to the data gathered for that stage.", "aria-hidden" => "true" } + %li.total-time-header + %span.stage-name + Total Time + %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The time taken by each data entry gathered by that stage.", "aria-hidden" => "true" } + .stage-panel-body + %nav.stage-nav + %ul + %li.stage-nav-item{ ':class' => '{ active: stage.active }', '@click' => 'selectStage(stage)', "v-for" => "stage in state.stages" } + .stage-nav-item-cell.stage-name + {{ stage.title }} + .stage-nav-item-cell.stage-median + %template{ "v-if" => "stage.isUserAllowed" } + %span{ "v-if" => "stage.value" } + {{ stage.value }} + %span.stage-empty{ "v-else" => true } + Not enough data + %template{ "v-else" => true } + %span.not-available + Not available + .section.stage-events + %template{ "v-if" => "isLoadingStage" } + = icon("spinner spin") + %template{ "v-if" => "currentStage && !currentStage.isUserAllowed" } + = render partial: "no_access" + %template{ "v-else" => true } + %template{ "v-if" => "isEmptyStage && !isLoadingStage" } + = render partial: "empty_stage" + %template{ "v-if" => "state.events.length && !isLoadingStage && !isEmptyStage" } + %component{ ":is" => "currentStage.component", ":stage" => "currentStage", ":items" => "state.events" } diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml index 450aaeb367c..d1e3cb14022 100644 --- a/app/views/projects/deploy_keys/_deploy_key.html.haml +++ b/app/views/projects/deploy_keys/_deploy_key.html.haml @@ -6,6 +6,9 @@ = deploy_key.title .description = deploy_key.fingerprint + - if deploy_key.can_push? + .write-access-allowed + Write access allowed .deploy-key-content.prepend-left-default.deploy-key-projects - deploy_key.projects.each do |project| - if can?(current_user, :read_project, project) diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml index 901605f7ca3..c91bb9c255a 100644 --- a/app/views/projects/deploy_keys/_form.html.haml +++ b/app/views/projects/deploy_keys/_form.html.haml @@ -10,4 +10,13 @@ %p.light.append-bottom-0 Paste a machine public key here. Read more about how to generate it = link_to "here", help_page_path("ssh/README") + .form-group + .checkbox + = f.label :can_push do + = f.check_box :can_push + %strong Write access allowed + .form-group + %p.light.append-bottom-0 + Allow this key to push to repository as well? (Default only allows pull access.) + = f.submit "Add key", class: "btn-create btn" diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index 58a214bdbd1..a680b1ca017 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -3,7 +3,7 @@ - if actions.present? .inline .dropdown - %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} + %a.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' } = custom_icon('icon_play') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml index ff250eeca50..170d786ecbf 100644 --- a/app/views/projects/deployments/_commit.html.haml +++ b/app/views/projects/deployments/_commit.html.haml @@ -1,4 +1,4 @@ -%div.branch-commit +.branch-commit - if deployment.ref .icon-container = deployment.tag? ? icon('tag') : icon('code-fork') diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml index 779c8ea0104..52a1ece7d60 100644 --- a/app/views/projects/diffs/_content.html.haml +++ b/app/views/projects/diffs/_content.html.haml @@ -9,8 +9,8 @@ - if !project.repository.diffable?(blob) .nothing-here-block This diff was suppressed by a .gitattributes entry. - elsif diff_file.collapsed? - - url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path)) - .nothing-here-block.diff-collapsed{data: { diff_for_path: url } } + - url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier)) + .nothing-here-block.diff-collapsed{ data: { diff_for_path: url } } This diff is collapsed. %a.click-to-expand Click to expand it. @@ -25,7 +25,7 @@ - elsif diff_file.renamed_file .nothing-here-block File moved - elsif blob.image? - - old_blob = diff_file.old_blob(diff_commit) + - old_blob = diff_file.old_blob(diff_file.old_content_commit || @base_commit) = render "projects/diffs/image", diff_file: diff_file, old_file: old_blob, file: blob - else .nothing-here-block No preview for this file type diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 067cf595da3..58c20e225c6 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -18,15 +18,16 @@ = 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, index| + - diff_files.each_with_index do |diff_file| - diff_commit = commit_for_diff(diff_file) - blob = diff_file.blob(diff_commit) - next unless blob - blob.load_all_data!(diffs.project.repository) unless blob.only_display_raw? + - file_hash = hexdigest(diff_file.file_path) - = render 'projects/diffs/file', index: index, project: diffs.project, + = render 'projects/diffs/file', file_hash: file_hash, project: diffs.project, diff_file: diff_file, diff_commit: diff_commit, blob: blob diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 8f4f9ad4a80..c37a33bbcd5 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,6 +1,6 @@ -.diff-file.file-holder{id: "diff-#{index}", data: diff_file_html_data(project, diff_file.file_path, diff_commit.id)} - .file-title{id: "file-path-#{hexdigest(diff_file.file_path)}"} - = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "#diff-#{index}" +.diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id) } + .file-title + = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}" - unless diff_file.submodule? .file-actions.hidden-xs @@ -9,7 +9,7 @@ = icon('comment') \ - if editable_diff?(diff_file) - - link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {} + - link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {} = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, blob: blob, link_opts: link_opts) diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml index d3ed8e1bf38..90c9a0c6c2b 100644 --- a/app/views/projects/diffs/_file_header.html.haml +++ b/app/views/projects/diffs/_file_header.html.haml @@ -21,7 +21,7 @@ - if diff_file.deleted_file deleted - = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy filename to clipboard') + = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard') - if diff_file.mode_changed? %small diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml index 9ec6a7aa5cd..1bccaaf5273 100644 --- a/app/views/projects/diffs/_image.html.haml +++ b/app/views/projects/diffs/_image.html.haml @@ -7,16 +7,16 @@ - if diff.renamed_file || diff.new_file || diff.deleted_file .image %span.wrap - .frame{class: image_diff_class(diff)} - %img{src: diff.deleted_file ? old_file_raw_path : file_raw_path} + .frame{ class: image_diff_class(diff) } + %img{ src: diff.deleted_file ? old_file_raw_path : file_raw_path, alt: diff.new_path } %p.image-info= "#{number_to_human_size file.size}" - else .image - %div.two-up.view + .two-up.view %span.wrap .frame.deleted - %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path))} - %img{src: old_file_raw_path} + %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path)) } + %img{ src: old_file_raw_path, alt: diff.old_path } %p.image-info.hide %span.meta-filesize= "#{number_to_human_size old_file.size}" | @@ -27,8 +27,8 @@ %span.meta-height %span.wrap .frame.added - %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path))} - %img{src: file_raw_path} + %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path)) } + %img{ src: file_raw_path, alt: diff.new_path } %p.image-info.hide %span.meta-filesize= "#{number_to_human_size file.size}" | @@ -38,32 +38,32 @@ %b H: %span.meta-height - %div.swipe.view.hide + .swipe.view.hide .swipe-frame .frame.deleted - %img{src: old_file_raw_path} + %img{ src: old_file_raw_path, alt: diff.old_path } .swipe-wrap .frame.added - %img{src: file_raw_path} + %img{ src: file_raw_path, alt: diff.new_path } %span.swipe-bar %span.top-handle %span.bottom-handle - %div.onion-skin.view.hide + .onion-skin.view.hide .onion-skin-frame .frame.deleted - %img{src: old_file_raw_path} + %img{ src: old_file_raw_path, alt: diff.old_path } .frame.added - %img{src: file_raw_path} + %img{ src: file_raw_path, alt: diff.new_path } .controls .transparent .drag-track - .dragger{:style => "left: 0px;"} + .dragger{ :style => "left: 0px;" } .opaque .view-modes.hide %ul.view-modes-menu - %li.two-up{data: {mode: 'two-up'}} 2-up - %li.swipe{data: {mode: 'swipe'}} Swipe - %li.onion-skin{data: {mode: 'onion-skin'}} Onion skin + %li.two-up{ data: { mode: 'two-up' } } 2-up + %li.swipe{ data: { mode: 'swipe' } } Swipe + %li.onion-skin{ data: { mode: 'onion-skin' } } Onion skin diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 7042e9f1fc9..cd18ba2ed00 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -16,18 +16,18 @@ - if plain = link_text - else - %a{href: "##{line_code}", data: { linenumber: link_text }} + %a{ href: "##{line_code}", data: { linenumber: link_text } } %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } - link_text = type == "old" ? " " : line.new_pos - if plain = link_text - else - %a{href: "##{line_code}", data: { linenumber: link_text }} + %a{ href: "##{line_code}", data: { linenumber: link_text } } %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }< - if email - %pre= diff_line_content(line.text, type) + %pre= line.text - else - = diff_line_content(line.text, type) + = diff_line_content(line.text) - discussions = local_assigns.fetch(:discussions, nil) - if discussions && !line.meta? diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 78aa9fb7391..b087485aa17 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -1,5 +1,5 @@ / Side-by-side diff view -%div.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data } +.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data } %table - last_line = 0 - diff_file.parallel_diff_lines.each do |line| @@ -13,9 +13,9 @@ - else - left_line_code = diff_file.line_code(left) - left_position = diff_file.position(left) - %td.old_line.diff-line-num{id: left_line_code, class: left.type, data: { linenumber: left.old_pos }} - %a{href: "##{left_line_code}" }= raw(left.old_pos) - %td.line_content.parallel.noteable_line{class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old')}= diff_line_content(left.text) + %td.old_line.diff-line-num{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } } + %a{ href: "##{left_line_code}" }= raw(left.old_pos) + %td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text) - else %td.old_line.diff-line-num.empty-cell %td.line_content.parallel @@ -26,9 +26,9 @@ - else - right_line_code = diff_file.line_code(right) - right_position = diff_file.position(right) - %td.new_line.diff-line-num{id: right_line_code, class: right.type, data: { linenumber: right.new_pos }} - %a{href: "##{right_line_code}" }= raw(right.new_pos) - %td.line_content.parallel.noteable_line{class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new')}= diff_line_content(right.text) + %td.new_line.diff-line-num{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } } + %a{ href: "##{right_line_code}" }= raw(right.new_pos) + %td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text) - else %td.old_line.diff-line-num.empty-cell %td.line_content.parallel diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index e751dabdf99..290f696d582 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -9,28 +9,28 @@ %strong.cred #{diff_files.sum(&:removed_lines)} deletions .file-stats.js-toggle-content.hide %ul - - diff_files.each_with_index do |diff_file, i| + - diff_files.each do |diff_file| + - file_hash = hexdigest(diff_file.file_path) %li - if diff_file.deleted_file %span.deleted-file - %a{href: "#diff-#{i}"} + %a{ href: "##{file_hash}" } %i.fa.fa-minus = diff_file.old_path - elsif diff_file.renamed_file %span.renamed-file - %a{href: "#diff-#{i}"} + %a{ href: "##{file_hash}" } %i.fa.fa-minus = diff_file.old_path → = diff_file.new_path - elsif diff_file.new_file %span.new-file - %a{href: "#diff-#{i}"} + %a{ href: "##{file_hash}" } %i.fa.fa-plus = diff_file.new_path - else %span.edit-file - %a{href: "#diff-#{i}"} + %a{ href: "##{file_hash}" } %i.fa.fa-adjust = diff_file.new_path - diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 0aa8801c2d8..1d7fdf68cb3 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -21,85 +21,78 @@ .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) + 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) - .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) + .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.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 :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 :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 + = 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 :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 - = 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? - .checkbox - = f.label :lfs_enabled do - = f.check_box :lfs_enabled - %strong LFS - %br - %span.descr + .row + .col-md-9 + = f.label :lfs_enabled, 'LFS', class: 'label-light' + %span.help-block Git Large File Storage = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + .col-md-3 + = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control', data: { field: 'lfs_enabled' } + - if Gitlab.config.registry.enabled .form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) } @@ -111,7 +104,8 @@ %span.descr Enable Container Registry for this project = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank' - = render 'merge_request_settings', f: f + = render 'merge_request_settings', form: f + %hr %fieldset.features.append-bottom-default %h5.prepend-top-0 @@ -144,7 +138,7 @@ such as compressing file revisions and removing unreachable objects. .col-lg-9 = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), - method: :post, class: "btn btn-save" + method: :post, class: "btn btn-default" %hr .row.prepend-top-default .col-lg-3 diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 7a39064adc5..58c085cdb9d 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -16,7 +16,7 @@ %p Otherwise you can start with adding a = succeed ',' do - = link_to "README", new_readme_path, class: 'underlined-link' + = link_to "README", add_special_file_path(@project, file_name: 'README.md'), class: 'underlined-link' a = succeed ',' do = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link' @@ -32,7 +32,7 @@ .empty_wrapper %h3.page-title-empty Command line instructions - %div.git-empty + .git-empty %fieldset %h5 Git global setup %pre.light-well @@ -52,7 +52,7 @@ git push -u origin master %fieldset - %h5 Existing folder or Git repository + %h5 Existing folder %pre.light-well :preserve cd existing_folder @@ -62,6 +62,15 @@ git commit git push -u origin master + %fieldset + %h5 Existing Git repository + %pre.light-well + :preserve + cd existing_repo + git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git push -u origin --all + git push -u origin --tags + - if can? current_user, :remove_project, @project .prepend-top-20 = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml deleted file mode 100644 index b75d5df4150..00000000000 --- a/app/views/projects/environments/_environment.html.haml +++ /dev/null @@ -1,35 +0,0 @@ -- last_deployment = environment.last_deployment - -%tr.environment - %td - = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment) - - %td.deployment-column - - if last_deployment - %span ##{last_deployment.iid} - - if last_deployment.user - by - = user_avatar(user: last_deployment.user, size: 20) - - %td - - if last_deployment && last_deployment.deployable - = link_to [@project.namespace.becomes(Namespace), @project, last_deployment.deployable], class: 'build-link' do - = "#{last_deployment.deployable.name} (##{last_deployment.deployable.id})" - - %td - - if last_deployment - = render 'projects/deployments/commit', deployment: last_deployment - - else - %p.commit-title - No deployments yet - - %td - - if last_deployment - #{time_ago_with_tooltip(last_deployment.created_at)} - - %td.hidden-xs - .pull-right - = render 'projects/environments/external_url', environment: environment - = render 'projects/deployments/actions', deployment: last_deployment - = render 'projects/environments/stop', environment: environment - = render 'projects/deployments/rollback', deployment: last_deployment diff --git a/app/views/projects/environments/_header_title.html.haml b/app/views/projects/environments/_header_title.html.haml deleted file mode 100644 index e056fccad5d..00000000000 --- a/app/views/projects/environments/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Environments", project_environments_path(@project)) diff --git a/app/views/projects/environments/_terminal_button.html.haml b/app/views/projects/environments/_terminal_button.html.haml new file mode 100644 index 00000000000..97de9c95de7 --- /dev/null +++ b/app/views/projects/environments/_terminal_button.html.haml @@ -0,0 +1,3 @@ +- if environment.has_terminals? && can?(current_user, :admin_environment, @project) + = link_to terminal_namespace_project_environment_path(@project.namespace, @project, environment), class: 'btn terminal-button' do + = icon('terminal') diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 8f555afcf11..8c728eb0f6a 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -2,47 +2,18 @@ - page_title "Environments" = render "projects/pipelines/head" -%div{ class: container_class } - .top-area - %ul.nav-links - %li{class: ('active' if @scope.nil?)} - = link_to project_environments_path(@project) do - Available - %span.badge.js-available-environments-count - = number_with_delimiter(@all_environments.available.count) +- content_for :page_specific_javascripts do + = page_specific_javascript_tag("environments/environments_bundle.js") - %li{class: ('active' if @scope == 'stopped')} - = link_to project_environments_path(@project, scope: :stopped) do - Stopped - %span.badge.js-stopped-environments-count - = number_with_delimiter(@all_environments.stopped.count) - - - if can?(current_user, :create_environment, @project) && !@all_environments.blank? - .nav-controls - = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do - New environment - - .environments-container - - if @all_environments.blank? - .blank-state.blank-state-no-icon - %h2.blank-state-title - You don't have any environments right now. - %p.blank-state-text - Environments are places where code gets deployed, such as staging or production. - %br - = succeed "." do - = link_to "Read more about environments", help_page_path("ci/environments") - - if can?(current_user, :create_environment, @project) - = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do - New environment - - else - .table-holder - %table.table.ci-table.environments - %tbody - %th Environment - %th Last Deployment - %th Build - %th Commit - %th - %th.hidden-xs - = render @environments +#environments-list-view{ data: { environments_data: environments_list_data, + "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, + "can-read-environment" => can?(current_user, :read_environment, @project).to_s, + "can-create-environment" => can?(current_user, :create_environment, @project).to_s, + "project-environments-path" => project_environments_path(@project), + "project-stopped-environments-path" => project_environments_path(@project, scope: :stopped), + "new-environment-path" => new_namespace_project_environment_path(@project.namespace, @project), + "help-page-path" => help_page_path("ci/environments"), + "css-class" => container_class, + "commit-icon-svg" => custom_icon("icon_commit"), + "terminal-icon-svg" => custom_icon("icon_terminal"), + "play-icon-svg" => custom_icon("icon_play") } } diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 992d98cdd96..da06ae1c313 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -8,6 +8,7 @@ %h3.page-title= @environment.name.capitalize .col-md-3 .nav-controls + = render 'projects/environments/terminal_button', environment: @environment = render 'projects/environments/external_url', environment: @environment - if can?(current_user, :update_environment, @environment) = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml new file mode 100644 index 00000000000..431253c1299 --- /dev/null +++ b/app/views/projects/environments/terminal.html.haml @@ -0,0 +1,22 @@ +- @no_container = true +- page_title "Terminal for environment", @environment.name += render "projects/pipelines/head" + +- content_for :page_specific_javascripts do + = stylesheet_link_tag "xterm/xterm" + = page_specific_javascript_tag("terminal/terminal_bundle.js") + +%div{ class: container_class } + .top-area + .row + .col-sm-6 + %h3.page-title + Terminal for environment + = @environment.name + + .col-sm-6 + .nav-controls + = render 'projects/deployments/actions', deployment: @environment.last_deployment + +.terminal-container{ class: container_class } + #terminal{ data: { project_path: "#{terminal_namespace_project_environment_path(@project.namespace, @project, @environment)}.ws" } } diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml index 9322c82904f..4cdb44325b3 100644 --- a/app/views/projects/find_file/show.html.haml +++ b/app/views/projects/find_file/show.html.haml @@ -9,11 +9,11 @@ = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do = @project.path %li.file-finder - %input#file_find.form-control.file-finder-input{type: "text", placeholder: 'Find by path', autocomplete: 'off'} + %input#file_find.form-control.file-finder-input{ type: "text", placeholder: 'Find by path', autocomplete: 'off' } - %div.tree-content-holder + .tree-content-holder .table-holder - %table.table.files-slider{class: "table_#{@hex_path} tree-table table-striped" } + %table.table.files-slider{ class: "table_#{@hex_path} tree-table table-striped" } %tbody = spinner nil, true diff --git a/app/views/projects/forks/error.html.haml b/app/views/projects/forks/error.html.haml index 3d0ab5b85d6..98d81308407 100644 --- a/app/views/projects/forks/error.html.haml +++ b/app/views/projects/forks/error.html.haml @@ -13,7 +13,11 @@ - if @forked_project && @forked_project.errors.any? %p – - = @forked_project.errors.full_messages.first + - error = @forked_project.errors.full_messages.first + - if error.include?("already been taken") + Name has already been taken + - else + = error %p = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork", class: "btn" do diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index abf4f697f86..6c8a6f051a9 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -1,7 +1,7 @@ .top-area .nav-text - full_count_title = "#{@public_forks_count} public and #{@private_forks_count} private" - == #{pluralize(@total_forks_count, 'fork')}: #{full_count_title} + = "#{pluralize(@total_forks_count, 'fork')}: #{full_count_title}" .nav-controls = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| @@ -9,13 +9,13 @@ spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' } .dropdown - %button.dropdown-toggle.btn.sort-forks{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span.light sort: - if @sort.present? = sort_options_hash[@sort] - else = sort_title_recently_created - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li - excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id] diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 80fe6be49b0..6d7af1685fd 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -1,39 +1,67 @@ -%tr.generic_commit_status +- admin = local_assigns.fetch(:admin, false) +- ref = local_assigns.fetch(:ref, nil) +- commit_sha = local_assigns.fetch(:commit_sha, nil) +- retried = local_assigns.fetch(:retried, false) +- pipeline_link = local_assigns.fetch(:pipeline_link, false) +- stage = local_assigns.fetch(:stage, false) +- coverage = local_assigns.fetch(:coverage, false) + +%tr.generic_commit_status{ class: ('retried' if retried) } %td.status - - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url - = ci_status_with_icon(generic_commit_status.status, generic_commit_status.target_url) - - else - = ci_status_with_icon(generic_commit_status.status) + = render 'ci/status/badge', status: generic_commit_status.detailed_status(current_user) %td.generic_commit_status-link - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url = link_to generic_commit_status.target_url do - %strong ##{generic_commit_status.id} + %span.build-link ##{generic_commit_status.id} - else - %strong ##{generic_commit_status.id} + %span.build-link ##{generic_commit_status.id} + + - if ref + - if generic_commit_status.ref + .icon-container + = generic_commit_status.tags.any? ? icon('tag') : icon('code-fork') + = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref) + - else + .light none + .icon-container.commit-icon + = custom_icon("icon_commit") + + - if commit_sha + = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "commit-id monospace" - - if defined?(retried) && retried + - if retried = icon('warning', class: 'text-warning has-tooltip', title: 'Status was retried.') - - if defined?(commit_sha) && commit_sha - %td - = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace" + .label-container + - if generic_commit_status.tags.any? + - generic_commit_status.tags.each do |tag| + %span.label.label-primary + = tag + - if retried + %span.label.label-warning retried - - if defined?(ref) && ref + - if pipeline_link %td - - if generic_commit_status.ref - = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref) + = link_to pipeline_path(generic_commit_status.pipeline) do + %span.pipeline-id ##{generic_commit_status.pipeline.id} + %span by + - if generic_commit_status.pipeline.user + = user_avatar(user: generic_commit_status.pipeline.user, size: 20) - else - .light none + %span.monospace API - - if defined?(runner) && runner + - if admin + %td + - if generic_commit_status.project + = link_to generic_commit_status.project.name_with_namespace, admin_namespace_project_path(generic_commit_status.project.namespace, generic_commit_status.project) %td - if generic_commit_status.try(:runner) = runner_link(generic_commit_status.runner) - else .light none - - if defined?(stage) && stage + - if stage %td = generic_commit_status.stage @@ -41,24 +69,19 @@ = generic_commit_status.name %td - - if generic_commit_status.tags.any? - - generic_commit_status.tags.each do |tag| - %span.label.label-primary - = tag - - if defined?(retried) && retried - %span.label.label-warning retried - - %td.duration - if generic_commit_status.duration - = icon("clock-o") - = time_interval_in_words(generic_commit_status.duration) + %p.duration + = custom_icon("icon_timer") + = duration_in_numbers(generic_commit_status.duration) - %td.timestamp - if generic_commit_status.finished_at - = icon("calendar") - %span #{time_ago_with_tooltip(generic_commit_status.finished_at)} + %p.finished-at + = icon("calendar") + %span #{time_ago_with_tooltip(generic_commit_status.finished_at)} - - if defined?(coverage) && coverage - %td.coverage - - if generic_commit_status.try(:coverage) - #{generic_commit_status.coverage}% + %td.coverage + - if coverage && generic_commit_status.try(:coverage) + #{generic_commit_status.coverage}% + + %td + -# empty column to match number of columns in ci/builds/_build.html.haml diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml deleted file mode 100644 index 1c457244a7a..00000000000 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } } - - if subject.target_url - = link_to subject.target_url do - %span.ci-status-icon - = ci_icon_for_status(subject.status) - %span.ci-status-text= subject.name - - else - %span.ci-status-icon - = ci_icon_for_status(subject.status) - %span.ci-status-text= subject.name diff --git a/app/views/projects/graphs/ci/_build_times.haml b/app/views/projects/graphs/ci/_build_times.haml index 195f18afc76..bb0975a9535 100644 --- a/app/views/projects/graphs/ci/_build_times.haml +++ b/app/views/projects/graphs/ci/_build_times.haml @@ -2,7 +2,7 @@ %p.light Commit duration in minutes for last 30 commits - %canvas#build_timesChart{height: 200} + %canvas#build_timesChart{ height: 200 } :javascript var data = { diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml index 1fbf6ca2c1c..431657c4dcb 100644 --- a/app/views/projects/graphs/ci/_builds.haml +++ b/app/views/projects/graphs/ci/_builds.haml @@ -13,18 +13,18 @@ %p.light Builds for last week (#{date_from_to(Date.today - 7.days, Date.today)}) - %canvas#weekChart{height: 200} + %canvas#weekChart{ height: 200 } .prepend-top-default %p.light Builds for last month (#{date_from_to(Date.today - 30.days, Date.today)}) - %canvas#monthChart{height: 200} + %canvas#monthChart{ height: 200 } .prepend-top-default %p.light Builds for last year - %canvas#yearChart.padded{height: 250} + %canvas#yearChart.padded{ height: 250 } - [:week, :month, :year].each do |scope| :javascript diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index ac5f792d140..5ebb939a109 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -21,7 +21,7 @@ %h3#date_header.page-title %p.light Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits. - %input#brush_change{:type => "hidden"} + %input#brush_change{ :type => "hidden" } .graphs.row #contributors-master #contributors.clearfix diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/_index.html.haml index 1b0dbbb8111..99d0df2ac34 100644 --- a/app/views/projects/group_links/index.html.haml +++ b/app/views/projects/group_links/_index.html.haml @@ -20,10 +20,10 @@ .form-group = label_tag :expires_at, 'Access expiration date', class: 'label-light' .clearable-input - = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date' + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date-groups', placeholder: 'Select access expiration date', id: 'expires_at_groups' %i.clear-icon.js-clear-input .help-block - On this date, all users in the group will automatically lose access to this project. + On this date, all members in the group will automatically lose access to this project. = submit_tag "Share", class: "btn btn-create" .col-lg-9.col-lg-offset-3 %hr diff --git a/app/views/projects/group_links/update.js.haml b/app/views/projects/group_links/update.js.haml index af9a5b19060..55520fda494 100644 --- a/app/views/projects/group_links/update.js.haml +++ b/app/views/projects/group_links/update.js.haml @@ -1,3 +1,4 @@ :plain var $listItem = $('#{escape_javascript(render('shared/members/group', group_link: @group_link))}'); $("#group_member_#{@group_link.id} .list-item-name").replaceWith($listItem.find('.list-item-name')); + gl.utils.localTimeAgo($('.js-timeago'), $("#group_member_#{@group_link.id}")); diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/_index.html.haml index 8faad351463..8faad351463 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/_index.html.haml diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml index 4d8ee562e6a..c52b3860636 100644 --- a/app/views/projects/imports/show.html.haml +++ b/app/views/projects/imports/show.html.haml @@ -1,4 +1,4 @@ -- page_title "Import in progress" +- page_title @project.forked? ? "Forking in progress" : "Import in progress" .save-project-loader .center %h2 diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index c80210d6ff4..bd46af339cf 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -43,7 +43,7 @@ = icon('clock-o') = issue.milestone.title - if issue.due_date - %span{class: "#{'cred' if issue.overdue?}"} + %span{ class: "#{'cred' if issue.overdue?}" } = icon('calendar') = issue.due_date.to_s(:medium) diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml index a4b752ad86d..34d5a3e1831 100644 --- a/app/views/projects/issues/_issues.html.haml +++ b/app/views/projects/issues/_issues.html.haml @@ -1,8 +1,7 @@ %ul.content-list.issues-list.issuable-list = render partial: "projects/issues/issue", collection: @issues - if @issues.blank? - %li - .nothing-here-block No issues to show + = render 'shared/empty_states/issues' - if @issues.present? = paginate @issues, theme: "gitlab" diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index 31d3ec23276..d48923b422a 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -2,12 +2,12 @@ %h2.merge-requests-title = pluralize(@merge_requests.count, 'Related Merge Request') %ul.unstyled-list.related-merge-requests - - has_any_ci = @merge_requests.any?(&:pipeline) + - has_any_ci = @merge_requests.any?(&:head_pipeline) - @merge_requests.each do |merge_request| %li %span.merge-request-ci-status - - if merge_request.pipeline - = render_pipeline_status(merge_request.pipeline) + - if merge_request.head_pipeline + = render_pipeline_status(merge_request.head_pipeline) - elsif has_any_ci = icon('blank fw') %span.merge-request-id @@ -19,11 +19,17 @@ in - project = merge_request.target_project = link_to project.name_with_namespace, namespace_project_path(project.namespace, project) - %span.merge-request-status.prepend-left-10 - - if merge_request.merged? - MERGED - - elsif merge_request.closed? - CLOSED + + - if merge_request.merged? + %span.merge-request-status.prepend-left-10.merged + Merged + - elsif merge_request.closed? + %span.merge-request-status.prepend-left-10.closed + Closed + - else + %span.merge-request-status.prepend-left-10.open + Open + - if @closed_by_merge_requests.present? %li = render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count} diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index c56b6cc11f5..13e2150f997 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -1,9 +1,6 @@ - if can?(current_user, :push_code, @project) .pull-right - #new-branch.new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)} - = link_to '#', class: 'checking btn btn-grouped', disabled: 'disabled' do - = icon('spinner spin') - Checking branches + #new-branch.new-branch{ 'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue) } = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn btn-new btn-inverted btn-grouped has-tooltip available hide', title: @issue.to_branch_name do New branch diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index c493ff3585b..18e8372ecab 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -6,12 +6,15 @@ = content_for :sub_nav do = render "projects/issues/head" +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('filtered_search/filtered_search_bundle.js') + = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues") -%div{ class: (container_class) } - - if @project.issues.any? +- if project_issues(@project).exists? + %div{ class: (container_class) } .top-area = render 'shared/issuable/nav', type: :issues .nav-controls @@ -20,7 +23,6 @@ = icon('rss') %span.icon-label Subscribe - = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) - if can? current_user, :create_issue, @project = link_to new_namespace_project_issue_path(@project.namespace, @project, @@ -30,27 +32,11 @@ title: "New Issue", id: "new_issue_link" do New Issue - = render 'shared/issuable/filter', type: :issues + = render 'shared/issuable/search_bar', type: :issues .issues-holder = render 'issues' - if new_issue_email = render 'issue_by_email', email: new_issue_email - - else - .blank-state.blank-state-welcome - %h2.blank-state-title.blank-state-welcome-title - Welcome to GitLab Issues - %p.blank-state-text - Code, test, and deploy together - .blank-state - .blank-state-icon - = custom_icon("issues", size: 50) - %h3.blank-state-title - You don't have any issues right now. - %p.blank-state-text - Issues are the best way to track your project progress - - if can? current_user, :create_issue, @project - = link_to new_namespace_project_issue_path(@project.namespace, @project), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do - New Issue - - if new_issue_email - = render 'issue_by_email', email: new_issue_email +- else + = render 'shared/empty_states/issues', button_path: new_namespace_project_issue_path(@project.namespace, @project) diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index bd629b5c519..9fa00811af0 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,6 +1,9 @@ +- @content_class = "limit-container-width" unless fluid_layout - page_title "#{@issue.title} (#{@issue.to_reference})", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/vue_resource.js') .clearfix.detail-page-header .issuable-header diff --git a/app/views/projects/mattermosts/_no_teams.html.haml b/app/views/projects/mattermosts/_no_teams.html.haml new file mode 100644 index 00000000000..605c7f61dee --- /dev/null +++ b/app/views/projects/mattermosts/_no_teams.html.haml @@ -0,0 +1,12 @@ +%p + You aren’t a member of any team on the Mattermost instance at + %strong= Gitlab.config.mattermost.host +%p + To install this service, + = link_to "#{Gitlab.config.mattermost.host}/select_team", target: '__blank' do + join a team + = icon('external-link') + and try again. +%hr +.clearfix + = link_to 'Go back', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg pull-right' diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml new file mode 100644 index 00000000000..a80f9aa4c4a --- /dev/null +++ b/app/views/projects/mattermosts/_team_selection.html.haml @@ -0,0 +1,47 @@ +%p + This service will be installed on the Mattermost instance at + %strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host +%hr += form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project)) do |f| + %h4 Team + %p + = @teams.one? ? 'The team' : 'Select the team' + where the slash commands will be used in + - selected_id = @teams.one? ? @teams.keys.first : 0 + - options = mattermost_teams_options(@teams) + - options = options_for_select(options, selected_id) + = f.select(:team_id, options, {}, { class: 'form-control', disabled: @teams.one?, selected: selected_id }) + = f.hidden_field(:team_id, value: selected_id) if @teams.one? + .help-block + - if @teams.one? + This is the only available team. + - else + The list shows all available teams. + To create a team, + = link_to "#{Gitlab.config.mattermost.host}/create_team" do + use Mattermost's interface + = icon('external-link') + or ask your Mattermost system administrator. + %hr + %h4 Command trigger word + %p Choose the word that will trigger commands + = f.text_field(:trigger, value: @project.path, class: 'form-control') + .help-block + %p + Trigger word must be unique, and can't begin with a slash or contain any spaces. + Use 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 + %p + Reserved: + = link_to 'https://docs.mattermost.com/help/messaging/executing-commands.html#built-in-commands', target: '__blank' do + see list of built-in slash commands + = icon('external-link') + %hr + .clearfix + .pull-right + = link_to 'Cancel', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg' + = f.submit 'Install', class: 'btn btn-save btn-lg' diff --git a/app/views/projects/mattermosts/new.html.haml b/app/views/projects/mattermosts/new.html.haml new file mode 100644 index 00000000000..96b1d2aee61 --- /dev/null +++ b/app/views/projects/mattermosts/new.html.haml @@ -0,0 +1,8 @@ +.service-installation + .inline.pull-right + = custom_icon('mattermost_logo', size: 48) + %h3 Install Mattermost Command + - if @teams.empty? + = render 'no_teams' + - else + = render 'team_selection' diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 12408068834..e3b0aa7e644 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,4 +1,4 @@ -%li{ class: mr_css_classes(merge_request) } +%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } } - if @bulk_edit .issue-check = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue" @@ -15,9 +15,9 @@ = icon('ban') CLOSED - - if merge_request.pipeline + - if merge_request.head_pipeline %li - = render_pipeline_status(merge_request.pipeline) + = render_pipeline_status(merge_request.head_pipeline) - if merge_request.open? && merge_request.broken? %li @@ -39,7 +39,7 @@ = icon('thumbs-down') = downvotes - - note_count = merge_request.mr_and_commit_notes.user.count + - note_count = merge_request.related_notes.user.count %li = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do = icon('comments') @@ -54,15 +54,18 @@ = link_to namespace_project_commits_path(merge_request.project.namespace, merge_request.project, merge_request.target_branch) do = icon('code-fork') = merge_request.target_branch + - if merge_request.milestone = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do = icon('clock-o') = merge_request.milestone.title + - if merge_request.labels.any? - merge_request.labels.each do |label| = link_to_label(label, subject: merge_request.project, type: :merge_request) + - if merge_request.tasks? %span.task-status diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 9c6f562f7db..36c6e7a8dad 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -10,8 +10,8 @@ %span.pull-right = link_to 'Change branches', mr_change_branches_path(@merge_request) %hr -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input' } do |f| - = render 'shared/issuable/form', f: f, issuable: @merge_request += 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, commits: @commits = f.hidden_field :source_project_id = f.hidden_field :source_branch = f.hidden_field :target_project_id @@ -34,10 +34,6 @@ = link_to url_for(params), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do Pipelines %span.badge= @pipelines.size - %li.builds-tab - = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do - Builds - %span.badge= @statuses.size %li.diffs-tab = link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do Changes @@ -49,8 +45,6 @@ #diffs.diffs.tab-pane - # This tab is always loaded via AJAX - if @pipelines.any? - #builds.builds.tab-pane - = render "projects/merge_requests/show/builds" #pipelines.pipelines.tab-pane = render "projects/merge_requests/show/pipelines" @@ -64,6 +58,5 @@ }); :javascript var merge_request = new MergeRequest({ - action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}", - buildsLoaded: "#{@pipelines.any? ? 'true' : 'false'}" + action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}" }); diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index f57abe73977..2a7cd3a19d0 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,13 +1,15 @@ +- @content_class = "limit-container-width" unless fluid_layout - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/vue_resource.js') = page_specific_javascript_tag('diff_notes/diff_notes_bundle.js') -.merge-request{'data-url' => merge_request_path(@merge_request)} +.merge-request{ 'data-url' => merge_request_path(@merge_request) } = render "projects/merge_requests/show/mr_title" - .merge-request-details.issuable-details{data: {id: @merge_request.project.id}} + .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } } = render "projects/merge_requests/show/mr_box" .append-bottom-default.mr-source-target.prepend-top-default - if @merge_request.open? @@ -31,7 +33,7 @@ %span.label-branch= source_branch_with_namespace(@merge_request) %span into %span.label-branch - = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) + = link_to_if @merge_request.target_branch_exists?, @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) - if @merge_request.open? && @merge_request.diverged_from_target_branch? %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind) @@ -41,69 +43,63 @@ = render "projects/merge_requests/widget/show.html.haml" - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user) - .light.prepend-top-default.append-bottom-default + .merge-manually.light.prepend-top-default You can also accept this merge request manually using the = succeed '.' do = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - - if @commits_count.nonzero? - .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } - %div{ class: container_class } - %ul.merge-request-tabs.nav-links.no-top.no-bottom - %li.notes-tab - = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do - Discussion - %span.badge= @merge_request.mr_and_commit_notes.user.count - - if @merge_request.source_project - %li.commits-tab - = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do - Commits - %span.badge= @commits_count - - if @pipeline - %li.pipelines-tab - = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do - Pipelines - %span.badge= @pipelines.size - %li.builds-tab - = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do - Builds - %span.badge= @statuses.size - %li.diffs-tab - = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do - Changes - %span.badge= @merge_request.diff_size - %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true } - %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" } + .content-block.content-block-small.emoji-list-container + = render 'award_emoji/awards_block', awardable: @merge_request, inline: true + + .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } + %div{ class: container_class } + %ul.merge-request-tabs.nav-links.no-top.no-bottom + %li.notes-tab + = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do + Discussion + %span.badge= @merge_request.related_notes.user.count + - if @merge_request.source_project + %li.commits-tab + = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do + Commits + %span.badge= @commits_count + - if @pipelines.any? + %li.pipelines-tab + = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do + Pipelines + %span.badge= @pipelines.size + %li.diffs-tab + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do + Changes + %span.badge= @merge_request.diff_size + %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true } + %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" } + %div .line-resolve-all{ "v-show" => "discussionCount > 0", ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } %span.line-resolve-btn.is-disabled{ type: "button", ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } = render "shared/icons/icon_status_success.svg" %span.line-resolve-text - {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ discussionCount | pluralize 'discussion' }} resolved + {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved = render "discussions/jump_to_next" - .tab-content#diff-notes-app - #notes.notes.tab-pane.voting_notes - .content-block.content-block-small - = render 'award_emoji/awards_block', awardable: @merge_request, inline: true - - .row - %section.col-md-12 - .issuable-discussion - = render "projects/merge_requests/discussion" + .tab-content#diff-notes-app + #notes.notes.tab-pane.voting_notes + .row + %section.col-md-12 + .issuable-discussion + = render "projects/merge_requests/discussion" - #commits.commits.tab-pane - - # This tab is always loaded via AJAX - #builds.builds.tab-pane - - # This tab is always loaded via AJAX - #pipelines.pipelines.tab-pane - - # This tab is always loaded via AJAX - #diffs.diffs.tab-pane - - # This tab is always loaded via AJAX + #commits.commits.tab-pane + - # This tab is always loaded via AJAX + #pipelines.pipelines.tab-pane + - # This tab is always loaded via AJAX + #diffs.diffs.tab-pane + - # This tab is always loaded via AJAX - .mr-loading-status - = spinner + .mr-loading-status + = spinner = render 'shared/issuable/sidebar', issuable: @merge_request - if @merge_request.can_be_reverted?(current_user) @@ -117,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 d9f74d2cbfb..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" @@ -9,32 +10,29 @@ = render 'shared/issuable/sidebar', issuable: @merge_request -#conflicts{"v-cloak" => "true", data: { conflicts_path: conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request, format: :json), +#conflicts{ "v-cloak" => "true", data: { conflicts_path: conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request, format: :json), resolve_conflicts_path: resolve_conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request) } } - .loading{"v-if" => "isLoading"} + .loading{ "v-if" => "isLoading" } %i.fa.fa-spinner.fa-spin - .nothing-here-block{"v-if" => "hasError"} + .nothing-here-block{ "v-if" => "hasError" } {{conflictsData.errorMessage}} = render partial: "projects/merge_requests/conflicts/commit_stats" - .files-wrapper{"v-if" => "!isLoading && !hasError"} + .files-wrapper{ "v-if" => "!isLoading && !hasError" } .files - .diff-file.file-holder.conflict{"v-for" => "file in conflictsData.files"} + .diff-file.file-holder.conflict{ "v-for" => "file in conflictsData.files" } .file-title - %i.fa.fa-fw{":class" => "file.iconClass"} + %i.fa.fa-fw{ ":class" => "file.iconClass" } %strong {{file.filePath}} = render partial: 'projects/merge_requests/conflicts/file_actions' .diff-content.diff-wrap-lines - .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } + .diff-wrap-lines.code.file-content.js-syntax-highlight{ "v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } = render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines" - .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } - = render partial: "projects/merge_requests/conflicts/components/parallel_conflict_lines" - %div{"v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'"} + .diff-wrap-lines.code.file-content.js-syntax-highlight{ "v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } + %parallel-conflict-lines{ ":file" => "file" } + %div{ "v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'" } = render partial: "projects/merge_requests/conflicts/components/diff_file_editor" = render partial: "projects/merge_requests/conflicts/submit_form" - --# Components -= render partial: 'projects/merge_requests/conflicts/components/parallel_conflict_line' diff --git a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml index 5ab3cd96163..964dc40a213 100644 --- a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml +++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml @@ -1,9 +1,9 @@ -.content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"} - .inline-parallel-buttons{"v-if" => "showDiffViewTypeSwitcher"} +.content-block.oneline-block.files-changed{ "v-if" => "!isLoading && !hasError" } + .inline-parallel-buttons{ "v-if" => "showDiffViewTypeSwitcher" } .btn-group - %button.btn{":class" => "{'active': !isParallel}", "@click" => "handleViewTypeChange('inline')"} + %button.btn{ ":class" => "{'active': !isParallel}", "@click" => "handleViewTypeChange('inline')" } Inline - %button.btn{":class" => "{'active': isParallel}", "@click" => "handleViewTypeChange('parallel')"} + %button.btn{ ":class" => "{'active': isParallel}", "@click" => "handleViewTypeChange('parallel')" } Side-by-side .js-toggle-container diff --git a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml index 05af57acf03..2595ce74ac0 100644 --- a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml +++ b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml @@ -1,5 +1,5 @@ .file-actions - .btn-group{"v-if" => "file.type === 'text'"} + .btn-group{ "v-if" => "file.type === 'text'" } %button.btn{ ":class" => "{ 'active': file.resolveMode == 'interactive' }", '@click' => "onClickResolveModeButton(file, 'interactive')", type: 'button' } @@ -8,5 +8,5 @@ '@click' => "onClickResolveModeButton(file, 'edit')", type: 'button' } Edit inline - %a.btn.view-file.btn-file-option{":href" => "file.blobPath"} + %a.btn.view-file.btn-file-option{ ":href" => "file.blobPath" } View file @{{conflictsData.shortCommitSha}} diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml index 6ffaa9ad4d2..62c9748c510 100644 --- a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml +++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml @@ -10,7 +10,7 @@ .col-sm-offset-2.col-sm-10 .row .col-xs-6 - %button{ type: "button", class: "btn btn-success js-submit-button", "@click" => "commit()", ":disabled" => "!readyToCommit" } + %button.btn.btn-success.js-submit-button{ type: "button", "@click" => "commit()", ":disabled" => "!readyToCommit" } %span {{commitButtonText}} .col-xs-6.text-right = link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel" diff --git a/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml b/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml index 3c927d362c2..aff3fb82fa6 100644 --- a/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml +++ b/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml @@ -1,4 +1,4 @@ -%diff-file-editor{"inline-template" => "true", ":file" => "file", ":on-cancel-discard-confirmation" => "cancelDiscardConfirmation", ":on-accept-discard-confirmation" => "acceptDiscardConfirmation"} +%diff-file-editor{ "inline-template" => "true", ":file" => "file", ":on-cancel-discard-confirmation" => "cancelDiscardConfirmation", ":on-accept-discard-confirmation" => "acceptDiscardConfirmation" } .diff-editor-wrap{ "v-show" => "file.showEditor" } .discard-changes-alert-wrap{ "v-if" => "file.promptDiscardConfirmation" } .discard-changes-alert diff --git a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml index f094df7fcaa..d828cb6cf9e 100644 --- a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml +++ b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml @@ -1,15 +1,14 @@ -%inline-conflict-lines{ "inline-template" => "true", ":file" => "file"} +%inline-conflict-lines{ "inline-template" => "true", ":file" => "file" } %table - %tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"} - %td.diff-line-num.new_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} + %tr.line_holder.diff-inline{ "v-for" => "line in file.inlineLines" } + %td.diff-line-num.new_line{ ":class" => "lineCssClass(line)", "v-if" => "!line.isHeader" } %a {{line.new_line}} - %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} + %td.diff-line-num.old_line{ ":class" => "lineCssClass(line)", "v-if" => "!line.isHeader" } %a {{line.old_line}} - %td.line_content{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} - {{{line.richText}}} - %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} - %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} - %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} - %strong {{{line.richText}}} + %td.line_content{ ":class" => "lineCssClass(line)", "v-if" => "!line.isHeader", "v-html" => "line.richText" } + %td.diff-line-num.header{ ":class" => "lineCssClass(line)", "v-if" => "line.isHeader" } + %td.diff-line-num.header{ ":class" => "lineCssClass(line)", "v-if" => "line.isHeader" } + %td.line_content.header{ ":class" => "lineCssClass(line)", "v-if" => "line.isHeader" } + %strong{ "v-html" => "line.richText" } %button.btn{ "@click" => "handleSelected(file, line.id, line.section)" } {{line.buttonTitle}} diff --git a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml deleted file mode 100644 index 5690bf7419c..00000000000 --- a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%script{"id" => 'parallel-conflict-line', "type" => "text/x-template"} - %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} - %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} - %strong {{line.richText}} - %button.btn{"@click" => "handleSelected(file, line.id, line.section)"} - {{line.buttonTitle}} - %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} - {{line.lineNumber}} - %td.line_content.parallel{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} - {{{line.richText}}} diff --git a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml deleted file mode 100644 index a8ecdf59393..00000000000 --- a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -%parallel-conflict-lines{"inline-template" => "true", ":file" => "file"} - %table - %tr.line_holder.parallel{"v-for" => "section in file.parallelLines"} - %td{"is"=>"parallel-conflict-line", "v-for" => "line in section", ":line" => "line", ":file" => "file"} diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml deleted file mode 100644 index 808ef7fed27..00000000000 --- a/app/views/projects/merge_requests/show/_builds.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render "projects/commit/pipeline", pipeline: @pipeline, link_to_commit: true diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index a0e12fb3f38..11793919ff7 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -1,6 +1,8 @@ -.content-block.oneline-block - = icon("sort-amount-desc") - Most recent commits displayed first - -%ol#commits-list.list-unstyled - = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch +- if @commits.empty? + .commits-empty + %h4 + There are no commits yet. + = custom_icon ('illustration_no_commits') +- else + %ol#commits-list.list-unstyled + = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index 99c71e1454a..5f048d04b27 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,13 +1,5 @@ -- if @merge_request_diff.collected? +- if @merge_request_diff.collected? || @merge_request_diff.overflow? = render 'projects/merge_requests/show/versions' = render "projects/diffs/diffs", diffs: @diffs - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} -- else - .alert.alert-warning - %h4 - Changes view for this comparison is extremely large. - %p - You can - = link_to "download it", merge_request_path(@merge_request, format: :diff), class: "vlink" - instead. diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index f1d5441f9dd..93ed4b68e0e 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -1,14 +1,14 @@ -%div#modal_merge_info.modal +#modal_merge_info.modal .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h3 Check out, review, and merge locally .modal-body %p %strong Step 1. Fetch and check out the branch for this merge request - = clipboard_button(clipboard_target: "pre#merge-info-1") + = clipboard_button(clipboard_target: "pre#merge-info-1", title: "Copy commands to clipboard") %pre.dark#merge-info-1 - if @merge_request.for_fork? :preserve @@ -25,7 +25,7 @@ %p %strong Step 3. Merge the branch and fix any conflicts that come up - = clipboard_button(clipboard_target: "pre#merge-info-3") + = clipboard_button(clipboard_target: "pre#merge-info-3", title: "Copy commands to clipboard") %pre.dark#merge-info-3 - if @merge_request.for_fork? :preserve @@ -38,7 +38,7 @@ %p %strong Step 4. Push the result of the merge to GitLab - = clipboard_button(clipboard_target: "pre#merge-info-4") + = clipboard_button(clipboard_target: "pre#merge-info-4", title: "Copy commands to clipboard") %pre.dark#merge-info-4 :preserve git push origin #{h @merge_request.target_branch} diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index ed23d06ee5e..683cb8a5a27 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -4,7 +4,7 @@ %div - if @merge_request.description.present? - .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''} + .description{ class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : '' } .wiki = preserve do = markdown_field(@merge_request, :description) diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml index eab48b78cb3..b0f3c86fd21 100644 --- a/app/views/projects/merge_requests/show/_versions.html.haml +++ b/app/views/projects/merge_requests/show/_versions.html.haml @@ -1,6 +1,6 @@ - if @merge_request_diffs.size > 1 .mr-version-controls - %div.mr-version-menus-container.content-block + .mr-version-menus-container.content-block Changes between %span.dropdown.inline.mr-version-dropdown %a.dropdown-toggle.btn.btn-default{ data: {toggle: :dropdown} } @@ -13,13 +13,13 @@ .dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-title %span Version: - %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } } = icon('times', class: 'dropdown-menu-close-icon') .dropdown-content %ul - @merge_request_diffs.each do |merge_request_diff| %li - = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do + = link_to merge_request_version_path(@project, @merge_request, merge_request_diff, @start_sha), class: ('is-active' if merge_request_diff == @merge_request_diff) do %strong - if merge_request_diff.latest? latest version @@ -27,7 +27,7 @@ version #{version_index(merge_request_diff)} .monospace #{short_sha(merge_request_diff.head_commit_sha)} %small - #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)}, + #{number_with_delimiter(merge_request_diff.commits_count)} #{'commit'.pluralize(merge_request_diff.commits_count)}, = time_ago_with_tooltip(merge_request_diff.created_at) - if @merge_request_diff.base_commit_sha @@ -43,7 +43,7 @@ .dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-title %span Compared with: - %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } } = icon('times', class: 'dropdown-menu-close-icon') .dropdown-content %ul diff --git a/app/views/projects/merge_requests/widget/_closed.html.haml b/app/views/projects/merge_requests/widget/_closed.html.haml index f3cc0e7e8a1..15f47ecf210 100644 --- a/app/views/projects/merge_requests/widget/_closed.html.haml +++ b/app/views/projects/merge_requests/widget/_closed.html.haml @@ -6,7 +6,7 @@ - if @merge_request.closed_event by #{link_to_member(@project, @merge_request.closed_event.author, avatar: true)} #{time_ago_with_tooltip(@merge_request.closed_event.created_at)} - %p + %p = succeed '.' do The changes were not merged into %span.label-branch= @merge_request.target_branch diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index a82c846baa7..c80dc33058d 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,25 +1,23 @@ - if @pipeline .mr-widget-heading - %w[success success_with_warnings skipped canceled failed running pending].each do |status| - .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) } + .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) } = ci_icon_for_status(status) %span Pipeline = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline' = ci_label_for_status(status) for - - commit = @merge_request.diff_head_commit = succeed "." do = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" %span.ci-coverage - = link_to "View details", pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'pipelines'} - elsif @merge_request.has_ci? - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX - - # Remove in later versions when services like Jenkins will set CI status via Commit status API + - # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API .mr-widget-heading - %w[success skipped canceled failed running pending].each do |status| - .ci_widget{class: "ci-#{status}", style: "display:none"} + .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: "display:none" } = ci_icon_for_status(status) %span CI build @@ -29,18 +27,16 @@ = succeed "." do = link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace" %span.ci-coverage - - if details_path = ci_build_details_path(@merge_request) - = link_to "View details", details_path, :"data-no-turbolink" => "data-no-turbolink" .ci_widget = icon("spinner spin") Checking CI status for #{@merge_request.diff_head_commit.short_id}… - .ci_widget.ci-not_found{style: "display:none"} + .ci_widget.ci-not_found{ style: "display:none" } = icon("times-circle") Could not find CI status for #{@merge_request.diff_head_commit.short_id}. - .ci_widget.ci-error{style: "display:none"} + .ci_widget.ci-error{ style: "display:none" } = icon("times-circle") Could not connect to the CI server. Please check your settings and try again. diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml index d836a253507..9eef011b591 100644 --- a/app/views/projects/merge_requests/widget/_merged_buttons.haml +++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml @@ -5,10 +5,10 @@ - if can_remove_source_branch || mr_can_be_reverted || mr_can_be_cherry_picked .clearfix.merged-buttons - if can_remove_source_branch - = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-sm remove_source_branch" do + = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default remove_source_branch" do = icon('trash-o') Remove Source Branch - if mr_can_be_reverted - = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm') + = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "warning") - if mr_can_be_cherry_picked - = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm') + = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "default") diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 01314eb37d0..c0d6ab669b8 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -9,10 +9,10 @@ - if @project.archived? = render 'projects/merge_requests/widget/open/archived' - - elsif @merge_request.commits.blank? - = render 'projects/merge_requests/widget/open/nothing' - elsif @merge_request.branch_missing? = render 'projects/merge_requests/widget/open/missing_branch' + - elsif @merge_request.has_no_commits? + = render 'projects/merge_requests/widget/open/nothing' - elsif @merge_request.unchecked? = render 'projects/merge_requests/widget/open/check' - elsif @merge_request.cannot_be_merged? && !resolved_conflicts @@ -23,18 +23,23 @@ = render 'projects/merge_requests/widget/open/merge_when_build_succeeds' - elsif !@merge_request.can_be_merged_by?(current_user) = render 'projects/merge_requests/widget/open/not_allowed' - - elsif !@merge_request.mergeable_ci_state? + - elsif !@merge_request.mergeable_ci_state? && (@pipeline.failed? || @pipeline.canceled?) = render 'projects/merge_requests/widget/open/build_failed' - elsif !@merge_request.mergeable_discussions_state? = render 'projects/merge_requests/widget/open/unresolved_discussions' - elsif @merge_request.can_be_merged? || resolved_conflicts = render 'projects/merge_requests/widget/open/accept' - - if mr_closes_issues.present? + - if mr_closes_issues.present? || mr_issues_mentioned_but_not_closing.present? .mr-widget-footer %span - %i.fa.fa-check - Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)} - = succeed '.' do - != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author - = mr_assign_issues_link + = icon('check') + - if mr_closes_issues.present? + Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)} + = succeed '.' do + != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author + = mr_assign_issues_link + - if mr_issues_mentioned_but_not_closing.present? + #{"Issue".pluralize(mr_issues_mentioned_but_not_closing.size)} + != markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author + #{mr_issues_mentioned_but_not_closing.size > 1 ? 'are' : 'is'} mentioned but will not be closed. diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 608fdf1c5f5..38328501ffd 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -14,7 +14,7 @@ ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", ci_environments_status_url: "#{ci_environments_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", gitlab_icon: "#{asset_path 'gitlab_logo.png'}", - ci_status: "#{@merge_request.pipeline ? @merge_request.pipeline.status : ''}", + ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}", ci_message: { normal: "Build {{status}} for \"{{title}}\"", preparing: "{{status}} build for \"{{title}}\"" @@ -24,12 +24,10 @@ preparing: "{{status}} build", normal: "Build {{status}}" }, - builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" }; if (typeof merge_request_widget !== 'undefined') { - clearInterval(merge_request_widget.fetchBuildStatusInterval); merge_request_widget.cancelPolling(); merge_request_widget.clearEventListeners(); } diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index ce43ca3a286..7809e9c8c72 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -1,3 +1,6 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('merge_request_widget/ci_bundle.js') + - status_class = @pipeline ? " ci-#{@pipeline.status}" : nil = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f| @@ -9,7 +12,7 @@ - if @pipeline && @pipeline.active? %span.btn-group = button_tag class: "btn btn-create js-merge-button merge_when_build_succeeds" do - Merge When Build Succeeds + Merge When Pipeline Succeeds - unless @project.only_allow_merge_if_build_succeeds? = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do = icon('caret-down') @@ -19,7 +22,7 @@ %li = link_to "#", class: "merge_when_build_succeeds" do = icon('check fw') - Merge When Build Succeeds + Merge When Pipeline Succeeds %li = link_to "#", class: "accept_merge_request" do = icon('warning fw') @@ -41,25 +44,9 @@ Modify commit message .js-toggle-content.hide.prepend-top-default = render 'shared/commit_message_container', params: params, + message_with_description: @merge_request.merge_commit_message(include_description: true), + message_without_description: @merge_request.merge_commit_message, text: @merge_request.merge_commit_message, rows: 14, hint: true = hidden_field_tag :merge_when_build_succeeds, "", autocomplete: "off" - - :javascript - $('.accept-mr-form').on('ajax:send', function() { - $(".accept-mr-form :input").disable(); - }); - - $('.accept_merge_request').on('click', function() { - $('.js-merge-button').html("<i class='fa fa-spinner fa-spin'></i> Merge in progress"); - }); - - $('.merge_when_build_succeeds').on('click', function() { - $("#merge_when_build_succeeds").val("1"); - }); - - $('.js-merge-dropdown a').on('click', function(e) { - e.preventDefault(); - $(this).closest("form").submit(); - }); diff --git a/app/views/projects/merge_requests/widget/open/_archived.html.haml b/app/views/projects/merge_requests/widget/open/_archived.html.haml index ab30fa6b243..0d61e56d8fb 100644 --- a/app/views/projects/merge_requests/widget/open/_archived.html.haml +++ b/app/views/projects/merge_requests/widget/open/_archived.html.haml @@ -1,4 +1,4 @@ -%h4 +%h4 Project is archived %p This merge request cannot be merged because archived projects cannot be written to. diff --git a/app/views/projects/merge_requests/widget/open/_check.html.haml b/app/views/projects/merge_requests/widget/open/_check.html.haml index e16878ba513..50086767446 100644 --- a/app/views/projects/merge_requests/widget/open/_check.html.haml +++ b/app/views/projects/merge_requests/widget/open/_check.html.haml @@ -1,9 +1,6 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('merge_request_widget/ci_bundle.js') + %strong = icon("spinner spin") Checking ability to merge automatically… - -:javascript - $(function() { - merge_request_widget.getMergeStatus(); - }); - diff --git a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml index af3096f04d9..c98b2c42597 100644 --- a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml +++ b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml @@ -1,21 +1,23 @@ +- can_resolve = @merge_request.conflicts_can_be_resolved_by?(current_user) +- can_resolve_in_ui = @merge_request.conflicts_can_be_resolved_in_ui? +- can_merge = @merge_request.can_be_merged_via_command_line_by?(current_user) + %h4.has-conflicts = icon("exclamation-triangle") This merge request contains merge conflicts %p - Please - - if @merge_request.conflicts_can_be_resolved_by?(current_user) - - if @merge_request.conflicts_can_be_resolved_in_ui? - = link_to "resolve these conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) - - else - %span.has-tooltip{title: "These conflicts cannot be resolved through GitLab"} - resolve these conflicts locally - - else - resolve these conflicts - + To merge this request, resolve these conflicts + - if can_resolve && !can_resolve_in_ui + locally or + - unless can_merge + ask someone with write access to this repository to + merge it locally. - - if @merge_request.can_be_merged_via_command_line_by?(current_user) - #{link_to "merge this request manually", "#modal_merge_info", class: "how_to_merge_link vlink", "data-toggle" => "modal"}. - - else - ask someone with write access to this repository to merge this request manually. +- if (can_resolve && can_resolve_in_ui) || can_merge + .btn-group + - if can_resolve && can_resolve_in_ui + = link_to "Resolve conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn" + - if can_merge + = link_to "Merge locally", "#modal_merge_info", class: "btn how_to_merge_link vlink", "data-toggle" => "modal" diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index 2b6b5e05e86..f70cd09c5f4 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -1,6 +1,9 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('merge_request_widget/ci_bundle.js') + %h4 Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} - to be merged automatically when the build succeeds. + to be merged automatically when the pipeline succeeds. %div %p = succeed '.' do @@ -21,5 +24,5 @@ Remove Source Branch When Merged - if user_can_cancel_automatic_merge - = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-warning btn-sm" do + = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-sm" do Cancel Automatic Merge diff --git a/app/views/projects/merge_requests/widget/open/_nothing.html.haml b/app/views/projects/merge_requests/widget/open/_nothing.html.haml index 35626b624b7..7af8c01c134 100644 --- a/app/views/projects/merge_requests/widget/open/_nothing.html.haml +++ b/app/views/projects/merge_requests/widget/open/_nothing.html.haml @@ -1,4 +1,4 @@ -%h4 +%h4 = icon("exclamation-triangle") Nothing to merge from %span.label-branch= source_branch_with_namespace(@merge_request) diff --git a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml index 35d5677ee37..e094f97f3b6 100644 --- a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml +++ b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml @@ -3,4 +3,8 @@ This merge request has unresolved discussions %p - Please resolve these discussions to allow this merge request to be merged.
\ No newline at end of file + Please resolve these discussions + - if @project.issues_enabled? && can?(current_user, :create_issue, @project) + or + = link_to "open an issue to resolve them later", new_namespace_project_issue_path(@project.namespace, @project, merge_request_for_resolving_discussions: @merge_request.iid) + to allow this merge request to be merged. diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index cbf1ba04170..0f4a8508751 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -14,17 +14,12 @@ = render 'projects/notes/hints' .clearfix .error-alert - .col-md-6 - .form-group - = f.label :due_date, "Due Date", class: "control-label" - .col-sm-10 - = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date" - %a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date + = render "shared/milestones/form_dates", f: f .form-actions - if @milestone.new_record? = f.submit 'Create milestone', class: "btn-create btn" = link_to "Cancel", namespace_project_milestones_path(@project.namespace, @project), class: "btn btn-cancel" - -else + - else = f.submit 'Save changes', class: "btn-save btn" = link_to "Cancel", namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-cancel" diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index f9ba77e87b5..c3a6096aa54 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -4,21 +4,24 @@ = render "projects/issues/head" %div{ class: container_class } - .detail-page-header + .detail-page-header.milestone-page-header .status-box{ class: status_box_class(@milestone) } - if @milestone.closed? Closed - elsif @milestone.expired? Past due + - elsif @milestone.upcoming? + Upcoming - else Open - %span.identifier - Milestone ##{@milestone.iid} - - if @milestone.expires_at - %span.creator - · - = @milestone.expires_at - .pull-right + .header-text-content + %span.identifier + Milestone ##{@milestone.iid} + - if @milestone.due_date || @milestone.start_date + %span.creator + · + = milestone_date_range(@milestone) + .milestone-buttons - if can?(current_user, :admin_milestone, @project) - if @milestone.active? = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped" diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 932603f03b0..064e92b15eb 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -69,10 +69,15 @@ = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do = icon('bug', text: 'Fogbugz') %div + - if gitea_import_enabled? + = link_to new_import_gitea_url, class: 'btn import_gitea' do + = custom_icon('go_logo') + Gitea + %div - if git_import_enabled? = link_to "#", class: 'btn js-toggle-button import_git' do = icon('git', text: 'Repo by URL') - %div{ class: 'import_gitlab_project' } + .import_gitlab_project - if gitlab_project_import_enabled? = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do = icon('gitlab', text: 'GitLab export') @@ -90,7 +95,8 @@ = f.label :visibility_level, class: 'label-light' do Visibility Level = link_to "(?)", help_page_path("public_access/public_access") - = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: @project.visibility_level, form_model: @project) + = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project + = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index 8620f492282..e8e450742b5 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -1,11 +1,14 @@ .note-edit-form - = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f| - = note_target_fields(note) - = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do - = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..." + = form_tag '#', method: :put, class: 'edit-note common-note-form js-quick-submit' do + = hidden_field_tag :target_id, '', class: 'js-form-target-id' + = hidden_field_tag :target_type, '', class: 'js-form-target-type' + = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview', referenced_users: true } do + = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..." = render 'projects/notes/hints' .note-form-actions.clearfix - = f.submit 'Save Comment', class: 'btn btn-nr btn-save js-comment-button' + .settings-message.note-edit-warning.js-edit-warning + Finish editing this message first! + = submit_tag 'Save Comment', class: 'btn btn-nr btn-save js-comment-button' %button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' } Cancel diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 46b402545cd..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 @@ -23,5 +24,5 @@ .note-form-actions.clearfix = f.submit 'Comment', class: "btn btn-nr btn-create append-right-10 comment-btn js-comment-button" = yield(:note_actions) - %a.btn.btn-cancel.js-note-discard{role: "button", data: {cancel_text: "Cancel"}} + %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } } Discard draft diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index ab719e38904..36c388c3318 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -5,15 +5,18 @@ %li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} } .timeline-entry-inner .timeline-icon - %a{href: user_path(note.author)} + %a{ href: user_path(note.author) } = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' .timeline-content .note-header = link_to_member(note.project, note.author, avatar: false) - .inline.note-headline-light + .note-headline-light = note.author.to_reference - unless note.system commented + - if note.system + %span.system-note-message + = note.redacted_note_html %a{ href: "##{dom_id(note)}" } = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') - unless note.system? @@ -32,7 +35,7 @@ "resolved-by" => "#{note.resolved_by.try(:name)}", "v-show" => "#{can_resolve || note.resolved?}", "inline-template" => true, - "v-ref:note_#{note.id}" => true } + "ref" => "note_#{note.id}" } .note-action-button = icon("spin spinner", "v-show" => "loading") @@ -43,7 +46,7 @@ "@click" => "resolve", ":title" => "buttonText", "v-show" => "!loading", - "v-el:button" => true } + ":ref" => "'button'" } = render "shared/icons/icon_status_success.svg" @@ -58,16 +61,20 @@ = icon('pencil', class: 'link-highlight') = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do = icon('trash-o', class: 'danger-highlight') - .note-body{class: note_editable ? 'js-task-list-container' : ''} + .note-body{ class: note_editable ? 'js-task-list-container' : '' } .note-text.md = preserve do = note.redacted_note_html - = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) + = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - if note_editable - = render 'projects/notes/edit_form', note: note + .original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } } + #{note.note} + %textarea.hidden.js-task-list-field.original-task-list #{note.note} .note-awards = render 'award_emoji/awards_block', awardable: note, inline: false - + - if note.system + .system-note-commit-list-toggler + Toggle commit list - if note.attachment.url .note-attachment - if note.attachment.image? diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 00b62a595ff..fbd2bff5bbb 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -1,5 +1,8 @@ %ul#notes-list.notes.main-notes-list.timeline = render "projects/notes/notes" + += render 'projects/notes/edit_form' + %ul.notes.notes-form.timeline %li.timeline-entry .flash-container.timeline-content @@ -20,4 +23,4 @@ to post a comment :javascript - var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") + var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") diff --git a/app/views/projects/pipelines/_graph.html.haml b/app/views/projects/pipelines/_graph.html.haml new file mode 100644 index 00000000000..0202833c0bf --- /dev/null +++ b/app/views/projects/pipelines/_graph.html.haml @@ -0,0 +1,4 @@ +- pipeline = local_assigns.fetch(:pipeline) +.pipeline-visualization.pipeline-graph + %ul.stage-column-list + = render partial: "projects/stage/graph", collection: pipeline.stages, as: :stage diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index d288efc546f..ca76f13ef5e 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,39 +1,47 @@ -%p -.commit-info-row - Pipeline - = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @pipeline.id), class: "monospace" - with - = pluralize @pipeline.statuses.count(:id), "build" - - if @pipeline.ref - for - = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace" - - if @pipeline.duration - in - = time_interval_in_words(@pipeline.duration) - - if @pipeline.queued_duration - = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})" - - .pull-right - = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do - = ci_icon_for_status(@pipeline.status) - = ci_label_for_status(@pipeline.status) - -- if @commit - .commit-info-row - %span.light Authored by - %strong - = commit_author_link(@commit, avatar: true, size: 24) - #{time_ago_with_tooltip(@commit.authored_date)} - -.commit-info-row - %span.light Commit - = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace" - = clipboard_button(clipboard_text: @pipeline.sha) +.page-content-header + .header-main-content + = render 'ci/status/badge', status: @pipeline.detailed_status(current_user) + %strong Pipeline ##{@commit.pipelines.last.id} + triggered #{time_ago_with_tooltip(@commit.authored_date)} by + = author_avatar(@commit, size: 24) + = commit_author_link(@commit) + .header-action-buttons + - if can?(current_user, :update_pipeline, @pipeline.project) + - if @pipeline.builds.latest.failed.any?(&:retryable?) + = link_to "Retry failed", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'btn btn-inverted-secondary', method: :post + - if @pipeline.builds.running_or_pending.any? + = link_to "Cancel running", cancel_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - if @commit - .commit-box.content-block + .commit-box %h3.commit-title = markdown(@commit.title, pipeline: :single_line) - if @commit.description.present? %pre.commit-description = preserve(markdown(@commit.description, pipeline: :single_line)) + +.info-well + - if @commit.status + .well-segment.pipeline-info + %div{ class: "icon-container ci-status-icon-#{@commit.status}" } + = ci_icon_for_status(@commit.status) + = pluralize @pipeline.statuses.count(:id), "build" + - if @pipeline.ref + from + = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace" + - if @pipeline.duration + in + = time_interval_in_words(@pipeline.duration) + - if @pipeline.queued_duration + = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})" + + .well-segment.branch-info + .icon-container.commit-icon + = custom_icon("icon_commit") + = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace js-details-short" + = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do + %span.text-expander + \... + %span.js-details-content.hide + = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace commit-hash-full" + = clipboard_button(clipboard_text: @pipeline.sha, title: "Copy commit SHA to clipboard") diff --git a/app/views/projects/pipelines/_stage.html.haml b/app/views/projects/pipelines/_stage.html.haml new file mode 100644 index 00000000000..a0b14a7274a --- /dev/null +++ b/app/views/projects/pipelines/_stage.html.haml @@ -0,0 +1,3 @@ +- @stage.statuses.latest.each do |status| + %li + = render 'ci/status/dropdown_graph_badge', subject: status diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml new file mode 100644 index 00000000000..88af41aa835 --- /dev/null +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -0,0 +1,42 @@ +.tabs-holder + %ul.pipelines-tabs.nav-links.no-top.no-bottom + %li.js-pipeline-tab-link + = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: { target: 'div#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do + Pipeline + %li.js-builds-tab-link + = link_to builds_namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do + Builds + %span.badge.js-builds-counter= pipeline.statuses.count + + + +.tab-content + #js-tab-pipeline.tab-pane + .build-content.middle-block.js-pipeline-graph + = render "projects/pipelines/graph", pipeline: pipeline + + #js-tab-builds.tab-pane + - if pipeline.yaml_errors.present? + .bs-callout.bs-callout-danger + %h4 Found errors in your .gitlab-ci.yml: + %ul + - pipeline.yaml_errors.split(",").each do |error| + %li= error + You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} + + - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file + .bs-callout.bs-callout-warning + \.gitlab-ci.yml not found in this commit + + .table-holder.pipeline-holder + %table.table.ci-table.pipeline + %thead + %tr + %th Status + %th Build ID + %th Name + %th + - if pipeline.project.build_coverage_enabled? + %th Coverage + %th + = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 4bc49072f35..df36279ed75 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -5,23 +5,23 @@ %div{ class: container_class } .top-area %ul.nav-links - %li{class: ('active' if @scope.nil?)} + %li{ class: ('active' if @scope.nil?) }> = link_to project_pipelines_path(@project) do All %span.badge.js-totalbuilds-count = number_with_delimiter(@pipelines_count) - %li{class: ('active' if @scope == 'running')} + %li{ class: ('active' if @scope == 'running') }> = link_to project_pipelines_path(@project, scope: :running) do Running %span.badge.js-running-count = number_with_delimiter(@running_or_pending_count) - %li{class: ('active' if @scope == 'branches')} + %li{ class: ('active' if @scope == 'branches') }> = link_to project_pipelines_path(@project, scope: :branches) do Branches - %li{class: ('active' if @scope == 'tags')} + %li{ class: ('active' if @scope == 'tags') }> = link_to project_pipelines_path(@project, scope: :tags) do Tags @@ -35,22 +35,33 @@ = link_to ci_lint_path, class: 'btn btn-default' do %span CI Lint - - %div.content-list.pipelines - - stages = @pipelines.stages + .content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } } - if @pipelines.blank? %div .nothing-here-block No pipelines to show - else - .table-holder - %table.table.ci-table - %thead - %th Status - %th Pipeline - %th Commit - %th Stages - %th - %th.hidden-xs - = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages - - = paginate @pipelines, theme: 'gitlab' + .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"), + "icon_status_canceled" => custom_icon("icon_status_canceled"), + "icon_status_running" => custom_icon("icon_status_running"), + "icon_status_skipped" => custom_icon("icon_status_skipped"), + "icon_status_created" => custom_icon("icon_status_created"), + "icon_status_pending" => custom_icon("icon_status_pending"), + "icon_status_success" => custom_icon("icon_status_success"), + "icon_status_failed" => custom_icon("icon_status_failed"), + "icon_status_warning" => custom_icon("icon_status_warning"), + "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), + "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), + "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), + "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), + "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), + "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), + "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), + "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), + "icon_play" => custom_icon("icon_play"), + "icon_timer" => custom_icon("icon_timer"), + "icon_status_manual" => custom_icon("icon_status_manual"), + } } + + .vue-pipelines-index + += page_specific_javascript_tag('vue_pipelines_index/index.js') diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index 688535ad764..49c1d886423 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -2,10 +2,8 @@ - page_title "Pipeline" = render "projects/pipelines/head" -%div{ class: container_class } - .prepend-top-default - - if @commit - = render "projects/pipelines/info" - %div.block-connector +.js-pipeline-container{ class: container_class, data: { controller_action: "#{controller.action_name}" } } + - if @commit + = render "projects/pipelines/info" - = render "projects/commit/pipeline", pipeline: @pipeline + = render "projects/pipelines/with_tabs", pipeline: @pipeline diff --git a/app/views/projects/pipelines_settings/_badge.html.haml b/app/views/projects/pipelines_settings/_badge.html.haml index 7b7fa56d993..22a3b884520 100644 --- a/app/views/projects/pipelines_settings/_badge.html.haml +++ b/app/views/projects/pipelines_settings/_badge.html.haml @@ -1,4 +1,4 @@ -.row{ class: badge.title.gsub(' ', '-') } +%div{ class: badge.title.gsub(' ', '-') } .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 = badge.title.capitalize diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml index 96221a20502..1f698558bce 100644 --- a/app/views/projects/pipelines_settings/show.html.haml +++ b/app/views/projects/pipelines_settings/show.html.haml @@ -86,6 +86,9 @@ %li tap --coverage-report=text-summary (NodeJS) - %code ^Statements\s*:\s*([^%]+) + %li + excoveralls (Elixir) - + %code \[TOTAL\]\s+(\d+\.\d+)% = f.submit 'Save changes', class: "btn btn-save" diff --git a/app/views/projects/project_members/_index.html.haml b/app/views/projects/project_members/_index.html.haml new file mode 100644 index 00000000000..ab0771b5751 --- /dev/null +++ b/app/views/projects/project_members/_index.html.haml @@ -0,0 +1,22 @@ +.row.prepend-top-default + .col-lg-3.settings-sidebar + %h4.prepend-top-0 + Members + - if can?(current_user, :admin_project_member, @project) + %p + Add a new member to + %strong= @project.name + .col-lg-9 + .light.prepend-top-default + - if can?(current_user, :admin_project_member, @project) + = render "projects/project_members/new_project_member" + + = render 'shared/members/requests', membership_source: @project, requesters: @requesters + .append-bottom-default.clearfix + %h5.member.existing-title + Existing members and groups + - if @group_links.any? + = render 'projects/project_members/groups', group_links: @group_links + + = render 'projects/project_members/team', members: @project_members + = paginate @project_members, theme: "gitlab" diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index 79dcd7a6ee9..2b1c23f7dda 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -1,22 +1,18 @@ = form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f| - .row - .col-md-4.col-lg-6 - = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true) - .help-block.append-bottom-10 - Search for users by name, username, or email, or invite new ones using their email address. - - .col-md-3.col-lg-2 - = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select" - .help-block.append-bottom-10 - = link_to "Read more", help_page_path("user/permissions"), class: "vlink" - about role permissions - - .col-md-3.col-lg-2 - .clearable-input - = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' - %i.clear-icon.js-clear-input - .help-block.append-bottom-10 - On this date, the user(s) will automatically lose access to this project. - - .col-md-2 - = f.submit "Add to project", class: "btn btn-create btn-block" + .form-group + = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true, placeholder: "Search for members to update or invite") + .help-block.append-bottom-10 + Search for members by name, username, or email, or invite new ones using their email address. + .form-group + = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select" + .help-block.append-bottom-10 + = link_to "Read more", help_page_path("user/permissions"), class: "vlink" + about role permissions + .form-group + .clearable-input + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' + %i.clear-icon.js-clear-input + .help-block.append-bottom-10 + On this date, the member(s) will automatically lose access to this project. + = f.submit "Add to project", class: "btn btn-create" + = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default", title: "Import members from another project" diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index c1e894d8f40..5292e73be7a 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -1,7 +1,13 @@ .panel.panel-default .panel-heading - Users with access to + Members with access to %strong #{@project.name} %span.badge= @project_members.total_count + = form_tag namespace_project_settings_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do + .form-group + = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } + %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } + = icon("search") + = render 'shared/members/sort_dropdown' %ul.content-list = render partial: 'shared/members/member', collection: members, as: :member diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml index eef97107d77..42ce4f8001b 100644 --- a/app/views/projects/project_members/import.html.haml +++ b/app/views/projects/project_members/import.html.haml @@ -12,5 +12,4 @@ .form-actions = button_tag 'Import project members', class: "btn btn-create" - = link_to "Cancel", namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-cancel" - + = link_to "Cancel", namespace_project_settings_members_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml deleted file mode 100644 index bdeb704b6da..00000000000 --- a/app/views/projects/project_members/index.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -- page_title "Members" - -.project-members-page.prepend-top-default - %h4.project-members-title.clearfix - Members - = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default pull-right hidden-xs", title: "Import members from another project" - - if can?(current_user, :admin_project_member, @project) - .project-members-new.append-bottom-default - %p.clearfix - Add new user to - %strong= @project.name - = render "new_project_member" - - = render 'shared/members/requests', membership_source: @project, requesters: @requesters - - .append-bottom-default.clearfix - %h5.member.existing-title - Existing users and groups - = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do - .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } - %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } - = icon("search") - - if @group_links.any? - = render 'groups', group_links: @group_links - - = render 'team', members: @project_members - = paginate @project_members, theme: "gitlab" diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml index 91927181efb..d15f4310ff5 100644 --- a/app/views/projects/project_members/update.js.haml +++ b/app/views/projects/project_members/update.js.haml @@ -1,3 +1,4 @@ :plain var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}'); $("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name')); + gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@project_member)}")); diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml index 44fa4b60343..d07bb661615 100644 --- a/app/views/projects/refs/logs_tree.js.haml +++ b/app/views/projects/refs/logs_tree.js.haml @@ -14,8 +14,8 @@ // Load more commit logs for each file in tree // if we still on the same page var url = "#{escape_javascript(@more_log_url)}"; - ajaxGet(url); + gl.utils.ajaxGet(url); } :plain - gl.utils.localTimeAgo($('.js-timeago', 'table.table_#{@hex_path} tbody'));
\ No newline at end of file + gl.utils.localTimeAgo($('.js-timeago', 'table.table_#{@hex_path} tbody')); diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index 6e58e5a0c78..7036b8a5ccc 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -1,4 +1,4 @@ -%li.runner{id: dom_id(runner)} +%li.runner{ id: dom_id(runner) } %h4 = runner_status_icon(runner) %span.monospace diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index b41edeb2c7e..fc338dcf887 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -7,14 +7,14 @@ %p= @service.description .col-lg-9 = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form| - = render 'shared/service_settings', form: form + = render 'shared/service_settings', form: form, subject: @service + .footer-block.row-content-block + = form.submit 'Save changes', class: 'btn btn-save' + + - if @service.valid? && @service.activated? + - unless @service.can_test? + - disabled_class = 'disabled' + - disabled_title = @service.disabled_title - = form.submit 'Save changes', class: 'btn btn-save' - - - if @service.valid? && @service.activated? - - unless @service.can_test? - - disabled_class = 'disabled' - - disabled_title = @service.disabled_title - - = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title - = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel" + = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title + = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/_index.html.haml index 4a33a5bc6f6..964133504e6 100644 --- a/app/views/projects/services/index.html.haml +++ b/app/views/projects/services/_index.html.haml @@ -1,5 +1,3 @@ -- page_title "Services" - .row.prepend-top-default.append-bottom-default .col-lg-3 %h4.prepend-top-0 @@ -28,5 +26,6 @@ %td.hidden-xs = service.description %td.light - = time_ago_in_words service.updated_at - ago + - if service.updated_at.present? + = time_ago_in_words service.updated_at + ago diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml new file mode 100644 index 00000000000..8ca4c51a064 --- /dev/null +++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml @@ -0,0 +1,91 @@ +- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}" + +To setup this service: +%ul.list-unstyled + %li + 1. + = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands' + on your Mattermost installation + %li + 2. + = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command' + in Mattermost with these options: + +%hr + +.help-form + .form-group + = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#display_name') + + .form-group + = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#description') + + .form-group + = label_tag nil, 'Command trigger word', 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 :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#request_url') + + .form-group + = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block POST + + .form-group + = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#response_username') + + .form-group + = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#response_icon') + + .form-group + = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block Yes + + .form-group + = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#autocomplete_hint') + + .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') + +%hr + +%ul.list-unstyled + %li + 3. After adding the slash command, paste the + + %strong token + into the field below diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml new file mode 100644 index 00000000000..c1e576b42fc --- /dev/null +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -0,0 +1,15 @@ +- enabled = Gitlab.config.mattermost.enabled + +.well + This service allows GitLab users to perform common operations on this + project by entering slash commands in Mattermost. + %br + See list of available commands in Mattermost after setting up this service, + by entering + %code /<command_trigger_word> help + + - unless enabled || @service.template? + = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service + +- if enabled && !@service.template? + = render 'projects/services/mattermost_slash_commands/installation_info', subject: @service diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml new file mode 100644 index 00000000000..c929eee3bb9 --- /dev/null +++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml @@ -0,0 +1,7 @@ +.services-installation-info + - unless @service.activated? + .row + .col-sm-9.col-sm-offset-3 + = link_to new_namespace_project_mattermost_path(@project.namespace, @project), class: 'btn btn-lg' do + = custom_icon('mattermost_logo', size: 15) + = 'Add to Mattermost' diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml new file mode 100644 index 00000000000..04b9100acc6 --- /dev/null +++ b/app/views/projects/services/slack_slash_commands/_help.html.haml @@ -0,0 +1,95 @@ +- 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 + project by entering slash commands in Slack. + %br + See list of available commands in Slack after setting up this service, + by entering + %code /<command> help + %br + %br + - 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 + + .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 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 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 :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 :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 + + %ul.list-unstyled + %li + 2. Paste the + %strong Token + into the field below + %li + 3. Select the + %strong Active + checkbox, press + %strong Save changes + and start using GitLab inside Slack! diff --git a/app/views/projects/hooks/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml index ceabe2eab3d..ceabe2eab3d 100644 --- a/app/views/projects/hooks/_project_hook.html.haml +++ b/app/views/projects/settings/integrations/_project_hook.html.haml diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml new file mode 100644 index 00000000000..aa38a889cdd --- /dev/null +++ b/app/views/projects/settings/integrations/show.html.haml @@ -0,0 +1,3 @@ +- page_title 'Integrations' += render 'projects/hooks/index' += render 'projects/services/index' diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml new file mode 100644 index 00000000000..d81ed7bb609 --- /dev/null +++ b/app/views/projects/settings/members/show.html.haml @@ -0,0 +1,6 @@ +- page_title "Members" + += render "projects/project_members/index" +- if can?(current_user, :admin_project, @project) + - if @project.allowed_to_share_with_group? + = render "projects/group_links/index" diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 4de95036eef..80d4081dd7b 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -17,10 +17,10 @@ %ul.nav %li = link_to project_files_path(@project) do - Files (#{repository_size}) + Files (#{storage_counter(@project.statistics.total_repository_size)}) %li = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do - #{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)}) + #{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)}) %li = link_to namespace_project_branches_path(@project.namespace, @project) do #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) @@ -64,19 +64,15 @@ - unless @repository.gitlab_ci_yml %li.missing = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do - Set Up CI - - %li.project-repo-buttons-right - .project-repo-buttons.project-right-buttons - - if current_user - = render 'shared/members/access_request_buttons', source: @project - = render "projects/buttons/koding" - - .btn-group.project-repo-btn-group - = render 'projects/buttons/download', project: @project, ref: @ref - = render 'projects/buttons/dropdown' + Set up CI + - if koding_enabled? && @repository.koding_yml.blank? + %li.missing + = link_to 'Set up Koding', add_koding_stack_path(@project) + - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present? + %li.missing + = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do + Set up auto deploy - = render 'shared/notifications/button', notification_setting: @notification_setting - if @repository.commit .project-last-commit{ class: container_class } = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index 32e1f8a21b0..068a6610350 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -1,13 +1,13 @@ .hidden-xs - - if can?(current_user, :create_project_snippet, @project) - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do - New snippet - - if can?(current_user, :update_project_snippet, @snippet) - = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do - Delete - if can?(current_user, :update_project_snippet, @snippet) - = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do + = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped" do Edit + - if can?(current_user, :update_project_snippet, @snippet) + = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do + Delete + - if can?(current_user, :create_project_snippet, @project) + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do + New snippet - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index e77e1b026f6..84e05cd6d88 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,11 +1,19 @@ - page_title "Snippets" -.sub-header-block - - if can?(current_user, :create_project_snippet, @project) - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do - New snippet +- if current_user + .top-area + - include_private = @project.team.member?(current_user) || current_user.admin? + = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private } + + .nav-controls.hidden-xs + - if can?(current_user, :create_project_snippet, @project) + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New snippet" do + New snippet - .oneline - Share code pastes with others out of git repository +- if can?(current_user, :create_project_snippet, @project) + .visible-xs + + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-block", title: "New snippet" do + New snippet = render 'snippets/snippets' diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 9503dbded13..485b23815bc 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -8,10 +8,11 @@ = blob_icon 0, @snippet.file_name = @snippet.file_name .file-actions - = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") + = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm") = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" = render 'shared/snippets/blob' - = render 'award_emoji/awards_block', awardable: @snippet, inline: true + .row-content-block.top-block.content-component-block + = render 'award_emoji/awards_block', awardable: @snippet, inline: true - %div#notes= render "projects/notes/notes_with_form" + #notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml new file mode 100644 index 00000000000..4ee30b023ac --- /dev/null +++ b/app/views/projects/stage/_graph.html.haml @@ -0,0 +1,19 @@ +- stage = local_assigns.fetch(:stage) +- statuses = stage.statuses.latest +- status_groups = statuses.sort_by(&:sortable_name).group_by(&:group_name) +%li.stage-column + .stage-name + %a{ name: stage.name } + = stage.name.titleize + .builds-container + %ul + - status_groups.each do |group_name, grouped_statuses| + - if grouped_statuses.one? + - status = grouped_statuses.first + %li.build{ 'id' => "ci-badge-#{group_name}" } + .curve + = render 'ci/status/graph_badge', subject: status + - else + %li.build{ 'id' => "ci-badge-#{group_name}" } + .curve + = render 'projects/stage/in_stage_group', name: group_name, subject: grouped_statuses diff --git a/app/views/projects/stage/_in_stage_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml new file mode 100644 index 00000000000..9c5eb501174 --- /dev/null +++ b/app/views/projects/stage/_in_stage_group.html.haml @@ -0,0 +1,14 @@ +- group_status = CommitStatus.where(id: subject).status +%button.dropdown-menu-toggle.build-content.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } } + %span{ class: "ci-status-icon ci-status-icon-#{group_status}" } + = ci_icon_for_status(group_status) + %span.ci-status-text + = name + %span.dropdown-counter-badge= subject.size + +%ul.dropdown-menu.big-pipeline-graph-dropdown-menu.js-grouped-pipeline-dropdown + .arrow + .scrollable-menu + - subject.each do |status| + %li + = render 'ci/status/dropdown_graph_badge', subject: status diff --git a/app/views/projects/stage/_stage.html.haml b/app/views/projects/stage/_stage.html.haml new file mode 100644 index 00000000000..28e1c060875 --- /dev/null +++ b/app/views/projects/stage/_stage.html.haml @@ -0,0 +1,13 @@ +%tr + %th{ colspan: 10 } + %strong + %a{ name: stage.name } + %span{ class: "ci-status-link ci-status-icon-#{stage.status}" } + = ci_icon_for_status(stage.status) + + = stage.name.titleize += render stage.statuses.latest_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, allow_retry: true += render stage.statuses.retried_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, retried: true +%tr + %td{ colspan: 10 } + diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index c42641afea0..8ef069b9e05 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -1,7 +1,7 @@ - commit = @repository.commit(tag.dereferenced_target) - release = @releases.find { |release| release.tag == tag.name } -%li - %div +%li.flex-row + .row-main-content.str-truncated = link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do %span.item-title = icon('tag') @@ -10,24 +10,25 @@ = strip_gpg_signature(tag.message) - .controls - = render 'projects/buttons/download', project: @project, ref: tag.name + - if commit + .block-truncated + = render 'projects/branches/commit', commit: commit, project: @project + - else + %p + Cant find HEAD commit for this tag + - if release && release.description.present? + .description.prepend-top-default + .wiki + = preserve do + = markdown_field(release, :description) - - if can?(current_user, :push_code, @project) - = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do - = icon("pencil") + .row-fixed-content.controls + = render 'projects/buttons/download', project: @project, ref: tag.name - - if can?(current_user, :admin_project, @project) - = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do - = icon("trash-o") + - if can?(current_user, :push_code, @project) + = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do + = icon("pencil") - - if commit - = render 'projects/branches/commit', commit: commit, project: @project - - else - %p - Cant find HEAD commit for this tag - - if release && release.description.present? - .description.prepend-top-default - .wiki - = preserve do - = markdown_field(release, :description) + - if can?(current_user, :admin_project, @project) + = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do + = icon("trash-o") diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 7a0d9dcc94f..e2f132f7742 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -2,23 +2,24 @@ - page_title "Tags" = render "projects/commits/head" -%div{ class: container_class } - .top-area - .nav-text +.flex-list{ class: container_class } + .top-area.flex-row + .nav-text.row-main-content Tags give the ability to mark specific points in history as being important - .nav-controls + .nav-controls.row-fixed-content = form_tag(filter_tags_path, method: :get) do = search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false } - .dropdown.inline - %button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} } + + .dropdown + %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown'} } %span.light - = @sort.humanize - = icon('caret-down') + = projects_sort_options_hash[@sort] + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li - = link_to filter_tags_path(sort: nil) do - Name + = link_to filter_tags_path(sort: sort_value_name) do + = sort_title_name = link_to filter_tags_path(sort: sort_value_recently_updated) do = sort_title_recently_updated = link_to filter_tags_path(sort: sort_value_oldest_updated) do @@ -29,7 +30,7 @@ .tags - if @tags.any? - %ul.content-list + %ul.flex-list.content-list = render partial: 'tag', collection: @tags = paginate @tags, theme: 'gitlab' diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 3a097750d6e..160d4c7a223 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -2,7 +2,7 @@ - if @error .alert.alert-danger - %button{ type: "button", class: "close", "data-dismiss" => "alert"} × + %button.close{ type: "button", "data-dismiss" => "alert" } × = @error %h3.page-title @@ -23,7 +23,7 @@ = label_tag :message, nil, class: 'control-label' .col-sm-10 = text_area_tag :message, nil, required: false, tabindex: 3, class: 'form-control', rows: 5 - .help-block Optionally, enter a message to create an annotated tag. + .help-block Optionally, add a message to the tag. %hr .form-group = label_tag :release_description, 'Release notes', class: 'control-label' diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 155af755759..fad3c5c2173 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -3,29 +3,32 @@ = render "projects/commits/head" %div{ class: container_class } - .sub-header-block - .pull-right.tag-buttons - - if can?(current_user, :push_code, @project) - = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Edit release notes' do - = icon("pencil") - = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Browse files' do - = icon('files-o') - = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Browse commits' do - = icon('history') - = render 'projects/buttons/download', project: @project, ref: @tag.name - - if can?(current_user, :admin_project, @project) - .pull-right - = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do - %i.fa.fa-trash-o - .tag-info.append-bottom-10 + .top-area.multi-line + .nav-text .title %span.item-title= @tag.name - if @commit = render 'projects/branches/commit', commit: @commit, project: @project - else Cant find HEAD commit for this tag + + .nav-controls.controls-flex + - if can?(current_user, :push_code, @project) + = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Edit release notes' do + = icon("pencil") + = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Browse files' do + = icon('files-o') + = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Browse commits' do + = icon('history') + .btn-container.controls-item + = render 'projects/buttons/download', project: @project, ref: @tag.name + - if can?(current_user, :admin_project, @project) + .btn-container.controls-item-full + = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do + %i.fa.fa-trash-o + - if @tag.message.present? - %pre.body + %pre.wrap = strip_gpg_signature(@tag.message) .append-bottom-default.prepend-top-default diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml index ee417b58cbf..425b460eb09 100644 --- a/app/views/projects/tree/_blob_item.html.haml +++ b/app/views/projects/tree/_blob_item.html.haml @@ -6,4 +6,4 @@ %span.str-truncated= file_name %td.hidden-xs.tree-commit %td.tree-time-ago.cgray.text-right - = render 'projects/tree/spinner'
\ No newline at end of file + = render 'projects/tree/spinner' diff --git a/app/views/projects/tree/_submodule_item.html.haml b/app/views/projects/tree/_submodule_item.html.haml index 2b5f671c09e..04d52361db0 100644 --- a/app/views/projects/tree/_submodule_item.html.haml +++ b/app/views/projects/tree/_submodule_item.html.haml @@ -1,4 +1,4 @@ -%tr{ class: "tree-item" } +%tr.tree-item %td.tree-item-file-name %i.fa.fa-archive.fa-fw = submodule_link(submodule_item, @ref) diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index 21e378b8735..2c08221565b 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -1,18 +1,15 @@ -%div.tree-content-holder +.tree-content-holder .table-holder - %table.table#tree-slider{class: "table_#{@hex_path} tree-table" } + %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" } %thead %tr %th Name %th.hidden-xs - .pull-left Last Commit + .pull-left Last commit .last-commit.hidden-sm.pull-left - - %i.fa.fa-angle-right - %small.light + = 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 %small.commit-history-link-spacer | diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 1c5f8b3928b..259207a6dfd 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -15,11 +15,11 @@ - if current_user %li - if !on_top_of_branch? - %span.btn.add-to-tree.disabled.has-tooltip{title: "You can only add files when you are on a branch", data: { container: 'body' }} + %span.btn.add-to-tree.disabled.has-tooltip{ title: "You can only add files when you are on a branch", data: { container: 'body' } } = icon('plus') - else %span.dropdown - %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"} + %a.dropdown-toggle.btn.add-to-tree{ href: '#', "data-toggle" => "dropdown" } = icon('plus') %ul.dropdown-menu - if can_edit_tree? @@ -28,11 +28,11 @@ = icon('pencil fw') New file %li - = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do + = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do = icon('file fw') Upload file %li - = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do + = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do = icon('folder fw') New directory - elsif can?(current_user, :fork_project, @project) diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml index 1ccef6d52ab..15c9536133c 100644 --- a/app/views/projects/tree/_tree_item.html.haml +++ b/app/views/projects/tree/_tree_item.html.haml @@ -6,4 +6,4 @@ %span.str-truncated= path %td.hidden-xs.tree-commit %td.tree-time-ago.text-right - = render 'projects/tree/spinner'
\ No newline at end of file + = render 'projects/tree/spinner' diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml index f6e0b0a7c8a..6e5dd1b196d 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -76,6 +76,16 @@ script: - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}" %h5.prepend-top-default + Use webhook + + %p.light + Add the following webhook to another project for Push and Tag push events. + The project will be rebuilt at the corresponding event. + + %pre + :plain + #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN + %h5.prepend-top-default Pass build variables %p.light @@ -83,10 +93,18 @@ %code variables[VARIABLE]=VALUE to an API request. Variable values can be used to distinguish between triggered builds and normal builds. - %pre.append-bottom-0 + With cURL: + + %pre :plain curl -X POST \ -F token=TOKEN \ -F "ref=REF_NAME" \ -F "variables[RUN_NIGHTLY_BUILD]=true" \ #{builds_trigger_url(@project.id)} + %p.light + With webhook: + + %pre.append-bottom-0 + :plain + #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN&variables[RUN_NIGHTLY_BUILD]=true diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml index 07cee86ba4c..c7cebf45160 100644 --- a/app/views/projects/variables/_table.html.haml +++ b/app/views/projects/variables/_table.html.haml @@ -12,8 +12,8 @@ - @project.variables.order_key_asc.each do |variable| - if variable.id? %tr - %td= variable.key - %td= variable.value + %td.variable-key= variable.key + %td.variable-value{ "data-value" => variable.value }****** %td = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do %span.sr-only diff --git a/app/views/projects/variables/index.html.haml b/app/views/projects/variables/index.html.haml index 09bb54600af..cf7ae0b489f 100644 --- a/app/views/projects/variables/index.html.haml +++ b/app/views/projects/variables/index.html.haml @@ -15,3 +15,4 @@ No variables found, add one with the form above. - else = render "table" + %button.btn.btn-info.js-btn-toggle-reveal-values{ "data-status" => 'hidden' } Reveal Values diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 4e41a15d9f4..c52527332bc 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -34,9 +34,6 @@ - if @page && @page.persisted? = f.submit 'Save changes', class: "btn-save btn" .pull-right - - if can?(current_user, :admin_wiki, @project) - = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger btn-grouped" do - Delete = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-cancel btn-grouped" - else = f.submit 'Create page', class: "btn-create btn" diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml deleted file mode 100644 index 09c4411d67e..00000000000 --- a/app/views/projects/wikis/_nav.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -= content_for :sub_nav do - .scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: (container_class) } - = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do - = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) - - = nav_link(path: 'wikis#pages') do - = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project) - - = nav_link(path: 'wikis#git_access') do - = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do - Git Access - - = render 'projects/wikis/new' diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index c32cb122c26..c74f53b4c39 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -1,11 +1,11 @@ - @no_container = true %div{ class: container_class } - %div#modal-new-wiki.modal + #modal-new-wiki.modal .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title New Wiki Page .modal-body %form.new-wiki-page diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml new file mode 100644 index 00000000000..cad9c15a49e --- /dev/null +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -0,0 +1,23 @@ +%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar + .block.wiki-sidebar-header.append-bottom-default + %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" } + = icon('angle-double-right') + + - git_access_url = namespace_project_wikis_git_access_path(@project.namespace, @project) + = link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '' do + = succeed ' ' do + = icon('cloud-download') + Clone repository + + .blocks-container + .block.block-first + %ul.wiki-pages + - @sidebar_wiki_pages.each do |wiki_page| + %li{ class: params[:id] == wiki_page.slug ? 'active' : '' } + = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do + = wiki_page.title.capitalize + .block + = link_to namespace_project_wikis_pages_path(@project.namespace, @project), class: 'btn btn-block' do + More Pages + += render 'projects/wikis/new' diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 679d6018bef..8cf018da1b7 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,23 +1,35 @@ - @no_container = true - page_title "Edit", @page.title.capitalize, "Wiki" -= render 'nav' %div{ class: container_class } - .top-area + .wiki-page-header.has-sidebar-toggle + %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + = icon('angle-double-left') + .nav-text - %strong + %h2.wiki-page-title - if @page.persisted? = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) - else = @page.title.capitalize - %span.light - · - Edit Page + %span.light + · + - if @page.persisted? + Edit Page + - else + Create Page .nav-controls - - if !(@page && @page.persisted?) - - if can?(current_user, :create_wiki, @project) - = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do - New Page + - if can?(current_user, :create_wiki, @project) + = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do + New Page + - if @page.persisted? + = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do + Page History + - if can?(current_user, :admin_wiki, @project) + = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger" do + Delete = render 'form' + += render 'sidebar' diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index b8811a28dd6..e25d6a48573 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -1,34 +1,36 @@ - @no_container = true - page_title "Git Access", "Wiki" -= render 'nav' %div{ class: container_class } - .sub-header-block - %span.oneline - Git access for + .wiki-page-header.has-sidebar-toggle + %button.btn.btn-default.visible-xs.visible-sm.pull-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + = icon('angle-double-left') + + .git-access-header + Clone repository %strong= @project_wiki.path_with_namespace - .pull-right - = render "shared/clone_panel", project: @project_wiki + = render "shared/clone_panel", project: @project_wiki + + .wiki-git-access + %h3 Install Gollum + %pre.dark + :preserve + gem install gollum - .prepend-top-default - %fieldset - %legend Install Gollum: - %pre.dark - :preserve - gem install gollum + %h3 Clone your wiki + %pre.dark + :preserve + git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')} + cd #{h @project_wiki.path} - %legend Clone Your Wiki: - %pre.dark - :preserve - git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')} - cd #{h @project_wiki.path} + %h3 Start Gollum and edit locally + %pre.dark + :preserve + gollum + == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin + >> Thin web server (v1.5.0 codename Knife) + >> Maximum connections set to 1024 + >> Listening on 0.0.0.0:4567, CTRL+C to stop - %legend Start Gollum And Edit Locally: - %pre.dark - :preserve - gollum - == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin - >> Thin web server (v1.5.0 codename Knife) - >> Maximum connections set to 1024 - >> Listening on 0.0.0.0:4567, CTRL+C to stop += render 'sidebar' diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index 4c0b14e2c42..dd7213622c1 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,13 +1,16 @@ - page_title "History", @page.title.capitalize, "Wiki" -= render 'nav' + %div{ class: container_class } - .top-area + .wiki-page-header.has-sidebar-toggle + %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + = icon('angle-double-left') + .nav-text - %strong + %h2.wiki-page-title = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) - %span.light - · - History + %span.light + · + History .table-holder %table.table @@ -35,3 +38,5 @@ %td %strong = @page.page.wiki.page(@page.page.name, commit.id).try(:format) + += render 'sidebar' diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index 9c10acd4cb6..e1eaffc6884 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -1,9 +1,18 @@ - @no_container = true - page_title "Pages", "Wiki" -= render 'nav' - %div{ class: container_class } + .wiki-page-header + + .nav-text + %h2.wiki-page-title + Wiki Pages + + .nav-controls + = link_to namespace_project_wikis_git_access_path(@project.namespace, @project), class: 'btn' do + = icon('cloud-download') + Clone repository + %ul.content-list - @wiki_pages.each do |wiki_page| %li diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 5cebb538cf5..1b6dceee241 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -1,15 +1,19 @@ - @no_container = true - page_title @page.title.capitalize, "Wiki" -= render 'nav' %div{ class: container_class } - .top-area + .wiki-page-header.has-sidebar-toggle + %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + = icon('angle-double-left') + .nav-text - %strong= @page.title.capitalize + %h2.wiki-page-title= @page.title.capitalize %span.wiki-last-edit-by - · - last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} + Last edited by + %strong + #{@page.commit.author.name} + #{time_ago_with_tooltip(@page.commit.authored_date)} .nav-controls = render 'main_links' @@ -19,8 +23,9 @@ This is an old version of this page. You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}. - .wiki-holder.prepend-top-default.append-bottom-default .wiki = preserve do = render_wiki_content(@page) + += render 'sidebar' diff --git a/app/views/repository_check_mailer/notify.html.haml b/app/views/repository_check_mailer/notify.html.haml index a585147ddd1..94e5a5d9709 100644 --- a/app/views/repository_check_mailer/notify.html.haml +++ b/app/views/repository_check_mailer/notify.html.haml @@ -2,7 +2,7 @@ #{@message}. %p - = link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repository_check_failed: 1) + = link_to "See the affected projects in the GitLab admin panel", admin_projects_url(last_repository_check_failed: 1) %p You are receiving this message because you are a GitLab administrator for #{Gitlab.config.gitlab.url}. diff --git a/app/views/repository_check_mailer/notify.text.haml b/app/views/repository_check_mailer/notify.text.haml index 93db151329e..0902c50d052 100644 --- a/app/views/repository_check_mailer/notify.text.haml +++ b/app/views/repository_check_mailer/notify.text.haml @@ -1,6 +1,6 @@ #{@message}. \ -View details: #{admin_namespaces_projects_url(last_repository_check_failed: 1)} +View details: #{admin_projects_url(last_repository_check_failed: 1)} You are receiving this message because you are a GitLab administrator for #{Gitlab.config.gitlab.url}. diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index 2c378231237..8cbecb725b5 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -1,70 +1,70 @@ %ul.nav-links.search-filter - if @project - %li{class: ("active" if @scope == 'blobs')} + %li{ class: ("active" if @scope == 'blobs') } = link_to search_filter_path(scope: 'blobs') do Code %span.badge = @search_results.blobs_count - %li{class: ("active" if @scope == 'issues')} + %li{ class: ("active" if @scope == 'issues') } = link_to search_filter_path(scope: 'issues') do Issues %span.badge = @search_results.issues_count - %li{class: ("active" if @scope == 'merge_requests')} + %li{ class: ("active" if @scope == 'merge_requests') } = link_to search_filter_path(scope: 'merge_requests') do Merge requests %span.badge = @search_results.merge_requests_count - %li{class: ("active" if @scope == 'milestones')} + %li{ class: ("active" if @scope == 'milestones') } = link_to search_filter_path(scope: 'milestones') do Milestones %span.badge = @search_results.milestones_count - %li{class: ("active" if @scope == 'notes')} + %li{ class: ("active" if @scope == 'notes') } = link_to search_filter_path(scope: 'notes') do Comments %span.badge = @search_results.notes_count - %li{class: ("active" if @scope == 'wiki_blobs')} + %li{ class: ("active" if @scope == 'wiki_blobs') } = link_to search_filter_path(scope: 'wiki_blobs') do Wiki %span.badge = @search_results.wiki_blobs_count - %li{class: ("active" if @scope == 'commits')} + %li{ class: ("active" if @scope == 'commits') } = link_to search_filter_path(scope: 'commits') do Commits %span.badge = @search_results.commits_count - elsif @show_snippets - %li{class: ("active" if @scope == 'snippet_blobs')} + %li{ class: ("active" if @scope == 'snippet_blobs') } = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do Snippet Contents %span.badge = @search_results.snippet_blobs_count - %li{class: ("active" if @scope == 'snippet_titles')} + %li{ class: ("active" if @scope == 'snippet_titles') } = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do Titles and Filenames %span.badge = @search_results.snippet_titles_count - else - %li{class: ("active" if @scope == 'projects')} + %li{ class: ("active" if @scope == 'projects') } = link_to search_filter_path(scope: 'projects') do Projects %span.badge = @search_results.projects_count - %li{class: ("active" if @scope == 'issues')} + %li{ class: ("active" if @scope == 'issues') } = link_to search_filter_path(scope: 'issues') do Issues %span.badge = @search_results.issues_count - %li{class: ("active" if @scope == 'merge_requests')} + %li{ class: ("active" if @scope == 'merge_requests') } = link_to search_filter_path(scope: 'merge_requests') do Merge requests %span.badge = @search_results.merge_requests_count - %li{class: ("active" if @scope == 'milestones')} + %li{ class: ("active" if @scope == 'milestones') } = link_to search_filter_path(scope: 'milestones') do Milestones %span.badge diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml index ef1c0296d49..938be20c7cf 100644 --- a/app/views/search/_filter.html.haml +++ b/app/views/search/_filter.html.haml @@ -3,7 +3,7 @@ - if params[:project_id].present? = hidden_field_tag :project_id, params[:project_id] .dropdown - %button.dropdown-menu-toggle.btn.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Group:" } } + %button.dropdown-menu-toggle.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Group:" } } %span.dropdown-toggle-text Group: - if @group.present? @@ -18,7 +18,7 @@ = dropdown_loading .dropdown.project-filter - %button.dropdown-menu-toggle.btn.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Project:" } } + %button.dropdown-menu-toggle.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Project:" } } %span.dropdown-toggle-text Project: - if @project.present? diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml index 6f0a0ea36ec..9e8adc82583 100644 --- a/app/views/search/results/_blob.html.haml +++ b/app/views/search/results/_blob.html.haml @@ -1,11 +1,13 @@ -- blob = parse_search_result(blob) +- file_name, blob = blob .blob-result .file-holder .file-title - - blob_link = namespace_project_blob_path(@project.namespace, @project, tree_join(blob.ref, blob.filename)) + - ref = @search_results.repository_ref + - blob_link = namespace_project_blob_path(@project.namespace, @project, tree_join(ref, file_name)) = link_to blob_link do %i.fa.fa-file %strong - = blob.filename - .file-content.code.term - = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link + = file_name + - if blob + .file-content.code.term + = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index c414acb6a11..027d42396b4 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -14,7 +14,7 @@ = link_to snippet_title.project.name_with_namespace, namespace_project_path(snippet_title.project.namespace, snippet_title.project) .snippet-info - = "##{snippet_title.id}" + = snippet_title.to_reference %span by = link_to user_snippets_path(snippet_title.author) do diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml index 000532b1c9a..94295970acf 100644 --- a/app/views/shared/_choose_group_avatar_button.html.haml +++ b/app/views/shared/_choose_group_avatar_button.html.haml @@ -1,4 +1,4 @@ -%a.choose-btn.btn.btn-sm.js-choose-group-avatar-button +%button.choose-btn.btn.btn-sm.js-choose-group-avatar-button{ type: 'button' } %i.fa.fa-paperclip %span Choose File ... diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 3b82d8e686f..03684389742 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -2,12 +2,12 @@ .git-clone-holder.input-group .input-group-btn - -if allowed_protocols_present? + - if allowed_protocols_present? .clone-dropdown-btn.btn.btn-static %span = enabled_project_button(project, enabled_protocol) - else - %a#clone-dropdown.clone-dropdown-btn.btn{href: '#', data: { toggle: 'dropdown' }} + %a#clone-dropdown.clone-dropdown-btn.btn{ href: '#', data: { toggle: 'dropdown' } } %span = default_clone_protocol.upcase = icon('caret-down') @@ -19,7 +19,7 @@ = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true .input-group-btn - = clipboard_button(clipboard_target: '#project_clone') + = clipboard_button(clipboard_target: '#project_clone', title: "Copy URL to clipboard") :javascript $('ul.clone-options-dropdown a').on('click',function(e){ diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index 0a38327baa2..c196bc06b17 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -1,5 +1,6 @@ .form-group.commit_message-group - nonce = SecureRandom.hex + - descriptions = local_assigns.slice(:message_with_description, :message_without_description) = label_tag "commit_message-#{nonce}", class: 'control-label' do Commit message .col-sm-10 @@ -8,9 +9,17 @@ = text_area_tag 'commit_message', (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]), class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder], + data: descriptions, required: true, rows: (local_assigns[:rows] || 3), id: "commit_message-#{nonce}" - if local_assigns[:hint] %p.hint Try to keep the first line under 52 characters and the others under 72. + - if descriptions.present? + %p.hint.js-with-description-hint + = link_to "#", class: "js-with-description-link" do + Include description in commit message + %p.hint.js-without-description-hint.hide + = link_to "#", class: "js-without-description-link" do + Don't include description in commit message diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml index b0fc60573f7..e7cb93b17a7 100644 --- a/app/views/shared/_confirm_modal.html.haml +++ b/app/views/shared/_confirm_modal.html.haml @@ -1,8 +1,8 @@ -#modal-confirm-danger.modal{tabindex: -1} +#modal-confirm-danger.modal{ tabindex: -1 } .modal-dialog .modal-content .modal-header - %a.close{href: "#", "data-dismiss" => "modal"} × + %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title Confirmation required diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index c367ae336db..e50ab5fea09 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -5,5 +5,7 @@ - if event_filter_visible(:merge_requests) = event_filter_link EventFilter.merged, 'Merge events' - if event_filter_visible(:issues) + = event_filter_link EventFilter.issue, 'Issue events' + - if comments_visible? = event_filter_link EventFilter.comments, 'Comments' = event_filter_link EventFilter.team, 'Team' diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index e26693bf5b9..8d64cb5d698 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -9,8 +9,8 @@ - offset = defined?(first_line_number) ? first_line_number : 1 - i = index + offset -# We're not using `link_to` because it is too slow once we get to thousands of lines. - %a.diff-line-num{href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i} + %a.diff-line-num{ href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i } = link_icon = i - .blob-content{data: {blob_id: blob.id}} + .blob-content{ data: { blob_id: blob.id } } = highlight(blob.path, blob.data, repository: repository, plain: blob.no_highlighting?) diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index ba25e09d638..0bc851b4256 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -13,8 +13,9 @@ .input-group-addon = root_url = f.text_field :path, placeholder: 'open-source', class: 'form-control', - autofocus: local_assigns[:autofocus] || false, pattern: "[a-zA-Z0-9-_]+", - required: true, title: 'Please choose a group name with no special characters.' + autofocus: local_assigns[:autofocus] || false, required: true, + pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, + title: 'Please choose a group name with no special characters.' - if @group.persisted? .alert.alert-warning.prepend-top-10 diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml index a5df502d7b5..26b349e8a62 100644 --- a/app/views/shared/_issues.html.haml +++ b/app/views/shared/_issues.html.haml @@ -1,4 +1,4 @@ -- if @issues.reorder(nil).any? +- if @issues.to_a.any? - @issues.group_by(&:project).each do |group| .panel.panel-default.panel-small - project = group[0] @@ -13,4 +13,4 @@ = render 'projects/issues/issue', issue: issue = paginate @issues, theme: "gitlab" - else - .nothing-here-block No issues to show + = render 'shared/empty_states/issues' diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index 6ccdef0df46..f11f4471a9d 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -1,9 +1,10 @@ - label_css_id = dom_id(label) - open_issues_count = label.open_issues_count(current_user) - open_merge_requests_count = label.open_merge_requests_count(current_user) +- status = label_subscription_status(label, @project).inquiry if current_user - subject = local_assigns[:subject] -%li{id: label_css_id, data: { id: label.id } } +%li{ id: label_css_id, data: { id: label.id } } = render "shared/label_row", label: label .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown @@ -18,10 +19,19 @@ %li = link_to_label(label, subject: subject) do = pluralize open_issues_count, 'open issue' - - if current_user - %li.label-subscription{ data: toggle_subscription_data(label) } - %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } } - %span= label_subscription_toggle_button_text(label) + - if current_user && defined?(@project) + %li.label-subscription + - if label.is_a?(ProjectLabel) + %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', data: { status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } + %span= label_subscription_toggle_button_text(label, @project) + - else + %a.js-unsubscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' if status.unsubscribed?), data: { url: group_label_unsubscribe_path(label, @project) } } + %span Unsubscribe + %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } + %span Subscribe at project level + %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } } + %span Subscribe at group level + - if can?(current_user, :admin_label, label) %li = link_to 'Edit', edit_label_path(label) @@ -34,12 +44,27 @@ = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action') do = pluralize open_issues_count, 'open issue' - - if current_user - .label-subscription.inline{ data: toggle_subscription_data(label) } - %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } } - %span.sr-only= label_subscription_toggle_button_text(label) - = icon('eye', class: 'label-subscribe-button-icon', disabled: label.is_a?(GroupLabel)) - = icon('spinner spin', class: 'label-subscribe-button-loading') + - if current_user && defined?(@project) + .label-subscription.inline + - if label.is_a?(ProjectLabel) + %button.js-subscribe-button.label-subscribe-button.btn.btn-default.btn-action{ type: 'button', title: label_subscription_toggle_button_text(label, @project), data: { toggle: 'tooltip', status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } + %span= label_subscription_toggle_button_text(label, @project) + = icon('spinner spin', class: 'label-subscribe-button-loading') + - else + %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default.btn-action{ type: 'button', class: ('hidden' if status.unsubscribed?), title: 'Unsubscribe', data: { toggle: 'tooltip', url: group_label_unsubscribe_path(label, @project) } } + %span Unsubscribe + = icon('spinner spin', class: 'label-subscribe-button-loading') + + .dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) } + %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } + %span Subscribe + = icon('chevron-down') + %ul.dropdown-menu + %li + %a.js-subscribe-button{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } + Project level + %a.js-subscribe-button{ data: { url: toggle_subscription_group_label_path(label.group, label) } } + Group level - if can?(current_user, :admin_label, label) = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do @@ -49,6 +74,10 @@ %span.sr-only Delete = icon('trash-o') - - if current_user && label.is_a?(ProjectLabel) - :javascript - new Subscription('##{dom_id(label)} .label-subscription'); + - if current_user && defined?(@project) + - if label.is_a?(ProjectLabel) + :javascript + new gl.ProjectLabelSubscription('##{dom_id(label)} .label-subscription'); + - else + :javascript + new gl.GroupLabelSubscription('##{dom_id(label)} .label-subscription'); diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml index ca3178395c1..2f3605b4d27 100644 --- a/app/views/shared/_merge_requests.html.haml +++ b/app/views/shared/_merge_requests.html.haml @@ -1,4 +1,4 @@ -- if @merge_requests.reorder(nil).any? +- if @merge_requests.to_a.any? - @merge_requests.group_by(&:target_project).each do |group| .panel.panel-default.panel-small - project = group[0] diff --git a/app/views/shared/_milestone_expired.html.haml b/app/views/shared/_milestone_expired.html.haml index b8eef15fbec..5e9007aaaac 100644 --- a/app/views/shared/_milestone_expired.html.haml +++ b/app/views/shared/_milestone_expired.html.haml @@ -1,5 +1,7 @@ - if milestone.expired? and not milestone.closed? %span.cred (Expired) -- if milestone.expires_at +- if milestone.upcoming? + %span.clgray (Upcoming) +- if milestone.due_date || milestone.start_date %span - = milestone.expires_at + = milestone_date_range(milestone) diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml index 73d288e2236..39294fe1a09 100644 --- a/app/views/shared/_milestones_filter.html.haml +++ b/app/views/shared/_milestones_filter.html.haml @@ -2,17 +2,17 @@ - counts = milestone_counts(@project.milestones) %ul.nav-links - %li{class: milestone_class_for_state(params[:state], 'opened', true)} + %li{ class: milestone_class_for_state(params[:state], 'opened', true) }> = link_to milestones_filter_path(state: 'opened') do Open - if @project %span.badge #{counts[:opened]} - %li{class: milestone_class_for_state(params[:state], 'closed')} + %li{ class: milestone_class_for_state(params[:state], 'closed') }> = link_to milestones_filter_path(state: 'closed') do Closed - if @project %span.badge #{counts[:closed]} - %li{class: milestone_class_for_state(params[:state], 'all')} + %li{ class: milestone_class_for_state(params[:state], 'all') }> = link_to milestones_filter_path(state: 'all') do All - if @project diff --git a/app/views/shared/_nav_scroll.html.haml b/app/views/shared/_nav_scroll.html.haml index 4e3b1b3a571..61646f150c1 100644 --- a/app/views/shared/_nav_scroll.html.haml +++ b/app/views/shared/_nav_scroll.html.haml @@ -1,4 +1,4 @@ .fade-left = icon('angle-left') .fade-right - = icon('angle-right')
\ No newline at end of file + = icon('angle-right') diff --git a/app/views/shared/_no_password.html.haml b/app/views/shared/_no_password.html.haml index a43bf33751a..ed6fc76c61e 100644 --- a/app/views/shared/_no_password.html.haml +++ b/app/views/shared/_no_password.html.haml @@ -1,8 +1,8 @@ - if cookies[:hide_no_password_message].blank? && !current_user.hide_no_password && current_user.require_password? - .no-password-message.alert.alert-warning.hidden-xs + .no-password-message.alert.alert-warning You won't be able to pull or push project code via #{gitlab_config.protocol.upcase} until you #{link_to 'set a password', edit_profile_password_path} on your account - .pull-right + .alert-link-group = link_to "Don't show again", profile_path(user: {hide_no_password: true}), method: :put | = link_to 'Remind later', '#', class: 'hide-no-password-message' diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml index bb5fff2d3bb..d663fa13d10 100644 --- a/app/views/shared/_no_ssh.html.haml +++ b/app/views/shared/_no_ssh.html.haml @@ -1,8 +1,8 @@ - if cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key? - .no-ssh-key-message.alert.alert-warning.hidden-xs + .no-ssh-key-message.alert.alert-warning You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', profile_keys_path, class: 'alert-link'} to your profile - .pull-right + .alert-link-group = link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'alert-link' | = link_to 'Remind later', '#', class: 'hide-no-ssh-message alert-link' diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index 5254d265918..9c5053dace5 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -1,46 +1,50 @@ = form_errors(@service) -- if @service.help.present? +- if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true) + = render "projects/services/#{@service.to_param}/help", subject: subject +- elsif @service.help.present? .well = preserve do = markdown @service.help -.form-group - = form.label :active, "Active", class: "control-label" - .col-sm-10 - = form.check_box :active - -.form-group - = form.label :url, "Trigger", class: 'control-label' - - .col-sm-10 - - @service.supported_events.each do |event| - %div - = form.check_box service_event_field_name(event), class: 'pull-left' - .prepend-left-20 - = form.label service_event_field_name(event), class: 'list-label' do - %strong - = event.humanize - - - field = @service.event_field(event) - - - if field - %p - = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] - - %p.light - = service_event_description(event) - -- @service.global_fields.each do |field| - - type = field[:type] - - - if type == 'fieldset' - - fields = field[:fields] - - legend = field[:legend] - - %fieldset - %legend= legend - - fields.each do |subfield| - = render 'shared/field', form: form, field: subfield - - else - = render 'shared/field', form: form, field: field +.service-settings + .form-group + = form.label :active, "Active", class: "control-label" + .col-sm-10 + = form.check_box :active + + - if @service.supported_events.present? + .form-group + = form.label :url, "Trigger", class: 'control-label' + + .col-sm-10 + - @service.supported_events.each do |event| + %div + = form.check_box service_event_field_name(event), class: 'pull-left' + .prepend-left-20 + = form.label service_event_field_name(event), class: 'list-label' do + %strong + = event.humanize + + - field = @service.event_field(event) + + - if field + %p + = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] + + %p.light + = service_event_description(event) + + - @service.global_fields.each do |field| + - type = field[:type] + + - if type == 'fieldset' + - fields = field[:fields] + - legend = field[:legend] + + %fieldset + %legend= legend + - fields.each do |subfield| + = render 'shared/field', form: form, field: subfield + - else + = render 'shared/field', form: form, field: field diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index 68e05cb72e1..0ce0d759e86 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -1,11 +1,11 @@ .dropdown.inline.prepend-left-10 - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle{ type: 'button', data: {toggle: 'dropdown' } } %span.light - if @sort.present? = sort_options_hash[@sort] - else = sort_title_recently_created - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort %li = link_to page_filter_path(sort: sort_value_priority, label: true) do diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml index 60353aee7f1..b6047ece592 100644 --- a/app/views/shared/builds/_tabs.html.haml +++ b/app/views/shared/builds/_tabs.html.haml @@ -1,23 +1,23 @@ %ul.nav-links - %li{ class: ('active' if scope.nil?) } + %li{ class: ('active' if scope.nil?) }> = link_to build_path_proc.call(nil) do All %span.badge.js-totalbuilds-count = number_with_delimiter(all_builds.count(:id)) - %li{ class: ('active' if scope == 'pending') } + %li{ class: ('active' if scope == 'pending') }> = link_to build_path_proc.call('pending') do Pending %span.badge = number_with_delimiter(all_builds.pending.count(:id)) - %li{ class: ('active' if scope == 'running') } + %li{ class: ('active' if scope == 'running') }> = link_to build_path_proc.call('running') do Running %span.badge = number_with_delimiter(all_builds.running.count(:id)) - %li{ class: ('active' if scope == 'finished') } + %li{ class: ('active' if scope == 'finished') }> = link_to build_path_proc.call('finished') do Finished %span.badge diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml new file mode 100644 index 00000000000..e2033654018 --- /dev/null +++ b/app/views/shared/empty_states/_issues.html.haml @@ -0,0 +1,22 @@ +- button_path = local_assigns.fetch(:button_path, false) +- project_select_button = local_assigns.fetch(:project_select_button, false) +- has_button = button_path || project_select_button + +.row.empty-state + .pull-right.col-xs-12{ class: "#{'col-sm-6' if has_button}" } + .svg-content + = render 'shared/empty_states/icons/issues.svg' + .col-xs-12{ class: "#{'col-sm-6' if has_button}" } + .text-content + - if has_button && current_user + %h4 + The Issue Tracker is the place to add things that need to be improved or solved in a project + %p + Issues can be bugs, tasks or ideas to be discussed. + Also, issues are searchable and filterable. + - if project_select_button + = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue' + - else + = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link' + - else + %h4.text-center There are no issues to show. diff --git a/app/views/shared/empty_states/icons/_issues.svg b/app/views/shared/empty_states/icons/_issues.svg new file mode 100644 index 00000000000..2e92bf19579 --- /dev/null +++ b/app/views/shared/empty_states/icons/_issues.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="790 253 425 254" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="25" height="8.9423" x="25" y="88.4231" rx="2"/><mask id="h" width="25" height="8.9423" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M16 29.8013h43V91.404H16z"/><mask id="i" width="43" height="61.6026" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><path id="c" d="M57 60.6026l13.1868 9.3587c.449.3188.876 1.0142.9556 1.5673l3.5747 24.863c.1564 1.0866-.253 1.2572-.912.384L66 86.436l-9-6.9552"/><mask id="j" width="17.7504" height="36.7306" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><path id="d" d="M.2496 60.6026l13.1868 9.3587c.449.3188.876 1.0142.9556 1.5673l3.5748 24.863c.1562 1.0866-.2532 1.2572-.9123.384L9.2495 86.436l-9-6.9552"/><mask id="k" width="17.7504" height="36.7306" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask><path id="e" d="M16 29.8013L35.786 1.4556c.9466-1.3562 2.4792-1.3594 3.428 0L59 29.8013"/><mask id="l" width="43" height="29.364" x="0" y="0" fill="#fff"><use xlink:href="#e"/></mask><rect id="f" width="26.2653" height="35.5088" x="6.3673" rx="13.1327"/><mask id="m" width="26.2653" height="35.5088" x="0" y="0" fill="#fff"><use xlink:href="#f"/></mask><rect id="g" width="16.8367" height="22.386" x="4.0816" rx="8.4184"/><mask id="n" width="16.8367" height="22.386" x="0" y="0" fill="#fff"><use xlink:href="#g"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(792.000000, 255.000000)"><g fill="#FDE5D8"><path d="M225.4372 59.5866c-.059.5897-.1323 1.2698-.2203 2.0305-.252 2.1764-.5717 4.559-.9653 7.07-.1283.8185.4312 1.586 1.2496 1.7143.8185.1283 1.586-.4312 1.7142-1.2497.4-2.5528.7253-4.975.9815-7.1898.0898-.7762.1646-1.4715.2252-2.0762.0366-.365.0604-.62.0722-.7557.0717-.8254-.539-1.5526-1.3645-1.6244-.8254-.0717-1.5526.539-1.6244 1.3645-.0106.1228-.0332.365-.0684.7166zM219.8738 87.9413c-.2563.7878.1745 1.6342.9622 1.8906.7878.2562 1.6342-.1745 1.8906-.9623.975-2.9962 1.849-6.2827 2.6287-9.797.1794-.8086-.3308-1.6097-1.1395-1.789-.8088-.1795-1.61.3306-1.7893 1.1394-.76 3.4256-1.6096 6.6206-2.5527 9.5183zM209.9266 103.166c-.781.2766-1.1897 1.134-.913 1.9148.2765.781 1.1338 1.1897 1.9147.913 2.9792-1.0552 5.5414-3.679 7.7796-7.6272.4084-.7207.1554-1.636-.5653-2.0447-.7207-.4086-1.636-.1556-2.0446.565-1.9152 3.3786-3.9945 5.508-6.1714 6.279zM190.439 107.5834c-.7636.3214-1.122 1.201-.8005 1.9645.3215.7634 1.201 1.1217 1.9645.8003 3.1204-1.314 6.2717-2.3243 9.258-2.9816.809-.178 1.3205-.9783 1.1424-1.7874-.178-.809-.9783-1.3205-1.7874-1.1424-3.1666.697-6.4914 1.763-9.777 3.1464zM173.231 118.6257c-.6005.5706-.6248 1.52-.0542 2.1206s1.52.625 2.1206.0543c2.282-2.1682 4.8656-4.162 7.6758-5.946.6994-.444.9064-1.371.4624-2.0704-.444-.6994-1.371-.9064-2.0704-.4624-2.9698 1.8854-5.707 3.998-8.1342 6.304zM162.4543 136.2492c-.2022.8034.2852 1.6185 1.0885 1.8207.8034.202 1.6186-.2853 1.8208-1.0886.7688-3.0547 2.0416-5.9768 3.781-8.7486.4403-.7018.2284-1.6276-.4733-2.068-.7017-.4402-1.6275-.2283-2.068.4734-1.9026 3.0322-3.3016 6.2438-4.149 9.611zM162.1894 156.693c.1036.822.854 1.4042 1.676 1.3006.8218-.1037 1.404-.854 1.3004-1.676-.367-2.9097-.5796-6.1364-.6444-9.8167-.0146-.8284-.698-1.488-1.5262-1.4734-.8283.0146-1.488.698-1.4733 1.5262.0665 3.783.286 7.1162.6674 10.1393zM168.408 176.1653c.3876.7322 1.2953 1.0117 2.0275.6242.7322-.3875 1.0117-1.2952.6242-2.0274-1.6733-3.162-2.9028-5.9954-3.8477-8.943-.2528-.789-1.0973-1.2235-1.8862-.9706-.789.2528-1.2234 1.0974-.9706 1.8863 1.0025 3.1275 2.3014 6.121 4.053 9.4306zM175.9738 188.9357c1.056 1.7165 1.8892 3.0806 2.7307 4.474.4283.709 1.3503.9368 2.0595.5085.709-.4283.9368-1.3503.5085-2.0595-.8464-1.4014-1.6836-2.772-2.7434-4.4948.0808.131-1.9545-3.1733-2.486-4.0405-.4328-.7063-1.3563-.928-2.0627-.495-.7063.4327-.928 1.3563-.495 2.0626.5334.8707 2.5708 4.1785 2.4885 4.0447zM184.83 211.3822c.011.8284.6912 1.491 1.5196 1.4803.8283-.0108 1.491-.691 1.4803-1.5194-.046-3.519-.6604-6.996-1.8367-10.3262-.276-.7812-1.1328-1.1908-1.914-.915-.781.276-1.1906 1.133-.9147 1.914 1.0668 3.0206 1.624 6.1733 1.6655 9.3664zM179.3467 229.4095c-.459.6896-.2723 1.6208.4173 2.08.6896.459 1.6208.272 2.08-.4175 1.966-2.9533 3.4756-6.124 4.4877-9.4165.2434-.7918-.2012-1.631-.993-1.8745-.792-.2434-1.6312.2012-1.8746.993-.9264 3.014-2.3108 5.922-4.1173 8.6355z"/></g><g transform="translate(336.866969, 147.225953) rotate(-300.000000) translate(-336.866969, -147.225953) translate(299.366969, 69.725953)"><path stroke="#FDE5D8" stroke-width="3" d="M19 154l10-52.6603m16 0L55 154" stroke-linecap="round"/><rect width="3" height="38.75" x="35" y="99.3526" fill="#FDE5D8" rx="1.5"/><use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#h)" xlink:href="#a"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#i)" xlink:href="#b"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#j)" xlink:href="#c"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#k)" transform="translate(9.124810, 78.967887) scale(-1, 1) translate(-9.124810, -78.967887)" xlink:href="#d"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#l)" xlink:href="#e"/><ellipse cx="28.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="34.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="40.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="46.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="37.5" cy="55.1378" stroke="#FDE5D8" stroke-width="3" rx="10.5" ry="10.4327"/><ellipse cx="37.5" cy="55.1378" stroke="#FDE5D8" stroke-width="3" rx="5.5" ry="5.4647"/></g><path fill="#EEE" d="M96.0426 37.2106c-.1512 1.6874.0814 3.815.997 6.146.2046.5207.7936.7774 1.3155.5733.522-.2043.7793-.792.5747-1.313-.7912-2.0142-.99-3.832-.865-5.226.0102-.1143.0195-.186.0238-.2113.092-.552-.2814-1.0738-.8344-1.1658-.553-.092-1.076.2808-1.168.8326-.0126.075-.0285.1975-.0434.364zM107.5302 52.8934c.4913.239 1.098.0626 1.355-.394.2572-.4566.0674-1.0205-.4238-1.2595-1.8668-.9083-3.4584-1.9152-4.7943-3.0075-.4162-.3404-1.0506-.3026-1.4168.0843-.3663.387-.3256.9766.0907 1.317 1.4583 1.1925 3.1828 2.2835 5.1893 3.2596zM120.661 58.9533c.5467.171 1.1257-.1425 1.2933-.7003.1675-.5577-.1397-1.1484-.6864-1.3194-3.0283-.9472-4.1984-1.3178-5.915-1.8824-.544-.179-1.1274.126-1.3028.6813-.1754.5552.1235 1.1504.6677 1.3294 1.729.5686 2.9053.941 5.943 1.8913zM132.5954 62.881c.449.246 1.022.0983 1.2798-.33.258-.4282.103-.975-.3458-1.221-1.4942-.819-3.1928-1.545-5.2675-2.2746-.486-.1708-1.025.0664-1.204.53-.179.4634.0697.9776.5555 1.1484 1.9832.6973 3.5892 1.3838 4.982 2.1472zM141.9774 73.383c.205.4938.809.742 1.3485.5543.5395-.1878.8106-.7404.6055-1.2344-.8504-2.0482-1.853-3.7962-3.0375-5.3046-.337-.429-.99-.527-1.4588-.2184-.4687.3085-.5755.9064-.2386 1.3354 1.0743 1.368 1.9926 2.9692 2.7808 4.8675zM144.609 87.025c.0183.5535.5682.99 1.2283.9746.66-.0153 1.1805-.4764 1.1622-1.03-.0725-2.2033-.2693-4.206-.622-6.1198-.1008-.5473-.7115-.9225-1.3642-.838-.6526.0846-1.1.597-.999 1.1442.336 1.8248.5248 3.745.5947 5.869z"/><path fill="#E5E5E5" d="M144.1423 95.7297c-.0863 2.5442-.1214 3.769-.1422 5.2548-.0076.5523.3963 1.007.9022 1.0154.506.0083.9223-.4326.93-.985.0205-1.4668.0554-2.6812.1412-5.2113l.026-.7667c.0185-.552-.3764-1.016-.882-1.0363-.5056-.0203-.9306.411-.949.963l-.026.766zM144.939 115.201c.1196.5447.6727.8925 1.2355.7768.5628-.1157.922-.651.8026-1.1957-.417-1.9-.7104-3.84-.8976-5.8637-.0513-.5545-.5574-.964-1.1305-.9142-.573.0497-.996.5396-.9448 1.0942.1944 2.1015.4998 4.121.9348 6.103zM149.995 127.5248c.296.454.9528.61 1.4668.3485.514-.2614.6907-.8413.3947-1.2952-1.0787-1.6535-2.0046-3.3145-2.7896-4.9916-.2266-.484-.8547-.7143-1.403-.5142-.548.2-.809.7546-.5823 1.2387.8208 1.7534 1.788 3.4886 2.9134 5.2138zM154.8088 135.226c1.0587 1.232 2.242 2.4097 3.543 3.531.404.3482 1.0276.3186 1.393-.066.3657-.3843.3346-.978-.0692-1.3262-1.2296-1.0597-2.345-2.17-3.3402-3.328-.195-.227-.3872-.4542-.5764-.6813-.3385-.4063-.9588-.4744-1.3856-.1522-.4267.3223-.4983.913-.1598 1.3192.1954.2346.3938.469.5952.7034zM170.634 146.9026c.4806.242 1.0517.0176 1.2758-.501.224-.5188.0162-1.1354-.4642-1.3773-1.7563-.8842-3.422-1.8432-4.9857-2.8726-.4527-.298-1.0434-.1435-1.3195.3452-.276.4885-.133 1.126.3198 1.424 1.6256 1.0704 3.354 2.0655 5.1738 2.9816z"/><path fill="#EEE" d="M184.7334 151.9698c.5527.1412 1.1072-.2262 1.2385-.8206.1312-.5944-.2104-1.1908-.763-1.332-2.001-.5114-3.9602-1.1002-5.8632-1.763-.5405-.1883-1.1205.1303-1.2955.7115-.175.5813.1212 1.205.6616 1.3934 1.9557.6813 3.9676 1.286 6.0214 1.8108zM197.9337 153.9977c.5532.04 1.0297-.445 1.0643-1.083.0346-.6383-.3857-1.188-.939-1.228-1.973-.1424-3.952-.3682-5.9206-.676-.5492-.086-1.0547.358-1.1292.9917-.0744.6336.3105 1.2168.8597 1.3027 2.0164.3154 4.0433.5467 6.0647.6927zM212.1213 152.6062c.5493-.055.9392-.4576.871-.8994-.0684-.442-.569-.7555-1.1184-.7006-1.9168.1917-3.893.3194-5.9104.382-.553.0173-.9842.392-.9628.8368.0213.445.487.7916 1.0402.7744 2.0737-.0645 4.1064-.1957 6.0803-.3932zM226.3665 149.949c.5293-.22.7755-.8162.5497-1.332-.2257-.5155-.838-.7553-1.3672-.5354-1.7815.74-3.7143 1.3827-5.7772 1.923-.5558.1454-.8852.7023-.7358 1.2436.1494.5414.721.8623 1.2768.7168 2.1547-.5643 4.1797-1.2376 6.0537-2.016zM237.8486 140.4168c.292-.4344.1488-1.006-.3202-1.2766-.469-.2706-1.086-.1378-1.3782.2967-.9575 1.4237-2.225 2.7337-3.7847 3.9202-.427.3248-.4888.9087-.138 1.3042.3505.3955.981.4528 1.408.128 1.723-1.3107 3.1363-2.7714 4.213-4.3726zM245.6725 130.6874c.3987-.3503.439-.9587.09-1.3588-.3492-.4-.9554-.4405-1.3542-.0902-1.5048 1.3222-2.8978 2.7094-4.1698 4.1635-.3497.3995-.3102 1.008.088 1.3587.3983.3508 1.0046.3113 1.3542-.0884 1.2153-1.389 2.5487-2.717 3.9918-3.985zM257.4814 122.8697c.476-.2568.657-.8577.4047-1.342-.2523-.4843-.8428-.6687-1.3188-.4118-1.7682.9542-3.4795 1.973-5.1228 3.0587-.4518.2985-.5803.9133-.287 1.373.2934.46.8975.5906 1.3494.292 1.5938-1.0528 3.2557-2.0423 4.9746-2.97zM270.276 116.9216c.5503-.1682.8513-.724.6723-1.241-.179-.5173-.77-.8003-1.3204-.632-1.9296.5898-3.932 1.2728-5.975 2.054-.536.205-.7936.7797-.5754 1.2835.218.504.8294.746 1.3654.541 1.9947-.7628 3.95-1.4298 5.833-2.0054z"/><circle cx="145" cy="90" r="5" fill="#FFF" stroke="#EEE" stroke-width="2"/><circle cx="238" cy="138" r="5" fill="#FFF" stroke="#EEE" stroke-width="2"/><path stroke="#B5A7DD" stroke-width="3" d="M20.0605 56s-17.4698 33-12 53c5.4697 20 17 32 38 44S78.5 148 107 159s29 43 29 43" stroke-linecap="round" stroke-dasharray="8 10"/><g stroke="#EEE" stroke-width="3" transform="translate(108.000000, 173.000000)"><path fill="#FFF" d="M154 77c0-42.526-34.474-77-77-77S0 34.474 0 77" stroke-linecap="round"/><circle cx="108" cy="41" r="16"/><circle cx="42.5" cy="30.5" r="8.5"/><circle cx="22" cy="58" r="5"/></g><g><g fill="#FC8A51" transform="translate(235.917801, 27.746228) rotate(-345.000000) translate(-235.917801, -27.746228) translate(216.417801, 4.246228) translate(19.897959, -0.000000)"><path d="M.398 11.2982h2.3877c0-4.234 3.3853-7.6666 7.5612-7.6666v-2.421C4.8522 1.2105.398 5.727.398 11.298z"/><ellipse cx="10.7449" cy="2.0175" rx="1.9898" ry="2.0175"/></g><g fill="#FC8A51" transform="translate(235.917801, 27.746228) rotate(-345.000000) translate(-235.917801, -27.746228) translate(216.417801, 4.246228) translate(12.602041, 6.000000) scale(-1, 1) translate(-12.602041, -6.000000) translate(6.102041, -0.000000)"><path d="M.398 11.2982h2.3877c0-4.234 3.3853-7.6666 7.5612-7.6666v-2.421C4.8522 1.2105.398 5.727.398 11.298z"/><ellipse cx="10.7449" cy="2.0175" rx="1.9898" ry="2.0175"/></g><g transform="translate(235.917801, 27.746228) rotate(-345.000000) translate(-235.917801, -27.746228) translate(216.417801, 4.246228) translate(0.000000, 10.491228)"><g fill="#FC8A51" transform="translate(29.448980, 11.298246)"><rect width="7.9592" height="2" x=".7959" y="8.8772" rx="1"/><rect width="7.9592" height="2" x=".7959" y="16.1404" transform="translate(4.775510, 17.140351) rotate(-345.000000) translate(-4.775510, -17.140351)" rx="1"/><rect width="7.9592" height="2" x=".9151" y="1.8072" transform="translate(4.894667, 2.807217) rotate(-15.000000) translate(-4.894667, -2.807217)" rx="1"/></g><g fill="#FC8A51" transform="translate(5.051020, 21.298246) scale(-1, 1) translate(-5.051020, -21.298246) translate(0.551020, 11.298246)"><rect width="7.9592" height="2" x=".7959" y="8.8772" rx="1"/><rect width="7.9592" height="2" x=".7959" y="16.1404" transform="translate(4.775510, 17.140351) rotate(-345.000000) translate(-4.775510, -17.140351)" rx="1"/><rect width="7.9592" height="2" x=".9151" y="1.8072" transform="translate(4.894667, 2.807217) rotate(-15.000000) translate(-4.894667, -2.807217)" rx="1"/></g><use stroke="#FC8A51" stroke-width="6" mask="url(#m)" xlink:href="#f"/><path fill="#FC8A51" d="M7.1633 12.9123H31.041v3H7.1632z"/></g></g><g><g fill="#EEE" transform="translate(92.956359, 18.724125) scale(-1, 1) rotate(-345.000000) translate(-92.956359, -18.724125) translate(80.456359, 3.724125) translate(12.755102, 0.000000)"><path d="M.255 7.1228h1.5307c0-2.6694 2.17-4.8333 4.847-4.8333V.7632C3.1104.7632.255 3.6105.255 7.1228z"/><ellipse cx="6.8878" cy="1.2719" rx="1.2755" ry="1.2719"/></g><g fill="#EEE" transform="translate(92.956359, 18.724125) scale(-1, 1) rotate(-345.000000) translate(-92.956359, -18.724125) translate(80.456359, 3.724125) translate(7.744898, 4.000000) scale(-1, 1) translate(-7.744898, -4.000000) translate(3.244898, 0.000000)"><path d="M.255 7.1228h1.5307c0-2.6694 2.17-4.8333 4.847-4.8333V.7632C3.1104.7632.255 3.6105.255 7.1228z"/><ellipse cx="6.8878" cy="1.2719" rx="1.2755" ry="1.2719"/></g><g transform="translate(92.956359, 18.724125) scale(-1, 1) rotate(-345.000000) translate(-92.956359, -18.724125) translate(80.456359, 3.724125) translate(0.000000, 6.614035)"><g fill="#EEE" transform="translate(18.877551, 7.122807)"><rect width="5.102" height="2" x=".5102" y="5.5965" rx="1"/><rect width="5.102" height="2" x=".5102" y="10.1754" transform="translate(3.061224, 11.175439) rotate(-345.000000) translate(-3.061224, -11.175439)" rx="1"/><rect width="5.102" height="2" x=".5866" y="1.1393" transform="translate(3.137607, 2.139333) rotate(-15.000000) translate(-3.137607, -2.139333)" rx="1"/></g><g fill="#EEE" transform="translate(3.122449, 13.622807) scale(-1, 1) translate(-3.122449, -13.622807) translate(0.122449, 7.122807)"><rect width="5.102" height="2" x=".5102" y="5.5965" rx="1"/><rect width="5.102" height="2" x=".5102" y="10.1754" transform="translate(3.061224, 11.175439) rotate(-345.000000) translate(-3.061224, -11.175439)" rx="1"/><rect width="5.102" height="2" x=".5866" y="1.1393" transform="translate(3.137607, 2.139333) rotate(-15.000000) translate(-3.137607, -2.139333)" rx="1"/></g><use stroke="#EEE" stroke-width="4" mask="url(#n)" xlink:href="#g"/><path fill="#EEE" d="M4.5918 8.1404h15.306v2H4.592z"/></g></g><g fill="#FFF" transform="translate(0.000000, 103.000000)"><circle cx="8.5" cy="8.5" r="8.5" stroke="#B5A7DD" stroke-width="4"/><circle cx="171.5" cy="20.5" r="6.5"/></g><g><g transform="translate(39.000000, 142.000000)"><ellipse cx="12.5" cy="12.5" fill="#FFF" stroke="#6B4FBB" stroke-width="4" rx="12.5" ry="12.5"/><path fill="#FC8A51" d="M10.7322 13.475l-1.7665-1.7667c-.5873-.5873-1.5368-.587-2.1226-.0012-.5897.59-.585 1.5362.0013 2.1226l2.826 2.826.0007.0007.0006.0006c.5898.5897 1.534.587 2.118.003l6.3704-6.3703c.577-.577.5826-1.5323-.003-2.118-.59-.59-1.5343-.5873-2.1183-.0033l-5.3065 5.3065z"/></g></g><circle cx="171.5" cy="122.5" r="6.5" fill="#FFF" stroke="#FC8A51" stroke-width="3"/><circle cx="22" cy="52" r="6" fill="#FFF" stroke="#B5A7DD" stroke-width="3"/><path fill="#FFF" stroke="#B5A7DD" stroke-width="3.6" d="M188.151 141.596c8.7045-7.7456 11.0126-20.9255 4.8625-31.5777-7.0208-12.1604-22.4055-16.422-34.363-9.5183-11.9572 6.9036-15.959 22.358-8.9382 34.5183 6.2353 10.8 19.068 15.3695 30.2375 11.4206l10.8992 18.8778c1.3167 2.2807 4.2302 3.063 6.5078 1.748 2.273-1.3122 3.0567-4.2295 1.74-6.51l-10.9458-18.9587zm-8.4343-4.6086c7.8576-4.5366 10.4874-14.6923 5.8738-22.6834-4.6137-7.991-14.7237-10.7915-22.5814-6.255-7.8575 4.5368-10.4873 14.6925-5.8737 22.6836 4.6137 7.991 14.7237 10.7915 22.5814 6.2548z"/></g></svg> diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index 19221e3391f..f9a7aa4e29b 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -21,14 +21,14 @@ = icon('users') = number_with_delimiter(group.users.count) - %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} + %span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group) } = visibility_level_icon(group.visibility_level, fw: false) .avatar-container.s40 = image_tag group_icon(group), class: "avatar s40 hidden-xs" .title = link_to group, class: 'group-name' do - = group.name + = group.full_name - if group_member as diff --git a/app/views/shared/icons/_delta.svg b/app/views/shared/icons/_delta.svg new file mode 100644 index 00000000000..7c0c0d3999c --- /dev/null +++ b/app/views/shared/icons/_delta.svg @@ -0,0 +1,3 @@ +<svg width="14px" height="10px" viewBox="322 21 14 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <path d="M330.078605,22.8166945 L335.259532,29.6235062 C335.615145,30.0907182 335.412062,30.4694683 334.822641,30.4694683 L331.657805,30.4694683 L324.04678,30.4694683 C323.449879,30.4694683 323.260751,30.0822112 323.609889,29.6235062 L328.790816,22.8166945 C329.146429,22.3494825 329.729467,22.3579895 330.078605,22.8166945 Z" id="delta" stroke="#5C5C5C" stroke-width="1" fill="none"></path> +</svg> diff --git a/app/views/shared/icons/_go_logo.svg.erb b/app/views/shared/icons/_go_logo.svg.erb new file mode 100644 index 00000000000..5052651c110 --- /dev/null +++ b/app/views/shared/icons/_go_logo.svg.erb @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="<%= size %>" height="<%= size %>" viewBox="0 0 16 16"><g fill-rule="evenodd" transform="translate(0 1)"><path d="m14 15.01h1v-8.02c0-3.862-3.134-6.991-7-6.991-3.858 0-7 3.13-7 6.991v8.02h1v-8.02c0-3.306 2.691-5.991 6-5.991 3.314 0 6 2.682 6 5.991v8.02m-10.52-13.354c-.366-.402-.894-.655-1.48-.655-1.105 0-2 .895-2 2 0 .868.552 1.606 1.325 1.883.102-.321.226-.631.371-.93-.403-.129-.695-.507-.695-.953 0-.552.448-1 1-1 .306 0 .58.138.764.354.222-.25.461-.483.717-.699m9.04-.002c.366-.401.893-.653 1.479-.653 1.105 0 2 .895 2 2 0 .867-.552 1.606-1.324 1.883-.101-.321-.225-.632-.37-.931.403-.129.694-.507.694-.952 0-.552-.448-1-1-1-.305 0-.579.137-.762.353-.222-.25-.461-.483-.717-.699"/><path d="m5.726 7.04h1.557v.124c0 .283-.033.534-.1.752-.065.202-.175.391-.33.566-.35.394-.795.591-1.335.591-.527 0-.979-.19-1.355-.571-.376-.382-.564-.841-.564-1.377 0-.547.191-1.01.574-1.391.382-.382.848-.574 1.396-.574.295 0 .57.06.825.181.244.12.484.316.72.586l-.405.388c-.309-.412-.686-.618-1.13-.618-.399 0-.733.138-1 .413-.27.27-.405.609-.405 1.015 0 .42.151.766.452 1.037.282.252.587.378.915.378.28 0 .531-.094.754-.283.223-.19.347-.418.373-.683h-.94v-.535m2.884.061c0-.53.194-.986.583-1.367.387-.381.853-.571 1.396-.571.537 0 .998.192 1.382.576.386.384.578.845.578 1.384 0 .542-.194 1-.581 1.379-.389.379-.858.569-1.408.569-.487 0-.923-.168-1.311-.505-.426-.373-.64-.861-.64-1.465m.574.007c0 .417.14.759.42 1.028.278.269.6.403.964.403.395 0 .729-.137 1-.41.272-.277.408-.613.408-1.01 0-.402-.134-.739-.403-1.01-.267-.273-.597-.41-.991-.41-.392 0-.723.137-.993.41-.27.27-.405.604-.405 1m-.184 3.918c.525.026.812.063.812.063.271.025.324-.096.116-.273 0 0-.775-.813-1.933-.813-1.159 0-1.923.813-1.923.813-.211.174-.153.3.12.273 0 0 .286-.037.81-.063v.477c0 .268.224.5.5.5.268 0 .5-.223.5-.498v-.252.25c0 .268.224.5.5.5.268 0 .5-.223.5-.498v-.478m-1-1.023c.552 0 1-.224 1-.5 0-.276-.448-.5-1-.5-.552 0-1 .224-1 .5 0 .276.448.5 1 .5"/></g></svg> diff --git a/app/views/shared/icons/_icon_cycle_analytics_overview.svg b/app/views/shared/icons/_icon_cycle_analytics_overview.svg new file mode 100644 index 00000000000..eea9c975c35 --- /dev/null +++ b/app/views/shared/icons/_icon_cycle_analytics_overview.svg @@ -0,0 +1,81 @@ +<svg width="366px" height="229px" viewBox="784 258 366 229" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch --> + <desc>Created with Sketch.</desc> + <defs> + <rect id="path-1" x="35" y="39" width="24" height="21" rx="10"></rect> + <mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="24" height="21" fill="white"> + <use xlink:href="#path-1"></use> + </mask> + <rect id="path-3" x="64.8662386" y="58.3882666" width="10" height="71" rx="5"></rect> + <mask id="mask-4" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="10" height="71" fill="white"> + <use xlink:href="#path-3"></use> + </mask> + <rect id="path-5" x="18.1550472" y="58.3882666" width="10" height="71" rx="5"></rect> + <mask id="mask-6" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="10" height="71" fill="white"> + <use xlink:href="#path-5"></use> + </mask> + <rect id="path-7" x="24" y="56" width="46" height="10" rx="5"></rect> + <mask id="mask-8" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="46" height="10" fill="white"> + <use xlink:href="#path-7"></use> + </mask> + <rect id="path-9" x="42" y="60" width="10" height="68" rx="5"></rect> + <mask id="mask-10" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="10" height="68" fill="white"> + <use xlink:href="#path-9"></use> + </mask> + <rect id="path-11" x="69" y="12" width="12" height="12" rx="3"></rect> + <mask id="mask-12" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="12" height="12" fill="white"> + <use xlink:href="#path-11"></use> + </mask> + <rect id="path-13" x="40" y="18" width="14" height="22" rx="6"></rect> + <mask id="mask-14" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="14" height="22" fill="white"> + <use xlink:href="#path-13"></use> + </mask> + <rect id="path-15" x="41" y="8" width="34" height="20" rx="3"></rect> + <mask id="mask-16" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="34" height="20" fill="white"> + <use xlink:href="#path-15"></use> + </mask> + <path d="M8,8.00793008 C8,6.34669617 9.34984627,5.0321392 11.0036812,5.07151622 L46.9963188,5.92848378 C48.6552061,5.9679811 50,7.34177063 50,8.99109042 L50,27.0089096 C50,28.6608432 48.6501537,30.0321392 46.9963188,30.0715162 L11.0036812,30.9284838 C9.34479389,30.9679811 8,29.6568766 8,27.9920699 L8,8.00793008 Z" id="path-17"></path> + <mask id="mask-18" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="42" height="25.858699" fill="white"> + <use xlink:href="#path-17"></use> + </mask> + <rect id="path-19" x="-7.10542736e-15" y="1.77635684e-14" width="16" height="36" rx="3"></rect> + <mask id="mask-20" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="16" height="36" fill="white"> + <use xlink:href="#path-19"></use> + </mask> + </defs> + <g id="Group-7" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(786.000000, 259.000000)"> + <g id="Group-5" transform="translate(132.727922, 71.000000)"> + <use id="Rectangle-21" stroke="#EEEEEE" mask="url(#mask-2)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-1"></use> + <use id="Rectangle-16-Copy" stroke="#EEEEEE" mask="url(#mask-4)" stroke-width="8" fill="#FFFFFF" transform="translate(69.866239, 93.888267) rotate(-20.000000) translate(-69.866239, -93.888267) " xlink:href="#path-3"></use> + <use id="Rectangle-16-Copy-2" stroke="#EEEEEE" mask="url(#mask-6)" stroke-width="8" fill="#FFFFFF" transform="translate(23.155047, 93.888267) scale(-1, 1) rotate(-20.000000) translate(-23.155047, -93.888267) " xlink:href="#path-5"></use> + <use id="Rectangle-15" stroke="#EEEEEE" mask="url(#mask-8)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-7"></use> + <use id="Rectangle-16" stroke="#EEEEEE" mask="url(#mask-10)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-9"></use> + <g id="Group" transform="translate(45.500000, 33.000000) rotate(20.000000) translate(-45.500000, -33.000000) translate(5.000000, 13.000000)"> + <use id="Rectangle-4" stroke="#EEEEEE" mask="url(#mask-12)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-11"></use> + <use id="Rectangle-20" stroke="#EEEEEE" mask="url(#mask-14)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-13"></use> + <use id="Rectangle-2" stroke="#EEEEEE" mask="url(#mask-16)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-15"></use> + <use id="Rectangle" stroke="#EEEEEE" mask="url(#mask-18)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-17"></use> + <rect id="Rectangle-17" fill="#EEEEEE" x="21" y="7" width="3" height="22"></rect> + <rect id="Rectangle-17-Copy" fill="#EEEEEE" x="64" y="8" width="3" height="17"></rect> + <circle id="Oval-9" fill="#B5A7DD" cx="40" cy="18" r="2"></circle> + <circle id="Oval-9-Copy-4" fill="#EEEEEE" cx="47" cy="33" r="2"></circle> + <use id="Rectangle-19" stroke="#EEEEEE" mask="url(#mask-20)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-19"></use> + </g> + </g> + <path d="M265.128496,225.286991 C247.289192,194.617726 214.068171,174 176.031622,174 C137.847583,174 104.51649,194.77793 86.7279221,225.644211" id="Oval-10" stroke="#EEEEEE" stroke-width="4" stroke-linecap="round" fill="#FFFFFF"></path> + <circle id="Oval-11" stroke="#FDE5D8" stroke-width="4" fill="#FFFFFF" cx="24.5" cy="25.5" r="24.5"></circle> + <path d="M24,1.00292933 C24,0.449026756 24.4438648,0 25,0 C25.5522847,0 26,0.437881351 26,1.00292933 L26,5.99707067 C26,6.55097324 25.5561352,7 25,7 C24.4477153,7 24,6.56211865 24,5.99707067 L24,1.00292933 Z M48.46461,17.3244238 C48.9914026,17.1532585 49.5556142,17.4366422 49.7274694,17.9655581 C49.8981348,18.4908122 49.6200365,19.0519274 49.0826439,19.2265369 L44.3329333,20.7698114 C43.8061406,20.9409767 43.241929,20.6575931 43.0700738,20.1286771 C42.8994084,19.6034231 43.1775067,19.0423078 43.7148993,18.8676984 L48.46461,17.3244238 Z M40.5019265,45.6352697 C40.8275022,46.0833863 40.7323394,46.7075538 40.2824166,47.0344419 C39.8356088,47.3590667 39.2160194,47.2679737 38.8838925,46.8108402 L35.9484099,42.770495 C35.6228341,42.3223784 35.717997,41.6982109 36.1679198,41.3713229 C36.6147275,41.0466981 37.234317,41.1377911 37.5664439,41.5949245 L40.5019265,45.6352697 Z M11.1161075,46.8108402 C10.7905317,47.2589568 10.1675063,47.3613299 9.71758344,47.0344419 C9.27077569,46.709817 9.16594665,46.0924031 9.49807352,45.6352697 L12.4335561,41.5949245 C12.7591319,41.1468079 13.3821574,41.0444348 13.8320802,41.3713229 C14.278888,41.6959477 14.383717,42.3133616 14.0515901,42.770495 L11.1161075,46.8108402 Z M0.917356057,19.2265369 C0.390563404,19.0553716 0.100675355,18.4944741 0.272530576,17.9655581 C0.44319595,17.4403041 0.997997482,17.1498144 1.53539005,17.3244238 L6.28510071,18.8676984 C6.81189336,19.0388637 7.10178141,19.5997611 6.92992619,20.1286771 C6.75926082,20.6539311 6.20445928,20.9444208 5.66706672,20.7698114 L0.917356057,19.2265369 Z" id="Rectangle-23" fill="#FDE5D8"></path> + <rect id="Rectangle-18" fill="#FC6D26" x="24" y="14" width="3" height="12" rx="1.5"></rect> + <rect id="Rectangle-22" fill="#FC6D26" x="24" y="24" width="12" height="3" rx="1.5"></rect> + <circle id="Oval-11" fill="#6B4FBB" cx="25.5" cy="25.5" r="2.5"></circle> + <path d="M358.949747,6.87474747 L357.453009,7.20729654 C356.9128,7.32732164 356.570654,6.9935311 356.692198,6.44648557 L357.024747,4.94974747 L356.692198,3.45300937 C356.572173,2.91279997 356.905964,2.57065443 357.453009,2.69219839 L358.949747,3.02474747 L360.446486,2.69219839 C360.986695,2.5721733 361.328841,2.90596384 361.207297,3.45300937 L360.874747,4.94974747 L361.207297,6.44648557 C361.327322,6.98669496 360.993531,7.32884051 360.446486,7.20729654 L358.949747,6.87474747 Z" id="Star-Copy-5" fill="#6B4FBB" transform="translate(358.949747, 4.949747) rotate(-315.000000) translate(-358.949747, -4.949747) "></path> + <path d="M113.949747,32.8747475 L112.453009,33.2072965 C111.9128,33.3273216 111.570654,32.9935311 111.692198,32.4464856 L112.024747,30.9497475 L111.692198,29.4530094 C111.572173,28.9128 111.905964,28.5706544 112.453009,28.6921984 L113.949747,29.0247475 L115.446486,28.6921984 C115.986695,28.5721733 116.328841,28.9059638 116.207297,29.4530094 L115.874747,30.9497475 L116.207297,32.4464856 C116.327322,32.986695 115.993531,33.3288405 115.446486,33.2072965 L113.949747,32.8747475 Z" id="Star-Copy-7" fill="#B5A7DD" transform="translate(113.949747, 30.949747) rotate(-315.000000) translate(-113.949747, -30.949747) "></path> + <path d="M329.949747,211.874747 L328.453009,212.207297 C327.9128,212.327322 327.570654,211.993531 327.692198,211.446486 L328.024747,209.949747 L327.692198,208.453009 C327.572173,207.9128 327.905964,207.570654 328.453009,207.692198 L329.949747,208.024747 L331.446486,207.692198 C331.986695,207.572173 332.328841,207.905964 332.207297,208.453009 L331.874747,209.949747 L332.207297,211.446486 C332.327322,211.986695 331.993531,212.328841 331.446486,212.207297 L329.949747,211.874747 Z" id="Star-Copy-6" fill="#B5A7DD" opacity="0.5" transform="translate(329.949747, 209.949747) rotate(-315.000000) translate(-329.949747, -209.949747) "></path> + <path d="M265.363961,54.838961 L263.153969,55.3299826 C262.617155,55.4492534 262.280283,55.1035008 262.397939,54.5739526 L262.888961,52.363961 L262.397939,50.1539694 C262.278669,49.6171548 262.624421,49.2802831 263.153969,49.3979395 L265.363961,49.888961 L267.573953,49.3979395 C268.110767,49.2786686 268.447639,49.6244213 268.329983,50.1539694 L267.838961,52.363961 L268.329983,54.5739526 C268.449253,55.1107673 268.103501,55.4476389 267.573953,55.3299826 L265.363961,54.838961 Z" id="Star-Copy-9" fill="#FC6D26" transform="translate(265.363961, 52.363961) rotate(-315.000000) translate(-265.363961, -52.363961) "></path> + <path d="M56.363961,142.838961 L54.1539694,143.329983 C53.6171548,143.449253 53.2802831,143.103501 53.3979395,142.573953 L53.888961,140.363961 L53.3979395,138.153969 C53.2786686,137.617155 53.6244213,137.280283 54.1539694,137.397939 L56.363961,137.888961 L58.5739526,137.397939 C59.1107673,137.278669 59.4476389,137.624421 59.3299826,138.153969 L58.838961,140.363961 L59.3299826,142.573953 C59.4492534,143.110767 59.1035008,143.447639 58.5739526,143.329983 L56.363961,142.838961 Z" id="Star-Copy-8" fill="#6B4FBB" transform="translate(56.363961, 140.363961) rotate(-315.000000) translate(-56.363961, -140.363961) "></path> + <g id="Group-6" transform="translate(311.872633, 125.094458) rotate(-345.000000) translate(-311.872633, -125.094458) translate(290.872633, 115.094458)"> + <circle id="Oval-12" stroke="#FDE5D8" stroke-width="4" fill="#FFFFFF" cx="21" cy="10" r="10"></circle> + <ellipse id="Oval-13" fill="#FDE5D8" cx="21" cy="10" rx="21" ry="2"></ellipse> + </g> + </g> +</svg> diff --git a/app/views/shared/icons/_icon_lock.svg b/app/views/shared/icons/_icon_lock.svg new file mode 100644 index 00000000000..6ec671a76ed --- /dev/null +++ b/app/views/shared/icons/_icon_lock.svg @@ -0,0 +1,25 @@ +<svg width="46px" height="54px" viewBox="227 0 46 54" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch --> + <desc>Created with Sketch.</desc> + <defs> + <rect id="path-1" x="0" y="20" width="46" height="34" rx="8"></rect> + <mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="46" height="34" fill="white"> + <use xlink:href="#path-1"></use> + </mask> + <path d="M29,16 C29,8.2680135 22.7319865,2 15,2 C7.2680135,2 1,8.2680135 1,16" id="path-3"></path> + <mask id="mask-4" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="28" height="14" fill="white"> + <use xlink:href="#path-3"></use> + </mask> + </defs> + <g id="locker" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(227.000000, 0.000000)"> + <g id="Group-8"> + <use id="Rectangle-14" stroke="#B5A7DD" mask="url(#mask-2)" stroke-width="6" xlink:href="#path-1"></use> + <g id="Group-7" transform="translate(8.000000, 0.000000)"> + <use id="Oval-3" stroke="#B5A7DD" mask="url(#mask-4)" stroke-width="6" xlink:href="#path-3"></use> + <rect id="Rectangle-13" fill="#B5A7DD" x="1" y="16" width="3" height="6"></rect> + <rect id="Rectangle-13-Copy" fill="#B5A7DD" x="26" y="16" width="3" height="6"></rect> + </g> + <path d="M25,37.4648712 C26.1956027,36.7732524 27,35.4805647 27,34 C27,31.790861 25.209139,30 23,30 C20.790861,30 19,31.790861 19,34 C19,35.4805647 19.8043973,36.7732524 21,37.4648712 L21,41.0026083 C21,42.1041422 21.8954305,43 23,43 C24.1122704,43 25,42.1057373 25,41.0026083 L25,37.4648712 Z" id="Combined-Shape" fill="#6B4FBB"></path> + </g> + </g> +</svg> diff --git a/app/views/shared/icons/_icon_no_data.svg b/app/views/shared/icons/_icon_no_data.svg new file mode 100644 index 00000000000..ced8653b88c --- /dev/null +++ b/app/views/shared/icons/_icon_no_data.svg @@ -0,0 +1,27 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="211 0 78 36" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <circle id="a" cx="5" cy="31" r="5"/> + <mask id="e" width="10" height="10" x="0" y="0" fill="#fff"> + <use xlink:href="#a"/> + </mask> + <circle id="b" cx="29" cy="14" r="5"/> + <mask id="f" width="10" height="10" x="0" y="0" fill="#fff"> + <use xlink:href="#b"/> + </mask> + <circle id="c" cx="53" cy="24" r="5"/> + <mask id="g" width="10" height="10" x="0" y="0" fill="#fff"> + <use xlink:href="#c"/> + </mask> + <circle id="d" cx="73" cy="5" r="5"/> + <mask id="h" width="10" height="10" x="0" y="0" fill="#fff"> + <use xlink:href="#d"/> + </mask> + </defs> + <g fill="none" fill-rule="evenodd" transform="translate(211)"> + <path stroke="#B5A7DD" stroke-width="2" d="M5 31l24-17 26 10L73 5" stroke-linecap="round" stroke-dasharray="3 6"/> + <use fill="#FFF" stroke="#6B4FBB" stroke-width="6" mask="url(#e)" xlink:href="#a"/> + <use fill="#FFF" stroke="#6B4FBB" stroke-width="6" mask="url(#f)" xlink:href="#b"/> + <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#g)" xlink:href="#c"/> + <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#h)" xlink:href="#d"/> + </g> +</svg> diff --git a/app/views/shared/icons/_icon_status_canceled.svg b/app/views/shared/icons/_icon_status_canceled.svg index 1b2d0891244..bd5d04e1cd7 100644..100755 --- a/app/views/shared/icons/_icon_status_canceled.svg +++ b/app/views/shared/icons/_icon_status_canceled.svg @@ -1,6 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" class="ci-status-icon-canceled" viewBox="0 0 14 14"> - <g fill="#5C5C5C" fill-rule="evenodd"> - <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/> - <rect width="8" height="2" x="3" y="6" transform="rotate(45 7 7)" rx=".5"/> - </g> -</svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_canceled_borderless.svg b/app/views/shared/icons/_icon_status_canceled_borderless.svg new file mode 100644 index 00000000000..bf7fb29185f --- /dev/null +++ b/app/views/shared/icons/_icon_status_canceled_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"><path d="M8.17142857,5.97142857 L15.8714286,13.6714286 C16.1857143,13.9857143 16.1857143,14.4571429 15.8714286,14.7714286 L14.7714286,15.8714286 C14.4571429,16.1857143 13.9857143,16.1857143 13.6714286,15.8714286 L5.97142857,8.17142857 C5.65714286,7.85714286 5.65714286,7.38571429 5.97142857,7.07142857 L7.07142857,5.97142857 C7.38571429,5.65714286 7.85714286,5.65714286 8.17142857,5.97142857" id="Shape"></path></svg> diff --git a/app/views/shared/icons/_icon_status_created.svg b/app/views/shared/icons/_icon_status_created.svg index dca5d289767..326ad04e017 100644..100755 --- a/app/views/shared/icons/_icon_status_created.svg +++ b/app/views/shared/icons/_icon_status_created.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" class="ci-status-icon-created" viewBox="0 0 14 14" enable-background="new 0 0 14 14"><path d="M12.5,7 C12.5,4 10,1.5 7,1.5 C4,1.5 1.5,4 1.5,7 C1.5,10 4,12.5 7,12.5 C10,12.5 12.5,10 12.5,7 L12.5,7 Z M0,7 C0,3.1 3.1,0 7,0 C10.9,0 14,3.1 14,7 C14,10.9 10.9,14 7,14 C3.1,14 0,10.9 0,7 L0,7 Z" /><circle cx="7" cy="7" r="3.25"/></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_created_borderless.svg b/app/views/shared/icons/_icon_status_created_borderless.svg new file mode 100644 index 00000000000..1810d023be8 --- /dev/null +++ b/app/views/shared/icons/_icon_status_created_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><circle id="Oval" cx="11" cy="11" r="5.10714286"></circle></svg> diff --git a/app/views/shared/icons/_icon_status_failed.svg b/app/views/shared/icons/_icon_status_failed.svg index e56e0887416..64da5aa31fc 100644..100755 --- a/app/views/shared/icons/_icon_status_failed.svg +++ b/app/views/shared/icons/_icon_status_failed.svg @@ -1,6 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"> - <g fill="#D22852" fill-rule="evenodd"> - <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/> - <path d="M7.72916667,6.27083333 L7.72916667,4.28939247 C7.72916667,4.12531853 7.59703895,4 7.43405116,4 L6.56594884,4 C6.40541585,4 6.27083333,4.12956542 6.27083333,4.28939247 L6.27083333,6.27083333 L4.28939247,6.27083333 C4.12531853,6.27083333 4,6.40296105 4,6.56594884 L4,7.43405116 C4,7.59458415 4.12956542,7.72916667 4.28939247,7.72916667 L6.27083333,7.72916667 L6.27083333,9.71060753 C6.27083333,9.87468147 6.40296105,10 6.56594884,10 L7.43405116,10 C7.59458415,10 7.72916667,9.87043458 7.72916667,9.71060753 L7.72916667,7.72916667 L9.71060753,7.72916667 C9.87468147,7.72916667 10,7.59703895 10,7.43405116 L10,6.56594884 C10,6.40541585 9.87043458,6.27083333 9.71060753,6.27083333 L7.72916667,6.27083333 Z" transform="rotate(-45 7 7)"/> - </g> -</svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_failed_borderless.svg b/app/views/shared/icons/_icon_status_failed_borderless.svg new file mode 100644 index 00000000000..b7022350c74 --- /dev/null +++ b/app/views/shared/icons/_icon_status_failed_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M12.1458333,9.85416667 L12.1458333,6.74047388 C12.1458333,6.4826434 11.9382041,6.28571429 11.6820804,6.28571429 L10.3179196,6.28571429 C10.0656535,6.28571429 9.85416667,6.48931709 9.85416667,6.74047388 L9.85416667,9.85416667 L6.74047388,9.85416667 C6.4826434,9.85416667 6.28571429,10.0617959 6.28571429,10.3179196 L6.28571429,11.6820804 C6.28571429,11.9343465 6.48931709,12.1458333 6.74047388,12.1458333 L9.85416667,12.1458333 L9.85416667,15.2595261 C9.85416667,15.5173566 10.0617959,15.7142857 10.3179196,15.7142857 L11.6820804,15.7142857 C11.9343465,15.7142857 12.1458333,15.5106829 12.1458333,15.2595261 L12.1458333,12.1458333 L15.2595261,12.1458333 C15.5173566,12.1458333 15.7142857,11.9382041 15.7142857,11.6820804 L15.7142857,10.3179196 C15.7142857,10.0656535 15.5106829,9.85416667 15.2595261,9.85416667 L12.1458333,9.85416667 Z" id="Combined-Shape" transform="translate(11.000000, 11.000000) rotate(-45.000000) translate(-11.000000, -11.000000) "></path></svg> diff --git a/app/views/shared/icons/_icon_status_manual.svg b/app/views/shared/icons/_icon_status_manual.svg new file mode 100755 index 00000000000..c98839f51a9 --- /dev/null +++ b/app/views/shared/icons/_icon_status_manual.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_manual_borderless.svg b/app/views/shared/icons/_icon_status_manual_borderless.svg new file mode 100644 index 00000000000..5eec665688b --- /dev/null +++ b/app/views/shared/icons/_icon_status_manual_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M16.5,11.9906832 L16.5,10.0093168 L15.2625,9.80434783 C15.19375,9.5310559 15.05625,9.25776398 14.85,8.84782609 L15.60625,7.82298137 L14.1625,6.38819876 L13.13125,7.13975155 C12.7875,6.93478261 12.44375,6.79813665 12.16875,6.72981366 L12.03125,5.5 L10.0375,5.5 L9.83125,6.72981366 C9.4875,6.79813665 9.2125,6.93478261 8.86875,7.13975155 L7.8375,6.38819876 L6.39375,7.82298137 L7.08125,8.84782609 C6.875,9.18944099 6.80625,9.46273292 6.66875,9.80434783 L5.5,9.94099379 L5.5,11.9223602 L6.7375,12.1273292 C6.80625,12.4689441 6.94375,12.742236 7.15,13.0838509 L6.4625,14.1086957 L7.90625,15.5434783 L8.9375,14.8602484 C9.2125,14.9968944 9.55625,15.1335404 9.9,15.2701863 L10.10625,16.5 L12.16875,16.5 L12.375,15.2701863 C12.71875,15.2018634 12.99375,15.0652174 13.3375,14.8602484 L14.36875,15.6118012 L15.8125,14.1770186 L15.05625,13.1521739 C15.2625,12.810559 15.4,12.4689441 15.46875,12.1956522 L16.5,11.9906832 L16.5,11.9906832 Z M11,13.015528 C9.83125,13.015528 8.9375,12.1273292 8.9375,10.9658385 C8.9375,9.80434783 9.83125,8.91614907 11,8.91614907 C12.16875,8.91614907 13.0625,9.80434783 13.0625,10.9658385 C13.0625,12.1273292 12.16875,13.015528 11,13.015528 L11,13.015528 Z" id="Shape" ></path></svg> diff --git a/app/views/shared/icons/_icon_status_pending.svg b/app/views/shared/icons/_icon_status_pending.svg index 117f0367161..02d5da407e3 100644..100755 --- a/app/views/shared/icons/_icon_status_pending.svg +++ b/app/views/shared/icons/_icon_status_pending.svg @@ -1,6 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"> - <g fill="#E75E40" fill-rule="evenodd"> - <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/> - <path d="M4.69999981,5.30065012 C4.69999981,5.13460564 4.83842754,5 5.00354719,5 L5.89645243,5 C6.06409702,5 6.19999981,5.13308716 6.19999981,5.30065012 L6.19999981,8.69934988 C6.19999981,8.86539436 6.06157207,9 5.89645243,9 L5.00354719,9 C4.8359026,9 4.69999981,8.86691284 4.69999981,8.69934988 L4.69999981,5.30065012 Z M7.69999981,5.30065012 C7.69999981,5.13460564 7.83842754,5 8.00354719,5 L8.89645243,5 C9.06409702,5 9.19999981,5.13308716 9.19999981,5.30065012 L9.19999981,8.69934988 C9.19999981,8.86539436 9.06157207,9 8.89645243,9 L8.00354719,9 C7.8359026,9 7.69999981,8.86691284 7.69999981,8.69934988 L7.69999981,5.30065012 Z"/> - </g> -</svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_pending_borderless.svg b/app/views/shared/icons/_icon_status_pending_borderless.svg new file mode 100644 index 00000000000..8d66e9e6c9c --- /dev/null +++ b/app/views/shared/icons/_icon_status_pending_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M7.38571429,8.32857143 C7.38571429,8.01428571 7.54285714,7.85714286 7.85714286,7.85714286 L9.27142857,7.85714286 C9.58571429,7.85714286 9.74285714,8.01428571 9.74285714,8.32857143 L9.74285714,13.6714286 C9.74285714,13.9857143 9.58571429,14.1428571 9.27142857,14.1428571 L7.85714286,14.1428571 C7.54285714,14.1428571 7.38571429,13.9857143 7.38571429,13.6714286 L7.38571429,8.32857143 M12.1,8.32857143 C12.1,8.01428571 12.2571429,7.85714286 12.5714286,7.85714286 L13.9857143,7.85714286 C14.3,7.85714286 14.4571429,8.01428571 14.4571429,8.32857143 L14.4571429,13.6714286 C14.4571429,13.9857143 14.3,14.1428571 13.9857143,14.1428571 L12.5714286,14.1428571 C12.2571429,14.1428571 12.1,13.9857143 12.1,13.6714286 L12.1,8.32857143" id="Shape"></path></svg> diff --git a/app/views/shared/icons/_icon_status_running.svg b/app/views/shared/icons/_icon_status_running.svg index 920d7952eb5..532f4fee33c 100644..100755 --- a/app/views/shared/icons/_icon_status_running.svg +++ b/app/views/shared/icons/_icon_status_running.svg @@ -1,6 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"> - <g fill="#2D9FD8" fill-rule="evenodd"> - <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/> - <path d="M7,3 C9.209139,3 11,4.790861 11,7 C11,9.209139 9.209139,11 7,11 C5.65802855,11 4.47040669,10.3391508 3.74481446,9.32513253 L7,7 L7,3 L7,3 Z"/> - </g> -</svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_running_borderless.svg b/app/views/shared/icons/_icon_status_running_borderless.svg new file mode 100644 index 00000000000..2757a168ed5 --- /dev/null +++ b/app/views/shared/icons/_icon_status_running_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11,4.71428571 C14.4571429,4.71428571 17.2857143,7.54285714 17.2857143,11 C17.2857143,14.4571429 14.4571429,17.2857143 11,17.2857143 C8.95714286,17.2857143 7.07142857,16.1857143 5.81428571,14.6142857 L11,11 L11,4.71428571" id="Shape"></path></svg> diff --git a/app/views/shared/icons/_icon_status_skipped.svg b/app/views/shared/icons/_icon_status_skipped.svg index 014ca86b61b..1998dfef9ea 100644..100755 --- a/app/views/shared/icons/_icon_status_skipped.svg +++ b/app/views/shared/icons/_icon_status_skipped.svg @@ -1 +1 @@ -<svg width="20" height="20" class="ci-status-icon-skipped" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Group Copy 31</title><g fill="#5C5C5C" fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7.69 7.7l-.905.905a.7.7 0 0 0 .99.99l1.85-1.85c.411-.412.411-1.078 0-1.49l-1.85-1.85a.7.7 0 0 0-.99.99l.905.905H4.48a.7.7 0 0 0 0 1.4h3.21z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_skipped_borderless.svg b/app/views/shared/icons/_icon_status_skipped_borderless.svg new file mode 100644 index 00000000000..fb3e930b3cb --- /dev/null +++ b/app/views/shared/icons/_icon_status_skipped_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M12.0846,12.1 L10.6623,13.5223 C10.2454306,13.9539168 10.2513924,14.6399933 10.6756996,15.0643004 C11.1000067,15.4886076 11.7860832,15.4945694 12.2177,15.0777 L15.1261,12.1693 C15.7708612,11.5230891 15.7708612,10.4769109 15.1261,9.8307 L12.2177,6.9223 C11.7860832,6.50543057 11.1000067,6.51139239 10.6756996,6.93569957 C10.2513924,7.36000675 10.2454306,8.04608322 10.6623,8.4777 L12.0846,9.9 L7.04,9.9 C6.43248678,9.9 5.94,10.3924868 5.94,11 C5.94,11.6075132 6.43248678,12.1 7.04,12.1 L12.0846,12.1 L12.0846,12.1 Z" id="Shape"></path></svg> diff --git a/app/views/shared/icons/_icon_status_success.svg b/app/views/shared/icons/_icon_status_success.svg index 67b378b3571..eed5006bebe 100644..100755 --- a/app/views/shared/icons/_icon_status_success.svg +++ b/app/views/shared/icons/_icon_status_success.svg @@ -1,6 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"> - <g fill="#31AF64" fill-rule="evenodd"> - <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/> - <path d="M7.29166667,7.875 L5.54840803,7.875 C5.38293028,7.875 5.25,8.00712771 5.25,8.17011551 L5.25,9.03821782 C5.25,9.19875081 5.38360183,9.33333333 5.54840803,9.33333333 L8.24853534,9.33333333 C8.52035522,9.33333333 8.75,9.11228506 8.75,8.83960819 L8.75,8.46475969 L8.75,4.07392947 C8.75,3.92144267 8.61787229,3.79166667 8.45488449,3.79166667 L7.58678218,3.79166667 C7.42624919,3.79166667 7.29166667,3.91804003 7.29166667,4.07392947 L7.29166667,7.875 Z" transform="rotate(45 7 6.563)"/> - </g> -</svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_success_borderless.svg b/app/views/shared/icons/_icon_status_success_borderless.svg new file mode 100644 index 00000000000..8ee5be7ab78 --- /dev/null +++ b/app/views/shared/icons/_icon_status_success_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11.4583333,12.375 L8.70008808,12.375 C8.45889044,12.375 8.25,12.5826293 8.25,12.8387529 L8.25,14.2029137 C8.25,14.4551799 8.4515113,14.6666667 8.70008808,14.6666667 L12.9619841,14.6666667 C13.3891296,14.6666667 13.75,14.3193051 13.75,13.8908129 L13.75,13.2899463 L13.75,6.42552703 C13.75,6.16226705 13.5423707,5.95833333 13.2862471,5.95833333 L11.9220863,5.95833333 C11.6698201,5.95833333 11.4583333,6.16750307 11.4583333,6.42552703 L11.4583333,12.375 Z" id="Combined-Shape" transform="translate(11.000000, 10.312500) rotate(-315.000000) translate(-11.000000, -10.312500) "></path></svg> diff --git a/app/views/shared/icons/_icon_status_warning.svg b/app/views/shared/icons/_icon_status_warning.svg index d0ad4bd65b1..cb785635b7e 100644..100755 --- a/app/views/shared/icons/_icon_status_warning.svg +++ b/app/views/shared/icons/_icon_status_warning.svg @@ -1,6 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"> - <g fill="#FF8A24" fill-rule="evenodd"> - <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/> - <path d="M6,3.49769878 C6,3.22282734 6.21403503,3 6.50468445,3 L7.49531555,3 C7.77404508,3 8,3.21484375 8,3.49769878 L8,7.50230122 C8,7.77717266 7.78596497,8 7.49531555,8 L6.50468445,8 C6.22595492,8 6,7.78515625 6,7.50230122 L6,3.49769878 Z M6,9.50468445 C6,9.22595492 6.21403503,9 6.50468445,9 L7.49531555,9 C7.77404508,9 8,9.21403503 8,9.50468445 L8,10.4953156 C8,10.7740451 7.78596497,11 7.49531555,11 L6.50468445,11 C6.22595492,11 6,10.785965 6,10.4953156 L6,9.50468445 Z"/> - </g> -</svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></svg> diff --git a/app/views/shared/icons/_icon_status_warning_borderless.svg b/app/views/shared/icons/_icon_status_warning_borderless.svg new file mode 100644 index 00000000000..7b061624521 --- /dev/null +++ b/app/views/shared/icons/_icon_status_warning_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M9.42857143,5.5 C9.42857143,5.02857143 9.74285714,4.71428571 10.2142857,4.71428571 L11.7857143,4.71428571 C12.2571429,4.71428571 12.5714286,5.02857143 12.5714286,5.5 L12.5714286,11.7857143 C12.5714286,12.2571429 12.2571429,12.5714286 11.7857143,12.5714286 L10.2142857,12.5714286 C9.74285714,12.5714286 9.42857143,12.2571429 9.42857143,11.7857143 L9.42857143,5.5 M9.42857143,14.9285714 C9.42857143,14.4571429 9.74285714,14.1428571 10.2142857,14.1428571 L11.7857143,14.1428571 C12.2571429,14.1428571 12.5714286,14.4571429 12.5714286,14.9285714 L12.5714286,16.5 C12.5714286,16.9714286 12.2571429,17.2857143 11.7857143,17.2857143 L10.2142857,17.2857143 C9.74285714,17.2857143 9.42857143,16.9714286 9.42857143,16.5 L9.42857143,14.9285714" id="Shape"></path></svg> 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/icons/_icon_terminal.svg b/app/views/shared/icons/_icon_terminal.svg new file mode 100644 index 00000000000..c80f44c3edf --- /dev/null +++ b/app/views/shared/icons/_icon_terminal.svg @@ -0,0 +1 @@ +<svg width="19" height="14" viewBox="0 0 19 14" xmlns="http://www.w3.org/2000/svg"><rect fill="#848484" x="7.2" y="9.25" width="6.46" height="1.5" rx=".5"/><path d="M5.851 7.016L3.81 9.103a.503.503 0 0 0 .017.709l.35.334c.207.198.524.191.717-.006l2.687-2.748a.493.493 0 0 0 .137-.376.493.493 0 0 0-.137-.376L4.894 3.892a.507.507 0 0 0-.717-.006l-.35.334a.503.503 0 0 0-.017.709L5.85 7.016z"/><path d="M1.25 11.497c0 .691.562 1.253 1.253 1.253h13.994c.694 0 1.253-.56 1.253-1.253V2.503c0-.691-.562-1.253-1.253-1.253H2.503c-.694 0-1.253.56-1.253 1.253v8.994zM2.503 0h13.994A2.504 2.504 0 0 1 19 2.503v8.994A2.501 2.501 0 0 1 16.497 14H2.503A2.504 2.504 0 0 1 0 11.497V2.503A2.501 2.501 0 0 1 2.503 0z"/></svg> diff --git a/app/views/shared/icons/_mattermost_logo.svg.erb b/app/views/shared/icons/_mattermost_logo.svg.erb new file mode 100644 index 00000000000..83fbd1a407d --- /dev/null +++ b/app/views/shared/icons/_mattermost_logo.svg.erb @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="<%= size %>" height="<%= size %>" version="1" viewBox="0 0 501 501"><path d="M236 .7C137.7 7.5 54 68.2 18.2 158.5c-32 81-19.6 172.8 33 242.5 39.8 53 97.2 87 164.3 97 16.5 2.7 48 3.2 63.5 1.2 48.7-6.3 92.2-24.6 129-54.2 13-10.5 33-31.2 42.2-43.7 26.4-35.5 42.8-75.8 49-120.3 1.6-12.3 1.6-48.7 0-61-4-28.3-12-54.8-24.2-79.5-12.8-26-26.5-45.3-46.8-65.8C417.8 64 400.2 49 398.4 49c-.6 0-.4 10.5.3 26l1.3 26 7 8.7c19 23.7 32.8 53.5 38.2 83 2.5 14 3 43 1 55.8-4.5 27.8-15.2 54-31 76.5-8.6 12.2-28 31.6-40.2 40.2-24 17-50 27.6-80 33-10 1.8-49 1.8-59 0-43-7.7-78.8-26-107.2-54.8-29.3-29.7-46.5-64-52.4-104.4-2-14-1.5-42 1-55C90 121.4 132 72 192 49.7c8-3 18.4-5.8 29.5-8.2 1.7-.4 34.4-38 35.3-40.6.3-1-10.2-1-20.8-.4z"/><path d="M322.2 24.6c-1.3.8-8.4 9.3-16 18.7-7.4 9.5-22.4 28-33.2 41.2-51 62.2-66 81.6-70.6 91-6 12-8.4 21-9 33-1.2 19.8 5 36 19 50C222 268 230 273 243 277.2c9 3 10.4 3.2 24 3.2 13.8 0 15 0 22.6-3 23.2-9 39-28.4 45-55.7 2-8.2 2-28.7.4-79.7l-2-72c-1-36.8-1.4-41.8-3-44-2-3-4.8-3.6-7.8-1.4z"/></svg> diff --git a/app/views/shared/icons/_scroll_down.svg b/app/views/shared/icons/_scroll_down.svg new file mode 100644 index 00000000000..acf22ac9314 --- /dev/null +++ b/app/views/shared/icons/_scroll_down.svg @@ -0,0 +1,3 @@ +<svg width="16" height="33" class="gitlab-icon-scroll-down" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg"> + <path fill="#ffffff" d="M1.385 5.534v12.47a4.145 4.145 0 0 0 4.144 4.15h4.942a4.151 4.151 0 0 0 4.144-4.15V5.535a4.145 4.145 0 0 0-4.144-4.15H5.53a4.151 4.151 0 0 0-4.144 4.15zM8.88 30.27v-4.351a.688.688 0 0 0-.69-.688.687.687 0 0 0-.69.688v4.334l-1.345-1.346a.69.69 0 0 0-.976.976l2.526 2.526a.685.685 0 0 0 .494.2.685.685 0 0 0 .493-.2l2.526-2.526a.69.69 0 1 0-.976-.976L8.88 30.27zM0 5.534A5.536 5.536 0 0 1 5.529 0h4.942A5.53 5.53 0 0 1 16 5.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 18.005V5.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0V6.544z" fill-rule="evenodd"/> +</svg> diff --git a/app/views/shared/icons/_scroll_down_hover_active.svg b/app/views/shared/icons/_scroll_down_hover_active.svg new file mode 100644 index 00000000000..262576acf54 --- /dev/null +++ b/app/views/shared/icons/_scroll_down_hover_active.svg @@ -0,0 +1,3 @@ +<svg width="16" height="33" class="gitlab-icon-scroll-down-hover" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg"> + <path fill="#ffffff" d="M8.88 30.27v-4.351a.688.688 0 0 0-.69-.688.687.687 0 0 0-.69.688v4.334l-1.345-1.346a.69.69 0 0 0-.976.976l2.526 2.526a.685.685 0 0 0 .494.2.685.685 0 0 0 .493-.2l2.526-2.526a.69.69 0 1 0-.976-.976L8.88 30.27zM0 5.534A5.536 5.536 0 0 1 5.529 0h4.942A5.53 5.53 0 0 1 16 5.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 18.005V5.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0V6.544z" fill-rule="evenodd"/> +</svg> diff --git a/app/views/shared/icons/_scroll_up.svg b/app/views/shared/icons/_scroll_up.svg new file mode 100644 index 00000000000..f11288fd59c --- /dev/null +++ b/app/views/shared/icons/_scroll_up.svg @@ -0,0 +1,3 @@ +<svg width="16" height="33" class="gitlab-icon-scroll-up" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg"> + <path fill="#ffffff" d="M1.385 14.534v12.47a4.145 4.145 0 0 0 4.144 4.15h4.942a4.151 4.151 0 0 0 4.144-4.15v-12.47a4.145 4.145 0 0 0-4.144-4.15H5.53a4.151 4.151 0 0 0-4.144 4.15zM8.88 2.609V6.96a.688.688 0 0 1-.69.688.687.687 0 0 1-.69-.688V2.627L6.155 3.972a.69.69 0 0 1-.976-.976L7.705.47a.685.685 0 0 1 .494-.2.685.685 0 0 1 .493.2l2.526 2.526a.69.69 0 1 1-.976.976L8.88 2.609zM0 14.534A5.536 5.536 0 0 1 5.529 9h4.942A5.53 5.53 0 0 1 16 14.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 27.005V14.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0v-2.143z" fill-rule="evenodd"/> +</svg> diff --git a/app/views/shared/icons/_scroll_up_hover_active.svg b/app/views/shared/icons/_scroll_up_hover_active.svg new file mode 100644 index 00000000000..4658dbb1bb7 --- /dev/null +++ b/app/views/shared/icons/_scroll_up_hover_active.svg @@ -0,0 +1,3 @@ +<svg width="16" height="33" class="gitlab-icon-scroll-up-hover" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg"> + <path fill="#ffffff" d="M8.88 2.646l1.362 1.362a.69.69 0 0 0 .976-.976L8.692.507A.685.685 0 0 0 8.2.306a.685.685 0 0 0-.494.2L5.179 3.033a.69.69 0 1 0 .976.976L7.5 2.663v4.179c0 .38.306.688.69.688.381 0 .69-.306.69-.688V2.646zM0 14.534A5.536 5.536 0 0 1 5.529 9h4.942A5.53 5.53 0 0 1 16 14.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 27.005V14.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0v-2.143z" fill-rule="evenodd"/> +</svg> diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index ed93857e6d4..b42eaabb111 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -29,20 +29,20 @@ .filter-item.inline.labels-filter = render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" } - - if issuable_filters_present + - if issuable_filter_present? .filter-item.inline.reset-filters - %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters + %a{ href: page_filter_path(without: issuable_filter_params) } Reset filters .pull-right - if boards_page - #js-boards-seach.issue-boards-search + #js-boards-search.issue-boards-search %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } - if can?(current_user, :admin_list, @project) .dropdown.pull-right %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } - Create new list + Add list .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable - = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" } + = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } - if can?(current_user, :admin_label, @project) = render partial: "shared/issuable/label_page_create" = dropdown_loading @@ -56,9 +56,9 @@ = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do %ul %li - %a{href: "#", data: {id: "reopen"}} Open + %a{ href: "#", data: { id: "reopen" } } Open %li - %a{href: "#", data: {id: "close"}} Closed + %a{ href: "#", data: {id: "close" } } Closed .filter-item.inline = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) @@ -70,9 +70,9 @@ = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do %ul %li - %a{href: "#", data: {id: "subscribe"}} Subscribe + %a{ href: "#", data: { id: "subscribe" } } Subscribe %li - %a{href: "#", data: {id: "unsubscribe"}} Unsubscribe + %a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe = hidden_field_tag 'update[issuable_ids]', [] = hidden_field_tag :state_event, params[:state_event] diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 3176af9c19b..c0e8a498316 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -1,3 +1,5 @@ +- form = local_assigns.fetch(:f) +- commits = local_assigns[:commits] - project = @target_project || @project = form_errors(issuable) @@ -10,91 +12,22 @@ and make sure your changes will not unintentionally remove theirs .form-group - = f.label :title, class: 'control-label' + = form.label :title, class: 'control-label' = render 'shared/issuable/form/template_selector', issuable: issuable + = render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?) - %div{ class: issuable_templates(issuable).any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' } - = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off', - class: 'form-control pad', required: true += render 'shared/issuable/form/description', issuable: issuable, form: form - - if issuable.is_a?(MergeRequest) - %p.help-block - .js-wip-explanation - %a.js-toggle-wip{href: "", tabindex: -1} - Remove the - %code WIP: - prefix from the title - to allow this - %strong Work In Progress - merge request to be merged when it's ready. - .js-no-wip-explanation - %a.js-toggle-wip{href: "", tabindex: -1} - Start the title with - %code WIP: - to prevent a - %strong Work In Progress - merge request from being merged before it's ready. - - - if can_add_template?(issuable) - %p.help-block - Add - = link_to "description templates", help_page_path('user/project/description_templates'), tabindex: -1 - to help your contributors communicate effectively! - -.form-group.detail-page-description - = f.label :description, 'Description', class: 'control-label' - .col-sm-10 - - = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: f, attr: :description, - classes: 'note-textarea', - placeholder: "Write a comment or drag your files here...", - supports_slash_commands: !issuable.persisted? - = render 'projects/notes/hints', supports_slash_commands: !issuable.persisted? - .clearfix - .error-alert - -- if issuable.is_a?(Issue) +- if issuable.respond_to?(:confidential) .form-group .col-sm-offset-2.col-sm-10 .checkbox - = f.label :confidential do - = f.check_box :confidential + = form.label :confidential do + = form.check_box :confidential This issue is confidential and should only be visible to team members with at least Reporter access. -- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) - - has_due_date = issuable.has_attribute?(:due_date) - %hr - .row - %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") } - .form-group.issue-assignee - = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" - .col-sm-10{ class: ("col-lg-8" if has_due_date) } - .issuable-form-select-holder - - if issuable.assignee_id - = f.hidden_field :assignee_id - = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", - placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) - .form-group.issue-milestone - = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" - .col-sm-10{ class: ("col-lg-8" if has_due_date) } - .issuable-form-select-holder - = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" - .form-group - - has_labels = @labels && @labels.any? - = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" - = f.hidden_field :label_ids, multiple: true, value: '' - .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } - .issuable-form-select-holder - = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label" - - if has_due_date - .col-lg-6 - .form-group - = f.label :due_date, "Due date", class: "control-label" - .col-sm-10 - .issuable-form-select-holder - = f.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" += render 'shared/issuable/form/metadata', issuable: issuable, form: form - if issuable.can_move?(current_user) %hr @@ -108,37 +41,29 @@ title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' } = icon('question-circle') -- if issuable.is_a?(MergeRequest) && !issuable.closed_without_fork? - %hr - - if @merge_request.new_record? - .form-group - = f.label :source_branch, class: 'control-label' - .col-sm-10 - .issuable-form-select-holder - = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) += render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form + +- if @merge_request_for_resolving_discussions .form-group - = f.label :target_branch, class: 'control-label' - .col-sm-10 - .issuable-form-select-holder - = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} }) - - if @merge_request.new_record? - - = link_to 'Change branches', mr_change_branches_path(@merge_request) - - if @merge_request.can_remove_source_branch?(current_user) - .form-group - .col-sm-10.col-sm-offset-2 - .checkbox - = label_tag 'merge_request[force_remove_source_branch]' do - = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil - = check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch? - Remove source branch when merge request is accepted. + .col-sm-10.col-sm-offset-2 + - if @merge_request_for_resolving_discussions.discussions_can_be_resolved_by?(current_user) + = icon('exclamation-triangle') + Creating this issue will mark all discussions in + = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions) + as resolved. + = hidden_field_tag 'merge_request_for_resolving_discussions', @merge_request_for_resolving_discussions.iid + - else + = icon('exclamation-triangle') + You can't automatically mark all discussions in + = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions) + as resolved. Ask someone with sufficient rights to resolve the them. - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) -.row-content-block{class: (is_footer ? "footer-block" : "middle-block")} +.row-content-block{ class: (is_footer ? "footer-block" : "middle-block") } - if issuable.new_record? - = f.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' + = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' - else - = f.submit 'Save changes', class: 'btn btn-save' + = form.submit 'Save changes', class: 'btn btn-save' - if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project)) .inline.prepend-left-10 @@ -151,8 +76,8 @@ - else .pull-right - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project) - = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" }, + = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" }, method: :delete, class: 'btn btn-danger btn-grouped' = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' -= f.hidden_field :lock_version += form.hidden_field :lock_version diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 1d778bc88de..93c7fa0c7d6 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -19,10 +19,10 @@ = hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : label.try(:title), id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data} + %button.dropdown-menu-toggle.js-label-select.js-multiselect{ class: classes.join(' '), type: "button", data: dropdown_data } %span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) } = multi_label_name(selected, "Labels") - = icon('caret-down') + = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable = render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create } - if show_create && project && can?(current_user, :admin_label, project) diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml index 3bc57d3d2ac..bd66f39fa59 100644 --- a/app/views/shared/issuable/_label_page_create.html.haml +++ b/app/views/shared/issuable/_label_page_create.html.haml @@ -9,7 +9,7 @@   .dropdown-label-color-input .dropdown-label-color-preview.js-dropdown-label-color-preview - %input#new_label_color.default-dropdown-input{ type: "text" } + %input#new_label_color.default-dropdown-input{ type: "text", placeholder: "Assign custom color like #FF0000" } .clearfix %button.btn.btn-primary.pull-left.js-new-label-btn{ type: "button" } Create diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml index c0dc63be2bf..9a8529c6cbb 100644 --- a/app/views/shared/issuable/_label_page_default.html.haml +++ b/app/views/shared/issuable/_label_page_default.html.haml @@ -1,17 +1,15 @@ - title = local_assigns.fetch(:title, 'Assign labels') - show_create = local_assigns.fetch(:show_create, true) - show_footer = local_assigns.fetch(:show_footer, true) -- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels') +- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search') - show_boards_content = local_assigns.fetch(:show_boards_content, false) .dropdown-page-one = dropdown_title(title) - if show_boards_content .issue-board-dropdown-content %p - Each label that exists in your issue tracker can have its own dedicated - list. Select a label below to add a list to your Board and it will - automatically be populated with issues that have that label. To create - a list for a label that doesn't exist yet, simply create the label below. + Create lists from the labels you use in your project. Issues with that + label will automatically be added to the list. = dropdown_filter(filter_placeholder) = dropdown_content - if @project && show_footer @@ -19,7 +17,7 @@ %ul.dropdown-footer-list - if can?(current_user, :admin_label, @project) %li - %a.dropdown-toggle-page{href: "#"} + %a.dropdown-toggle-page{ href: "#" } Create new label %li = link_to namespace_project_labels_path(@project.namespace, @project), :"data-is-link" => true do diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index 40fe53e6a8d..415361f8fbf 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -3,7 +3,7 @@ - show_menu_above = show_menu_above || false - selected_text = selected.try(:title) || params[:milestone_title] - dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone") -- if selected.present? +- if selected.present? || params[:milestone_title].present? = hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id) = dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index 5527a2f889a..1154316c03f 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -3,23 +3,23 @@ - issuables = @issues || @merge_requests %ul.nav-links.issues-state-filters - %li{class: ("active" if params[:state] == 'opened')} - = link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do + %li{ class: ("active" if params[:state] == 'opened') }> + = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened." do #{issuables_state_counter_text(type, :opened)} - if type == :merge_requests - %li{class: ("active" if params[:state] == 'merged')} - = link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do + %li{ class: ("active" if params[:state] == 'merged') }> + = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.' do #{issuables_state_counter_text(type, :merged)} - %li{class: ("active" if params[:state] == 'closed')} - = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do + %li{ class: ("active" if params[:state] == 'closed') }> + = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.' do #{issuables_state_counter_text(type, :closed)} - else - %li{class: ("active" if params[:state] == 'closed')} - = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do + %li{ class: ("active" if params[:state] == 'closed') }> + = link_to page_filter_path(state: 'closed', label: true), id: 'state-all', title: 'Filter by issues that are currently closed.' do #{issuables_state_counter_text(type, :closed)} - %li{class: ("active" if params[:state] == 'all')} - = link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do + %li{ class: ("active" if params[:state] == 'all') }> + = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}." do #{issuables_state_counter_text(type, :all)} diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml index 33a9a494857..171da899937 100644 --- a/app/views/shared/issuable/_participants.html.haml +++ b/app/views/shared/issuable/_participants.html.haml @@ -13,8 +13,8 @@ .participants-author.js-participants-author = link_to_member(@project, participant, name: false, size: 24) - if participants_extra > 0 - %div.participants-more - %a.js-participants-more{href: "#", data: {original_text: "+ #{participants_size - 7} more", less_text: "- show less"}} + .participants-more + %a.js-participants-more{ href: "#", data: { original_text: "+ #{participants_size - 7} more", less_text: "- show less" } } + #{participants_extra} more :javascript IssuableContext.prototype.PARTICIPANTS_ROW_COUNT = #{participants_row}; diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml new file mode 100644 index 00000000000..e9644ca0f12 --- /dev/null +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -0,0 +1,137 @@ +- type = local_assigns.fetch(:type) + +.issues-filters + .issues-details-filters.row-content-block.second-block.filtered-search-block + = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do + - if params[:search].present? + = hidden_field_tag :search, params[:search] + - if @bulk_edit + .check-all-holder + = check_box_tag "check_all_issues", nil, false, + class: "check_all_issues left" + .issues-other-filters.filtered-search-container + .filtered-search-input-container + %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]) } + = icon('filter') + %button.clear-search.hidden{ type: 'button' } + = icon('times') + #js-dropdown-hint.dropdown-menu.hint-dropdown + %ul{ 'data-dropdown' => true } + %li.filter-dropdown-item{ 'data-action' => 'submit' } + %button.btn.btn-link + = icon('search') + %span + Keep typing and press Enter + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %li.filter-dropdown-item + %button.btn.btn-link + -# Encapsulate static class name `{{icon}}` inside #{} to bypass + -# haml lint's ClassAttributeWithStaticValue + %i.fa{ class: "#{'{{icon}}'}" } + %span.js-filter-hint + {{hint}} + %span.js-filter-tag.dropdown-light-content + {{tag}} + #js-dropdown-author.dropdown-menu + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %li.filter-dropdown-item + %button.btn.btn-link.dropdown-user + %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } + .dropdown-user-details + %span + {{name}} + %span.dropdown-light-content + @{{username}} + #js-dropdown-assignee.dropdown-menu + %ul{ 'data-dropdown' => true } + %li.filter-dropdown-item{ 'data-value' => 'none' } + %button.btn.btn-link + No Assignee + - if current_user + %li.filter-dropdown-item{ 'data-value' => current_user.to_reference } + %button.btn.btn-link + Assigned to me + %li.divider + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %li.filter-dropdown-item + %button.btn.btn-link.dropdown-user + %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } + .dropdown-user-details + %span + {{name}} + %span.dropdown-light-content + @{{username}} + #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } + %ul{ 'data-dropdown' => true } + %li.filter-dropdown-item{ 'data-value' => 'none' } + %button.btn.btn-link + No Milestone + %li.filter-dropdown-item{ 'data-value' => 'upcoming' } + %button.btn.btn-link + Upcoming + %li.divider + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %li.filter-dropdown-item + %button.btn.btn-link.js-data-value + {{title}} + #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } + %ul{ 'data-dropdown' => true } + %li.filter-dropdown-item{ 'data-value' => 'none' } + %button.btn.btn-link + No Label + %li.divider + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %li.filter-dropdown-item + %button.btn.btn-link + %span.dropdown-label-box{ style: 'background: {{color}}' } + %span.label-title.js-data-value + {{title}} + .pull-right + = render 'shared/sort_dropdown' + + - if @bulk_edit + .issues_bulk_update.hide + = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do + .filter-item.inline + = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do + %ul + %li + %a{ href: "#", data: { id: "reopen" } } Open + %li + %a{ href: "#", data: { id: "close" } } Closed + .filter-item.inline + = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) + .filter-item.inline + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) + .filter-item.inline.labels-filter + = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } + .filter-item.inline + = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do + %ul + %li + %a{ href: "#", data: { id: "subscribe" } } Subscribe + %li + %a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe + + = hidden_field_tag 'update[issuable_ids]', [] + = hidden_field_tag :state_event, params[:state_event] + .filter-item.inline + = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save" + +:javascript + new UsersSelect(); + new LabelsSelect(); + new MilestoneSelect(); + new IssueStatusSelect(); + new SubscriptionSelect(); + + $(document).off('page:restore').on('page:restore', function (event) { + if (gl.FilteredSearchManager) { + new gl.FilteredSearchManager(); + } + Issuable.init(); + new gl.IssuableBulkActions({ + prefixId: 'issue_', + }); + }); diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 7363ead09ff..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 @@ -9,17 +11,17 @@ %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", aria: { label: "Toggle sidebar" } } = sidebar_gutter_toggle_icon - if current_user - %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add Todo" : "Mark Done") }, data: { todo_text: "Add Todo", mark_text: "Mark Done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } } + %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add todo" : "Mark done") }, data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } } %span.js-issuable-todo-text - if todo - Mark Done + Mark done - else - Add Todo + Add todo = icon('spin spinner', class: 'hidden js-issuable-todo-loading') - = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| + = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f| .block.assignee - .sidebar-collapsed-icon.sidebar-collapsed-user{data: {toggle: "tooltip", placement: "left", container: "body"}, title: (issuable.assignee.name if issuable.assignee)} + .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee.name if issuable.assignee) } - if issuable.assignee = link_to_member(@project, issuable.assignee, size: 24) - else @@ -54,7 +56,7 @@ = icon('clock-o') %span - if issuable.milestone - %span.has-tooltip{title: milestone_remaining_days(issuable.milestone), data: {container: 'body', html: 1, placement: 'left'}} + %span.has-tooltip{ title: milestone_remaining_days(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } } = issuable.milestone.title - else None @@ -72,7 +74,13 @@ .selectbox.hide-collapsed = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }}) - + - if issuable.has_attribute?(:time_estimate) + #issuable-time-tracker.block + %issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'stopwatch-svg' => custom_icon('icon_stopwatch'), 'docs-url' => help_page_path('workflow/time_tracking.md') } + // Fallback while content is loading + .title.hide-collapsed + Time tracking + = icon('spinner spin') - if issuable.has_attribute?(:due_date) .block.due_date .sidebar-collapsed-icon @@ -97,7 +105,7 @@ remove due date - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .selectbox.hide-collapsed - = f.hidden_field :due_date, value: issuable.due_date + = f.hidden_field :due_date, value: issuable.due_date.try(:strftime, 'yy-mm-dd') .dropdown %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } } %span.dropdown-toggle-text Due date @@ -129,8 +137,8 @@ - selected_labels.each do |label| = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} - %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?)} + %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project) } } + %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } = multi_label_name(selected_labels, "Labels") = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable @@ -140,36 +148,33 @@ = render "shared/issuable/participants", participants: issuable.participants(current_user) - if current_user - - subscribed = issuable.subscribed?(current_user) - .block.light.subscription{data: {url: toggle_subscription_path(issuable)}} + - subscribed = issuable.subscribed?(current_user, @project) + .block.light.subscription{ data: { url: toggle_subscription_path(issuable) } } .sidebar-collapsed-icon = icon('rss') - .title.hide-collapsed + %span.issuable-header-text.hide-collapsed.pull-left Notifications - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' - %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } + %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } %span= subscribed ? 'Unsubscribe' : 'Subscribe' - .subscription-status.hide-collapsed{data: {status: subscribtion_status}} - .unsubscribed{class: ( 'hidden' if subscribed )} - You're not receiving notifications from this thread. - .subscribed{class: ( 'hidden' unless subscribed )} - You're receiving notifications because you're subscribed to this thread. - project_ref = cross_project_reference(@project, issuable) .block.project-reference .sidebar-collapsed-icon.dont-change-state - = clipboard_button(clipboard_text: project_ref) + = clipboard_button(clipboard_text: project_ref, title: "Copy reference to clipboard", placement: "left") .cross-project-reference.hide-collapsed %span Reference: - %cite{title: project_ref} + %cite{ title: project_ref } = project_ref - = clipboard_button(clipboard_text: project_ref) + = clipboard_button(clipboard_text: project_ref, title: "Copy reference to clipboard", placement: "left") :javascript + gl.IssuableResource = new gl.SubbableResource('#{issuable_json_path(issuable)}'); + new gl.IssuableTimeTracking("#{escape_javascript(serialize_issuable(issuable))}"); new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}'); new LabelsSelect(); new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); - new Subscription('.subscription') + gl.Subscription.bindAll('.subscription'); new gl.DueDateSelectors(); - sidebar = new Sidebar(); + window.sidebar = new Sidebar(); diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml new file mode 100644 index 00000000000..b757893ea04 --- /dev/null +++ b/app/views/shared/issuable/form/_branch_chooser.html.haml @@ -0,0 +1,30 @@ +- issuable = local_assigns.fetch(:issuable) +- form = local_assigns.fetch(:form) + +- return unless issuable.is_a?(MergeRequest) +- return if issuable.closed_without_fork? + +%hr +- if issuable.new_record? + .form-group + = form.label :source_branch, class: 'control-label' + .col-sm-10 + .issuable-form-select-holder + = form.select(:source_branch, [issuable.source_branch], {}, { class: 'source_branch select2 span2', disabled: true }) +.form-group + = form.label :target_branch, class: 'control-label' + .col-sm-10 + .issuable-form-select-holder + = form.select(:target_branch, issuable.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: issuable.new_record?, data: { placeholder: "Select branch" }}) + - if issuable.new_record? + + = link_to 'Change branches', mr_change_branches_path(issuable) + +- if issuable.can_remove_source_branch?(current_user) + .form-group + .col-sm-10.col-sm-offset-2 + .checkbox + = label_tag 'merge_request[force_remove_source_branch]' do + = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil + = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch? + Remove source branch when merge request is accepted. diff --git a/app/views/shared/issuable/form/_description.html.haml b/app/views/shared/issuable/form/_description.html.haml new file mode 100644 index 00000000000..dbace9ce401 --- /dev/null +++ b/app/views/shared/issuable/form/_description.html.haml @@ -0,0 +1,15 @@ +- issuable = local_assigns.fetch(:issuable) +- form = local_assigns.fetch(:form) + +.form-group.detail-page-description + = form.label :description, 'Description', class: 'control-label' + .col-sm-10 + + = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do + = render 'projects/zen', f: form, attr: :description, + classes: 'note-textarea', + placeholder: "Write a comment or drag your files here...", + supports_slash_commands: !issuable.persisted? + = render 'projects/notes/hints', supports_slash_commands: !issuable.persisted? + .clearfix + .error-alert diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml new file mode 100644 index 00000000000..a47085230b8 --- /dev/null +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -0,0 +1,38 @@ +- issuable = local_assigns.fetch(:issuable) + +- return unless can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) + +- has_due_date = issuable.has_attribute?(:due_date) +- has_labels = @labels && @labels.any? +- form = local_assigns.fetch(:form) + +%hr +.row + %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") } + .form-group.issue-assignee + = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" + .col-sm-10{ class: ("col-lg-8" if has_due_date) } + .issuable-form-select-holder + - if issuable.assignee_id + = form.hidden_field :assignee_id + = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", + placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) + .form-group.issue-milestone + = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" + .col-sm-10{ class: ("col-lg-8" if has_due_date) } + .issuable-form-select-holder + = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" + .form-group + - has_labels = @labels && @labels.any? + = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" + = form.hidden_field :label_ids, multiple: true, value: '' + .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } + .issuable-form-select-holder + = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label" + - if has_due_date + .col-lg-6 + .form-group + = form.label :due_date, "Due date", class: "control-label" + .col-sm-10 + .issuable-form-select-holder + = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml new file mode 100644 index 00000000000..64826d41d60 --- /dev/null +++ b/app/views/shared/issuable/form/_title.html.haml @@ -0,0 +1,36 @@ +- 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' + +%div{ class: div_class } + = form.text_field :title, required: true, maxlength: 255, autofocus: true, + autocomplete: 'off', class: 'form-control pad' + + - if issuable.respond_to?(:work_in_progress?) + %p.help-block + .js-wip-explanation + %a.js-toggle-wip{ href: '', tabindex: -1 } + Remove the + %code WIP: + prefix from the title + to allow this + %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: + to prevent a + %strong Work In Progress + merge request from being merged before it's ready. + + - if no_issuable_templates && can?(current_user, :push_code, issuable.project) + %p.help-block + Add + = link_to 'description templates', help_page_path('user/project/description_templates'), tabindex: -1 + to help your contributors communicate effectively! diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml index eff914398bb..fb795ad1c72 100644 --- a/app/views/shared/members/_access_request_buttons.html.haml +++ b/app/views/shared/members/_access_request_buttons.html.haml @@ -1,10 +1,17 @@ -- if can?(current_user, :request_access, source) - - if requester = source.requesters.find_by(user_id: current_user.id) +- model_name = source.model_name.to_s.downcase + +.project-action-button.inline + - if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id)) + = link_to "Leave #{model_name}", polymorphic_path([:leave, source, :members]), + method: :delete, + data: { confirm: leave_confirmation_message(source) }, + class: 'btn' + - elsif requester = source.requesters.find_by(user_id: current_user.id) = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]), method: :delete, data: { confirm: remove_member_message(requester) }, class: 'btn' - - else + - elsif source.request_access_enabled && can?(current_user, :request_access, source) = link_to 'Request Access', polymorphic_path([:request_access, source, :members]), method: :post, class: 'btn' diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml index 1c0346bbc78..81b5bc1de30 100644 --- a/app/views/shared/members/_group.html.haml +++ b/app/views/shared/members/_group.html.haml @@ -1,8 +1,9 @@ - group_link = local_assigns[:group_link] - group = group_link.group - can_admin_member = can?(current_user, :admin_project_member, @project) -%li.member.group_member{ id: "group_member_#{group_link.id}" } - %span{ class: "list-item-name" } +- dom_id = "group_member_#{group_link.id}" +%li.member.group_member{ id: dom_id } + %span.list-item-name = image_tag group_icon(group), class: "avatar s40", alt: '' %strong = link_to group.name, group_path(group) @@ -14,13 +15,28 @@ Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)} .controls.member-controls = form_tag namespace_project_group_link_path(@project.namespace, @project, group_link), method: :put, remote: true, class: 'form-horizontal js-edit-member-form' do - = select_tag 'group_link[group_access]', options_for_select(ProjectGroupLink.access_options, group_link.group_access), class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{group.id}", disabled: !can_admin_member + = hidden_field_tag "group_link[group_access]", group_link.group_access + .member-form-control.dropdown.append-right-5 + %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", + disabled: !can_admin_member, + data: { toggle: "dropdown", field_name: "group_link[group_access]" } } + %span.dropdown-toggle-text + = group_link.human_access + = icon("chevron-down") + .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable + = dropdown_title("Change permissions") + .dropdown-content + %ul + - Gitlab::Access.options.each do |role, role_id| + %li + = link_to role, "javascript:void(0)", + class: ("is-active" if group_link.group_access == role_id), + data: { id: role_id, el_id: dom_id } .prepend-left-5.clearable-input.member-form-control = text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{group.id}", disabled: !can_admin_member %i.clear-icon.js-clear-input - if can_admin_member = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), - remote: true, method: :delete, data: { confirm: "Are you sure you want to remove #{group.name}?" }, class: 'btn btn-remove prepend-left-10' do diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 432047a1c4e..659d4c905fc 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -20,8 +20,8 @@ %strong Blocked - if source.instance_of?(Group) && !@group - = link_to source, class: "member-group-link prepend-left-5" do - = "· #{source.name}" + · + = link_to source.name, source, class: "member-group-link" .hidden-xs.cgray - if member.request? @@ -45,12 +45,28 @@ = time_ago_with_tooltip(member.created_at) - if show_roles .controls.member-controls - - if show_controls + - if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project) - if user != current_user = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| - = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{member.id}", disabled: !can_admin_member + = f.hidden_field :access_level + .member-form-control.dropdown.append-right-5 + %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", + disabled: !can_admin_member, + data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } } + %span.dropdown-toggle-text + = member.human_access + = icon("chevron-down") + .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable + = dropdown_title("Change permissions") + .dropdown-content + %ul + - Gitlab::Access.options.each do |role, role_id| + %li + = link_to role, "javascript:void(0)", + class: ("is-active" if member.access_level == role_id), + data: { id: role_id, el_id: dom_id(member) } .prepend-left-5.clearable-input.member-form-control - = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member + = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member, data: { el_id: dom_id(member) } %i.clear-icon.js-clear-input - else %span.member-access-text= member.human_access diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml new file mode 100644 index 00000000000..bad0891f9f2 --- /dev/null +++ b/app/views/shared/members/_sort_dropdown.html.haml @@ -0,0 +1,9 @@ +.dropdown.inline.member-sort-dropdown + = dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' }) + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable + %li.dropdown-header + Sort by + - member_sort_options_hash.each do |value, title| + %li + = link_to filter_group_project_member_path(sort: value), class: ("is-active" if @sort == value) do + = title diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml new file mode 100644 index 00000000000..748b10a1298 --- /dev/null +++ b/app/views/shared/milestones/_form_dates.html.haml @@ -0,0 +1,15 @@ +.col-md-6 + .form-group + = f.label :start_date, "Start Date", class: "control-label" + .col-sm-10 + = f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date" + %a.inline.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date +.col-md-6 + .form-group + = f.label :due_date, "Due Date", class: "control-label" + .col-sm-10 + = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date" + %a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date + +:javascript + new gl.DueDateSelectors(); diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml index 9e1b0379428..28935c8b598 100644 --- a/app/views/shared/milestones/_issuable.html.haml +++ b/app/views/shared/milestones/_issuable.html.haml @@ -14,15 +14,15 @@ - if issuable.is_a?(Issue) = confidential_icon(issuable) = link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title - %div{class: 'issuable-detail'} + .issuable-detail = link_to [project.namespace.becomes(Namespace), project, issuable] do - %span{ class: '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 - render_colored_label(label) - %span{ class: "assignee-icon" } + %span.assignee-icon - if assignee = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }), class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml index 8619939dde7..31eb07ca666 100644 --- a/app/views/shared/milestones/_issuables.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -3,10 +3,12 @@ - panel_class = primary ? 'panel-primary' : 'panel-default' .panel{ class: panel_class } - .panel-heading - = title + .panel-heading.split + .left + = title - if show_counter - .pull-right= issuables.size + .right + = 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 3dccfb147bf..9e6a76e1ddb 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -1,7 +1,7 @@ - dashboard = local_assigns[:dashboard] - custom_dom_id = dom_id(@project ? milestone : milestone.milestones.first) -%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id } +%li{ class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id } .row .col-sm-6 %strong= link_to_gfm truncate(milestone.title, length: 100), milestone_path @@ -9,7 +9,7 @@ .pull-right.light #{milestone.percent_complete(current_user)}% complete .row .col-sm-6 - = link_to pluralize(milestone.issues_visible_to_user(current_user).size, 'Issue'), issues_path + = link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path · = link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path .col-sm-6= milestone_progress_bar(milestone) diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml index dee2472fa79..d27fba805a3 100644 --- a/app/views/shared/milestones/_summary.html.haml +++ b/app/views/shared/milestones/_summary.html.haml @@ -3,32 +3,40 @@ .context.prepend-top-default .milestone-summary %h4 Progress - %strong= milestone.issues_visible_to_user(current_user).size - issues: - %span.milestone-stat - %strong= milestone.issues_visible_to_user(current_user).opened.size - open and - %strong= milestone.issues_visible_to_user(current_user).closed.size - closed - %strong= milestone.merge_requests.size - merge requests: - %span.milestone-stat - %strong= milestone.merge_requests.opened.size - open and - %strong= milestone.merge_requests.merged.size - merged - %span.milestone-stat - %strong== #{milestone.percent_complete(current_user)}% - complete - %span.milestone-stat - %span.remaining-days= milestone_remaining_days(milestone) - %span.pull-right.tab-issues-buttons - - if project && can?(current_user, :create_issue, project) - = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn btn-grouped", title: "New Issue" do - New Issue - = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn btn-grouped" - %span.pull-right.tab-merge-requests-buttons.hidden - = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn btn-grouped" + .milestone-stats-and-buttons + .milestone-stats + %span.milestone-stat.with-drilldown + %strong= milestone.issues_visible_to_user(current_user).size + issues: + %span.milestone-stat + %strong= milestone.issues_visible_to_user(current_user).opened.size + open and + %strong= milestone.issues_visible_to_user(current_user).closed.size + closed + %span.milestone-stat.with-drilldown + %strong= milestone.merge_requests.size + merge requests: + %span.milestone-stat + %strong= milestone.merge_requests.opened.size + open and + %strong= milestone.merge_requests.merged.size + merged + %span.milestone-stat + %strong== #{milestone.percent_complete(current_user)}% + complete + - remaining_days = milestone_remaining_days(milestone) + - if remaining_days.present? + %span.milestone-stat + %span.remaining-days= remaining_days - = milestone_progress_bar(milestone) + .milestone-progress-buttons + %span.tab-issues-buttons + - if project && can?(current_user, :create_issue, project) + = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do + New Issue + = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn" + %span.tab-merge-requests-buttons.hidden + = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn" + + = milestone_progress_bar(milestone) diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index 2b6ce2d7e7a..c8f2319d95a 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -21,7 +21,7 @@ .tab-content.milestone-content .tab-pane.active#tab-issues - = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user), show_project_name: show_project_name, show_full_project_name: show_full_project_name + = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name .tab-pane#tab-merge-requests = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name .tab-pane#tab-participants diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index 548215243db..497446c1ef3 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -12,10 +12,10 @@ Open %span.identifier Milestone #{milestone.title} - - if milestone.expires_at + - if milestone.due_date || milestone.start_date %span.creator · - = milestone.expires_at + = milestone_date_range(milestone) - if group .pull-right - if can?(current_user, :admin_milestones, group) diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml index feaa5570c21..1d072c16b32 100644 --- a/app/views/shared/notifications/_button.html.haml +++ b/app/views/shared/notifications/_button.html.haml @@ -1,6 +1,5 @@ -- left_align = local_assigns[:left_align] - if notification_setting - .dropdown.notification-dropdown.pull-right + .js-notification-dropdown.notification-dropdown.project-action-button.dropdown.inline = form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f| = hidden_setting_source_input(notification_setting) = f.hidden_field :level, class: "notification_setting_level" @@ -19,7 +18,7 @@ = notification_title(notification_setting.level) = icon("caret-down") - = render "shared/notifications/notification_dropdown", notification_setting: notification_setting, left_align: left_align + = render "shared/notifications/notification_dropdown", notification_setting: notification_setting = content_for :scripts_body do = render "shared/notifications/custom_notifications", notification_setting: notification_setting diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml index a82fc95df84..b5c0a7fd6d4 100644 --- a/app/views/shared/notifications/_custom_notifications.html.haml +++ b/app/views/shared/notifications/_custom_notifications.html.haml @@ -18,7 +18,7 @@ %p Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out = succeed "." do - %a{ href: "http://docs.gitlab.com/ce/workflow/notifications.html", target: "_blank"} notification emails + %a{ href: "http://docs.gitlab.com/ce/workflow/notifications.html", target: "_blank" } notification emails .col-lg-8 - NotificationSetting::EMAIL_EVENTS.each_with_index do |event, index| - field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]" diff --git a/app/views/shared/notifications/_notification_dropdown.html.haml b/app/views/shared/notifications/_notification_dropdown.html.haml index d3258ee64cb..85ad74f9a39 100644 --- a/app/views/shared/notifications/_notification_dropdown.html.haml +++ b/app/views/shared/notifications/_notification_dropdown.html.haml @@ -1,5 +1,4 @@ -- left_align = local_assigns[:left_align] -%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting), ("dropdown-menu-align-right" unless left_align)] } +%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting)] } - NotificationSetting.levels.each_key do |level| - next if level == "custom" - next if level == "global" && notification_setting.source.nil? diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 264391fe84f..4a27965754d 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -25,7 +25,7 @@ %span = icon('star') = number_with_delimiter(project.star_count) - %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)} + %span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) } = visibility_level_icon(project.visibility_level, fw: true) .title diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index dcdba01aee9..ad5c0c2d8c8 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -1,6 +1,6 @@ - unless @snippet.content.empty? - if markup?(@snippet.file_name) - %textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}} + %textarea.markdown-snippet-copy.blob-content{ data: { blob_id: @snippet.id } } = @snippet.content .file-content.wiki - if gitlab_markdown?(@snippet.file_name) diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index d7506e07ff6..d084f5e9684 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -8,10 +8,6 @@ %span.creator authored = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') - - if @snippet.updated_at != @snippet.created_at - %span - = icon('edit', title: 'edited') - = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago') by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")} .snippet-actions @@ -20,5 +16,9 @@ - else = render "snippets/actions" -%h2.snippet-title.prepend-top-0.append-bottom-0 - = markdown_field(@snippet, :title) +.snippet-header + %h2.snippet-title.prepend-top-0.append-bottom-0 + = markdown_field(@snippet, :title) + + - if @snippet.updated_at != @snippet.created_at + = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago') diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index ea17bec8677..5d2d2317f22 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -1,17 +1,16 @@ +- link_project = local_assigns.fetch(:link_project, false) + %li.snippet-row = image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: '' .title = link_to reliable_snippet_path(snippet) do = snippet.title - - if snippet.private? - %span.label.label-gray.hidden-xs - = icon('lock') - private - %span.monospace.pull-right.hidden-xs - = snippet.file_name + - if snippet.file_name + %span.snippet-filename.monospace.hidden-xs + = snippet.file_name - %ul.controls.visible-xs + %ul.controls %li - note_count = snippet.notes.user.count = link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do @@ -22,11 +21,17 @@ = visibility_level_label(snippet.visibility_level) = visibility_level_icon(snippet.visibility_level, fw: false) - %small.pull-right.cgray.hidden-xs - - if snippet.project_id? - = link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project) - - .snippet-info.hidden-xs + .snippet-info + #{snippet.to_reference} · + authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom', html_class: 'snippet-created-ago')} + by = link_to user_snippets_path(snippet.author) do = snippet.author_name - authored #{time_ago_with_tooltip(snippet.created_at)} + - if link_project && snippet.project_id? + %span.hidden-xs + in + = link_to namespace_project_path(snippet.project.namespace, snippet.project) do + = snippet.project.name_with_namespace + + .pull-right.snippet-updated-at + %span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')} diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml new file mode 100644 index 00000000000..5074afb63a1 --- /dev/null +++ b/app/views/shared/tokens/_scopes_form.html.haml @@ -0,0 +1,9 @@ +- scopes = local_assigns.fetch(:scopes) +- prefix = local_assigns.fetch(:prefix) +- token = local_assigns.fetch(:token) + +- scopes.each do |scope| + %fieldset + = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}" + = label_tag "#{prefix}_scopes_#{scope}", scope + %span= "(#{t(scope, scope: [:doorkeeper, :scopes])})" diff --git a/app/views/shared/tokens/_scopes_list.html.haml b/app/views/shared/tokens/_scopes_list.html.haml new file mode 100644 index 00000000000..f99e905e95c --- /dev/null +++ b/app/views/shared/tokens/_scopes_list.html.haml @@ -0,0 +1,13 @@ +- token = local_assigns.fetch(:token) + +- return unless token.scopes.present? + +%tr + %td + Scopes + %td + %ul.scopes-list.append-bottom-0 + - token.scopes.each do |scope| + %li + %span.scope-name= scope + = "(#{t(scope, scope: [:doorkeeper, :scopes])})" diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 5d659eb83a9..13586a5a12a 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -1,6 +1,3 @@ -- page_title "Webhooks" -- context_title = @project ? 'project' : 'group' - .row.prepend-top-default .col-lg-3 %h4.prepend-top-0 diff --git a/app/views/sherlock/file_samples/show.html.haml b/app/views/sherlock/file_samples/show.html.haml index 94d4dd4fa7d..92151176fce 100644 --- a/app/views/sherlock/file_samples/show.html.haml +++ b/app/views/sherlock/file_samples/show.html.haml @@ -41,7 +41,7 @@ %th= t('sherlock.percent') %tbody - @file_sample.line_samples.each_with_index do |sample, index| - %tr{class: sample.majority_of?(@file_sample.duration) ? 'slow' : ''} + %tr{ class: sample.majority_of?(@file_sample.duration) ? 'slow' : '' } %td= index + 1 %td= sample.events %td diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml index 7073c0f4d90..5a447f791dc 100644 --- a/app/views/sherlock/queries/_general.html.haml +++ b/app/views/sherlock/queries/_general.html.haml @@ -26,7 +26,7 @@ .panel.panel-default .panel-heading .pull-right - %button.js-clipboard-trigger.btn.btn-xs{title: t('sherlock.copy_to_clipboard'), type: :button} + %button.js-clipboard-trigger.btn.btn-xs{ title: t('sherlock.copy_to_clipboard'), type: :button } %i.fa.fa-clipboard %pre.hidden = @query.formatted_query @@ -41,7 +41,7 @@ .panel.panel-default .panel-heading .pull-right - %button.js-clipboard-trigger.btn.btn-xs{title: t('sherlock.copy_to_clipboard'), type: :button} + %button.js-clipboard-trigger.btn.btn-xs{ title: t('sherlock.copy_to_clipboard'), type: :button } %i.fa.fa-clipboard %pre.hidden = @query.explain diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml index fc2863dca8e..c45da6ee9a4 100644 --- a/app/views/sherlock/queries/show.html.haml +++ b/app/views/sherlock/queries/show.html.haml @@ -3,10 +3,10 @@ %ul.nav-links %li.active - %a(href="#tab-general" data-toggle="tab") + %a{ href: "#tab-general", data: { toggle: "tab" } } = t('sherlock.general') %li - %a(href="#tab-backtrace" data-toggle="tab") + %a{ href: "#tab-backtrace", data: { toggle: "tab" } } = t('sherlock.backtrace') .row-content-block diff --git a/app/views/sherlock/transactions/index.html.haml b/app/views/sherlock/transactions/index.html.haml index da969c02765..bc05659dfa8 100644 --- a/app/views/sherlock/transactions/index.html.haml +++ b/app/views/sherlock/transactions/index.html.haml @@ -28,7 +28,7 @@ %tr %td= trans.type %td - %span{title: trans.path} + %span{ title: trans.path } = truncate(trans.path, length: 70) %td = trans.duration.round(2) diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml index 8aa6b437d95..eab91e8fbe4 100644 --- a/app/views/sherlock/transactions/show.html.haml +++ b/app/views/sherlock/transactions/show.html.haml @@ -3,15 +3,15 @@ %ul.nav-links %li.active - %a(href="#tab-general" data-toggle="tab") + %a{ href: "#tab-general", data: { toggle: "tab" } } = t('sherlock.general') %li - %a(href="#tab-queries" data-toggle="tab") + %a{ href: "#tab-queries", data: { toggle: "tab" } } = t('sherlock.queries') %span.badge #{@transaction.queries.length} %li - %a(href="#tab-file-samples" data-toggle="tab") + %a{ href: "#tab-file-samples", data: { toggle: "tab" } } = t('sherlock.file_samples') %span.badge #{@transaction.file_samples.length} diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 1d0e549ed3d..95fc7198104 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -1,13 +1,13 @@ .hidden-xs - - if current_user - = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New snippet" do - New snippet - - if can?(current_user, :admin_personal_snippet, @snippet) - = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do - Delete - if can?(current_user, :update_personal_snippet, @snippet) - = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do + = link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do Edit + - if can?(current_user, :admin_personal_snippet, @snippet) + = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do + Delete + - if current_user + = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do + New snippet - if current_user .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml index 77b66ca74b6..ac3701233ad 100644 --- a/app/views/snippets/_snippets.html.haml +++ b/app/views/snippets/_snippets.html.haml @@ -1,8 +1,9 @@ - remote = local_assigns.fetch(:remote, false) +- link_project = local_assigns.fetch(:link_project, false) .snippets-list-holder %ul.content-list - = render partial: 'shared/snippets/snippet', collection: @snippets + = render partial: 'shared/snippets/snippet', collection: @snippets, locals: { link_project: link_project } - if @snippets.empty? %li .nothing-here-block Nothing here. diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml new file mode 100644 index 00000000000..2dda5fed647 --- /dev/null +++ b/app/views/snippets/_snippets_scope_menu.html.haml @@ -0,0 +1,31 @@ +- subject = local_assigns.fetch(:subject, current_user) +- include_private = local_assigns.fetch(:include_private, false) + +.nav-links.snippet-scope-menu + %li{ class: ("active" unless params[:scope]) } + = link_to subject_snippets_path(subject) do + All + %span.badge + - if include_private + = subject.snippets.count + - else + = subject.snippets.public_and_internal.count + + - if include_private + %li{ class: ("active" if params[:scope] == "are_private") } + = link_to subject_snippets_path(subject, scope: 'are_private') do + Private + %span.badge + = subject.snippets.are_private.count + + %li{ class: ("active" if params[:scope] == "are_internal") } + = link_to subject_snippets_path(subject, scope: 'are_internal') do + Internal + %span.badge + = subject.snippets.are_internal.count + + %li{ class: ("active" if params[:scope] == "are_public") } + = link_to subject_snippets_path(subject, scope: 'are_public') do + Public + %span.badge + = subject.snippets.are_public.count diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 27d7a6c5bb6..837a1a0cc8c 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -7,9 +7,9 @@ = blob_icon 0, @snippet.file_name = @snippet.file_name .file-actions - = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") + = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm") = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" = link_to 'Download', download_snippet_path(@snippet), class: "btn btn-sm" = render 'shared/snippets/blob' -= render 'award_emoji/awards_block', awardable: @snippet, inline: true
\ No newline at end of file += render 'award_emoji/awards_block', awardable: @snippet, inline: true diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml index 232ca26c1af..f878bece2fa 100644 --- a/app/views/u2f/_authenticate.html.haml +++ b/app/views/u2f/_authenticate.html.haml @@ -1,30 +1,21 @@ #js-authenticate-u2f +%a.btn.btn-block.btn-info#js-login-2fa-device{ href: '#' } Sign in via 2FA code %script#js-authenticate-u2f-not-supported{ type: "text/template" } %p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer). -%script#js-authenticate-u2f-setup{ type: "text/template" } - %div - %p Insert your security key (if you haven't already), and press the button below. - %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Sign in via U2F device - %script#js-authenticate-u2f-in-progress{ type: "text/template" } %p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now. %script#js-authenticate-u2f-error{ type: "text/template" } %div - %p <%= error_message %> - %a.btn.btn-warning#js-u2f-try-again Try again? + %p <%= error_message %> (error code: <%= error_code %>) + %a.btn.btn-block.btn-warning#js-u2f-try-again Try again? %script#js-authenticate-u2f-authenticated{ type: "text/template" } %div - %p We heard back from your U2F device. Click this button to authenticate with the GitLab server. - = form_tag(new_user_session_path, method: :post) do |f| + %p We heard back from your U2F device. You have been authenticated. + = form_tag(new_user_session_path, method: :post, id: 'js-login-u2f-form') do |f| - resource_params = params[resource_name].presence || params = hidden_field_tag 'user[remember_me]', resource_params.fetch(:remember_me, 0) = hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response" - = submit_tag "Authenticate via U2F Device", class: "btn btn-success" - -:javascript - var u2fAuthenticate = new U2FAuthenticate($("#js-authenticate-u2f"), gon.u2f); - u2fAuthenticate.start(); diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml index 8f7b42eb351..adc07bcba73 100644 --- a/app/views/u2f/_register.html.haml +++ b/app/views/u2f/_register.html.haml @@ -23,11 +23,11 @@ %script#js-register-u2f-error{ type: "text/template" } %div %p - %span <%= error_message %> + %span <%= error_message %> (error code: <%= error_code %>) %a.btn.btn-warning#js-u2f-try-again Try again? %script#js-register-u2f-registered{ type: "text/template" } - %div.row.append-bottom-10 + .row.append-bottom-10 .col-md-12 %p Your device was successfully set up! Give it a name and register it with the GitLab server. = form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml index 09ff8a76d27..6228245d8d0 100644 --- a/app/views/users/calendar.html.haml +++ b/app/views/users/calendar.html.haml @@ -6,4 +6,4 @@ new Calendar( #{@activity_dates.to_json}, '#{user_calendar_activities_path}' - );
\ No newline at end of file + ); diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 1e0752bd3c3..fb25eed4f37 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -18,11 +18,11 @@ - elsif current_user - if @user.abuse_report %button.btn.btn-danger{ title: 'Already reported for abuse', - data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }} + data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } } = icon('exclamation-circle') - else = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray', - title: 'Report abuse', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do = icon('exclamation-circle') - if current_user = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do @@ -101,12 +101,12 @@ .tab-content #activity.tab-pane .row-content-block.calender-block.white.second-block.hidden-xs - .user-calendar{data: {href: user_calendar_path}} + .user-calendar{ data: { href: user_calendar_path } } %h4.center.light %i.fa.fa-spinner.fa-spin .user-calendar-activities - .content_list{ data: {href: user_path} } + .content_list{ data: { href: user_path } } = spinner #groups.tab-pane diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb new file mode 100644 index 00000000000..2badd0680fb --- /dev/null +++ b/app/workers/authorized_projects_worker.rb @@ -0,0 +1,14 @@ +class AuthorizedProjectsWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + def self.bulk_perform_async(args_list) + Sidekiq::Client.push_bulk('class' => self, 'args' => args_list) + end + + def perform(user_id) + user = User.find_by(id: user_id) + + user.refresh_authorized_projects if user + end +end diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb new file mode 100644 index 00000000000..fa9e097e40a --- /dev/null +++ b/app/workers/build_queue_worker.rb @@ -0,0 +1,10 @@ +class BuildQueueWorker + include Sidekiq::Worker + include BuildQueue + + def perform(build_id) + Ci::Build.find_by(id: build_id).try do |build| + Ci::UpdateBuildQueueService.new.execute(build) + end + end +end diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb index e0ad5268664..e17add7421f 100644 --- a/app/workers/build_success_worker.rb +++ b/app/workers/build_success_worker.rb @@ -4,15 +4,13 @@ class BuildSuccessWorker def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| - create_deployment(build) + create_deployment(build) if build.has_environment? end end private def create_deployment(build) - return if build.environment.blank? - service = CreateDeploymentService.new( build.project, build.user, environment: build.environment, diff --git a/app/workers/delete_merged_branches_worker.rb b/app/workers/delete_merged_branches_worker.rb new file mode 100644 index 00000000000..f870da4ecfd --- /dev/null +++ b/app/workers/delete_merged_branches_worker.rb @@ -0,0 +1,20 @@ +class DeleteMergedBranchesWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + def perform(project_id, user_id) + begin + project = Project.find(project_id) + rescue ActiveRecord::RecordNotFound + return + end + + user = User.find(user_id) + + begin + DeleteMergedBranchesService.new(project, user).execute + rescue Gitlab::Access::AccessDeniedError + return + end + end +end diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb index c3e62bb88c0..926162b8c53 100644 --- a/app/workers/new_note_worker.rb +++ b/app/workers/new_note_worker.rb @@ -2,10 +2,14 @@ class NewNoteWorker include Sidekiq::Worker include DedicatedSidekiqQueue - def perform(note_id, note_params) - note = Note.find(note_id) - - NotificationService.new.new_note(note) - Notes::PostProcessService.new(note).execute + # Keep extra parameter to preserve backwards compatibility with + # old `NewNoteWorker` jobs (can remove later) + def perform(note_id, _params = {}) + if note = Note.find_by(id: note_id) + NotificationService.new.new_note(note) + Notes::PostProcessService.new(note).execute + else + Rails.logger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job") + end end end diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb index 34f6ef161fb..070943f1ecc 100644 --- a/app/workers/pipeline_metrics_worker.rb +++ b/app/workers/pipeline_metrics_worker.rb @@ -12,11 +12,11 @@ class PipelineMetricsWorker private def update_metrics_for_active_pipeline(pipeline) - metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil) + metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil, pipeline_id: pipeline.id) end def update_metrics_for_succeeded_pipeline(pipeline) - metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: pipeline.finished_at) + metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: pipeline.finished_at, pipeline_id: pipeline.id) end def metrics(pipeline) diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb index 2aa6fff24da..cc0eb708cf9 100644 --- a/app/workers/pipeline_success_worker.rb +++ b/app/workers/pipeline_success_worker.rb @@ -4,7 +4,7 @@ class PipelineSuccessWorker def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| - MergeRequests::MergeWhenBuildSucceedsService + MergeRequests::MergeWhenPipelineSucceedsService .new(pipeline.project, nil) .trigger(pipeline) end diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb index 071741fbacd..e9a5bd7f24e 100644 --- a/app/workers/process_commit_worker.rb +++ b/app/workers/process_commit_worker.rb @@ -10,9 +10,10 @@ class ProcessCommitWorker # project_id - The ID of the project this commit belongs to. # user_id - The ID of the user that pushed the commit. - # commit_sha - The SHA1 of the commit to process. + # commit_hash - Hash containing commit details to use for constructing a + # Commit object without having to use the Git repository. # default - The data was pushed to the default branch. - def perform(project_id, user_id, commit_sha, default = false) + def perform(project_id, user_id, commit_hash, default = false) project = Project.find_by(id: project_id) return unless project @@ -21,10 +22,7 @@ class ProcessCommitWorker return unless user - commit = find_commit(project, commit_sha) - - return unless commit - + commit = build_commit(project, commit_hash) author = commit.author || user process_commit_message(project, commit, user, author, default) @@ -59,9 +57,18 @@ class ProcessCommitWorker update_all(first_mentioned_in_commit_at: commit.committed_date) end - private + def build_commit(project, hash) + date_suffix = '_date' + + # When processing Sidekiq payloads various timestamps are stored as Strings. + # Commit in turn expects Time-like instances upon input, so we have to + # manually parse these values. + hash.each do |key, value| + if key.to_s.end_with?(date_suffix) && value.is_a?(String) + hash[key] = Time.parse(value) + end + end - def find_commit(project, sha) - project.commit(sha) + Commit.from_hash(hash, project) end end diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index 4dfa745fb50..8ff9d07860f 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -1,54 +1,39 @@ # Worker for updating any project specific caches. -# -# This worker runs at most once every 15 minutes per project. This is to ensure -# that multiple instances of jobs for this worker don't hammer the underlying -# storage engine as much. class ProjectCacheWorker include Sidekiq::Worker include DedicatedSidekiqQueue LEASE_TIMEOUT = 15.minutes.to_i - def self.lease_for(project_id) - Gitlab::ExclusiveLease. - new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT) - end - - # Overwrite Sidekiq's implementation so we only schedule when actually needed. - def self.perform_async(project_id) - # If a lease for this project is still being held there's no point in - # scheduling a new job. - super unless lease_for(project_id).exists? - end + # project_id - The ID of the project for which to flush the cache. + # files - An Array containing extra types of files to refresh such as + # `:readme` to flush the README and `:changelog` to flush the + # CHANGELOG. + # statistics - An Array containing columns from ProjectStatistics to + # refresh, if empty all columns will be refreshed + def perform(project_id, files = [], statistics = []) + project = Project.find_by(id: project_id) - def perform(project_id) - if try_obtain_lease_for(project_id) - Rails.logger. - info("Obtained ProjectCacheWorker lease for project #{project_id}") - else - Rails.logger. - info("Could not obtain ProjectCacheWorker lease for project #{project_id}") + return unless project && project.repository.exists? - return - end + update_statistics(project, statistics.map(&:to_sym)) - update_caches(project_id) + project.repository.refresh_method_caches(files.map(&:to_sym)) end - def update_caches(project_id) - project = Project.find(project_id) + def update_statistics(project, statistics = []) + return unless try_obtain_lease_for(project.id, :update_statistics) - return unless project.repository.exists? + Rails.logger.info("Updating statistics for project #{project.id}") - project.update_repository_size - project.update_commit_count - - if project.repository.root_ref - project.repository.build_cache - end + project.statistics.refresh!(only: statistics) end - def try_obtain_lease_for(project_id) - self.class.lease_for(project_id).try_obtain + private + + def try_obtain_lease_for(project_id, section) + Gitlab::ExclusiveLease. + new("project_cache_worker:#{project_id}:#{section}", timeout: LEASE_TIMEOUT). + try_obtain end end diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb new file mode 100644 index 00000000000..18b8daf4e1e --- /dev/null +++ b/app/workers/reactive_caching_worker.rb @@ -0,0 +1,15 @@ +class ReactiveCachingWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + def perform(class_name, id, *args) + klass = begin + Kernel.const_get(class_name) + rescue NameError + nil + end + return unless klass + + klass.find_by(id: id).try(:exclusively_update_reactive_cache!, *args) + end +end diff --git a/app/workers/use_key_worker.rb b/app/workers/use_key_worker.rb new file mode 100644 index 00000000000..c9d382cc5d6 --- /dev/null +++ b/app/workers/use_key_worker.rb @@ -0,0 +1,13 @@ +class UseKeyWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + def perform(key_id) + key = Key.find(key_id) + key.touch(:last_used_at) + rescue ActiveRecord::RecordNotFound + Rails.logger.error("UseKeyWorker: couldn't find key with ID=#{key_id}, skipping job") + + false + end +end |