diff options
Diffstat (limited to 'app')
289 files changed, 3055 insertions, 1941 deletions
diff --git a/app/assets/javascripts/LabelManager.js b/app/assets/javascripts/LabelManager.js deleted file mode 100644 index d4a4c7abaa1..00000000000 --- a/app/assets/javascripts/LabelManager.js +++ /dev/null @@ -1,115 +0,0 @@ -(function() { - this.LabelManager = (function() { - LabelManager.prototype.errorMessage = 'Unable to update label prioritization at this time'; - - function LabelManager(opts) { - // Defaults - var ref, ref1, ref2; - if (opts == null) { - opts = {}; - } - this.togglePriorityButton = (ref = opts.togglePriorityButton) != null ? ref : $('.js-toggle-priority'), this.prioritizedLabels = (ref1 = opts.prioritizedLabels) != null ? ref1 : $('.js-prioritized-labels'), this.otherLabels = (ref2 = opts.otherLabels) != null ? ref2 : $('.js-other-labels'); - this.prioritizedLabels.sortable({ - items: 'li', - placeholder: 'list-placeholder', - axis: 'y', - update: this.onPrioritySortUpdate.bind(this) - }); - this.bindEvents(); - } - - LabelManager.prototype.bindEvents = function() { - return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); - }; - - LabelManager.prototype.onTogglePriorityClick = function(e) { - var $btn, $label, $tooltip, _this, action; - e.preventDefault(); - _this = e.data; - $btn = $(e.currentTarget); - $label = $("#" + ($btn.data('domId'))); - action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; - // Make sure tooltip will hide - $tooltip = $("#" + ($btn.find('.has-tooltip:visible').attr('aria-describedby'))); - $tooltip.tooltip('destroy'); - return _this.toggleLabelPriority($label, action); - }; - - LabelManager.prototype.toggleLabelPriority = function($label, action, persistState) { - var $from, $target, _this, url, xhr; - if (persistState == null) { - persistState = true; - } - _this = this; - url = $label.find('.js-toggle-priority').data('url'); - $target = this.prioritizedLabels; - $from = this.otherLabels; - // Optimistic update - if (action === 'remove') { - $target = this.otherLabels; - $from = this.prioritizedLabels; - } - if ($from.find('li').length === 1) { - $from.find('.empty-message').removeClass('hidden'); - } - if (!$target.find('li').length) { - $target.find('.empty-message').addClass('hidden'); - } - $label.detach().appendTo($target); - // Return if we are not persisting state - if (!persistState) { - return; - } - if (action === 'remove') { - xhr = $.ajax({ - url: url, - type: 'DELETE' - }); - // Restore empty message - if (!$from.find('li').length) { - $from.find('.empty-message').removeClass('hidden'); - } - } else { - xhr = this.savePrioritySort($label, action); - } - return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); - }; - - LabelManager.prototype.onPrioritySortUpdate = function() { - var xhr; - xhr = this.savePrioritySort(); - return xhr.fail(function() { - return new Flash(this.errorMessage, 'alert'); - }); - }; - - LabelManager.prototype.savePrioritySort = function() { - return $.post({ - url: this.prioritizedLabels.data('url'), - data: { - label_ids: this.getSortedLabelsIds() - } - }); - }; - - LabelManager.prototype.rollbackLabelPosition = function($label, originalAction) { - var action; - action = originalAction === 'remove' ? 'add' : 'remove'; - this.toggleLabelPriority($label, action, false); - return new Flash(this.errorMessage, 'alert'); - }; - - LabelManager.prototype.getSortedLabelsIds = function() { - var sortedIds; - sortedIds = []; - this.prioritizedLabels.find('li').each(function() { - return sortedIds.push($(this).data('id')); - }); - return sortedIds; - }; - - return LabelManager; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 1cd2302111e..599331df3f5 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -5,7 +5,7 @@ namespacesPath: "/api/:version/namespaces.json", groupProjectsPath: "/api/:version/groups/:id/projects.json", projectsPath: "/api/:version/projects.json?simple=true", - labelsPath: "/api/:version/projects/:id/labels", + labelsPath: "/:namespace_path/:project_path/labels", licensePath: "/api/:version/licenses/:key", gitignorePath: "/api/:version/gitignores/:key", gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key", @@ -23,12 +23,13 @@ }, // Return groups list. Filtered by query // Only active groups retrieved - groups: function(query, skip_ldap, callback) { + groups: function(query, skip_ldap, skip_groups, callback) { var url = Api.buildUrl(Api.groupsPath); return $.ajax({ url: url, data: { search: query, + skip_groups: skip_groups, per_page: 20 }, dataType: "json" @@ -65,13 +66,14 @@ return callback(projects); }); }, - newLabel: function(project_id, data, callback) { + newLabel: function(namespace_path, project_path, data, callback) { var url = Api.buildUrl(Api.labelsPath) - .replace(':id', project_id); + .replace(':namespace_path', namespace_path) + .replace(':project_path', project_path); return $.ajax({ url: url, type: "POST", - data: data, + data: {'label': data}, dataType: "json" }).done(function(label) { return callback(label); diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js b/app/assets/javascripts/blob/blob_ci_yaml.js deleted file mode 100644 index 68758574967..00000000000 --- a/app/assets/javascripts/blob/blob_ci_yaml.js +++ /dev/null @@ -1,46 +0,0 @@ - -/*= require blob/template_selector */ - -(function() { - var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - hasProp = {}.hasOwnProperty; - - this.BlobCiYamlSelector = (function(superClass) { - extend(BlobCiYamlSelector, superClass); - - function BlobCiYamlSelector() { - return BlobCiYamlSelector.__super__.constructor.apply(this, arguments); - } - - BlobCiYamlSelector.prototype.requestFile = function(query) { - return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this)); - }; - - return BlobCiYamlSelector; - - })(TemplateSelector); - - this.BlobCiYamlSelectors = (function() { - function BlobCiYamlSelectors(opts) { - var ref; - this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-gitlab-ci-yml-selector'), this.editor = opts.editor; - this.$dropdowns.each((function(_this) { - return function(i, dropdown) { - var $dropdown; - $dropdown = $(dropdown); - return new BlobCiYamlSelector({ - pattern: /(.gitlab-ci.yml)/, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'), - dropdown: $dropdown, - editor: _this.editor - }); - }; - })(this)); - } - - return BlobCiYamlSelectors; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 new file mode 100644 index 00000000000..d6ea4f84f57 --- /dev/null +++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 @@ -0,0 +1,40 @@ +/*= require blob/template_selector */ +((global) => { + + class BlobCiYamlSelector extends gl.TemplateSelector { + requestFile(query) { + return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this)); + } + + requestFileSuccess(file) { + return super.requestFileSuccess(file); + } + } + + global.BlobCiYamlSelector = BlobCiYamlSelector; + + class BlobCiYamlSelectors { + constructor({ editor, $dropdowns } = {}) { + this.editor = editor; + this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector'); + this.initSelectors(); + } + + initSelectors() { + const editor = this.editor; + this.$dropdowns.each((i, dropdown) => { + const $dropdown = $(dropdown); + return new BlobCiYamlSelector({ + editor, + pattern: /(.gitlab-ci.yml)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'), + dropdown: $dropdown + }); + }); + } + } + + global.BlobCiYamlSelectors = BlobCiYamlSelectors; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js index 54a09e919f8..cd746b05cf6 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js @@ -18,6 +18,6 @@ return BlobGitignoreSelector; - })(TemplateSelector); + })(gl.TemplateSelector); }).call(this); diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js index 9a8ef08f4e5..2701df3e6de 100644 --- a/app/assets/javascripts/blob/blob_license_selector.js +++ b/app/assets/javascripts/blob/blob_license_selector.js @@ -23,6 +23,6 @@ return BlobLicenseSelector; - })(TemplateSelector); + })(gl.TemplateSelector); }).call(this); diff --git a/app/assets/javascripts/blob/blob_license_selectors.js b/app/assets/javascripts/blob/blob_license_selectors.js deleted file mode 100644 index 39237705e8d..00000000000 --- a/app/assets/javascripts/blob/blob_license_selectors.js +++ /dev/null @@ -1,25 +0,0 @@ -(function() { - this.BlobLicenseSelectors = (function() { - function BlobLicenseSelectors(opts) { - var ref; - this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-license-selector'), this.editor = opts.editor; - this.$dropdowns.each((function(_this) { - return function(i, dropdown) { - var $dropdown; - $dropdown = $(dropdown); - return new BlobLicenseSelector({ - pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-license-selector-wrap'), - dropdown: $dropdown, - editor: _this.editor - }); - }; - })(this)); - } - - return BlobLicenseSelectors; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js.es6 new file mode 100644 index 00000000000..153ed457559 --- /dev/null +++ b/app/assets/javascripts/blob/blob_license_selectors.js.es6 @@ -0,0 +1,21 @@ +((global) => { + class BlobLicenseSelectors { + constructor({ $dropdowns, editor }) { + this.$dropdowns = $('.js-license-selector'); + this.editor = editor; + this.$dropdowns.each((i, dropdown) => { + const $dropdown = $(dropdown); + return new BlobLicenseSelector({ + editor, + pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-license-selector-wrap'), + dropdown: $dropdown, + }); + }); + } + } + + global.BlobLicenseSelectors = BlobLicenseSelectors; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js deleted file mode 100644 index 95352164d76..00000000000 --- a/app/assets/javascripts/blob/template_selector.js +++ /dev/null @@ -1,100 +0,0 @@ -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.TemplateSelector = (function() { - function TemplateSelector(opts) { - var ref; - if (opts == null) { - opts = {}; - } - this.onClick = bind(this.onClick, this); - this.dropdown = opts.dropdown, this.data = opts.data, this.pattern = opts.pattern, this.wrapper = opts.wrapper, this.editor = opts.editor, this.fileEndpoint = opts.fileEndpoint, this.$input = (ref = opts.$input) != null ? ref : $('#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); - } - - TemplateSelector.prototype.buildDropdown = function() { - 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; - } - }); - }; - - TemplateSelector.prototype.bindEvents = function() { - return this.$input.on('keyup blur', (function(_this) { - return function(e) { - return _this.onFilenameUpdate(); - }; - })(this)); - }; - - TemplateSelector.prototype.toggleLabel = function(item) { - return item.name; - }; - - TemplateSelector.prototype.onFilenameUpdate = function() { - 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'); - }; - - TemplateSelector.prototype.onClick = function(item, el, e) { - e.preventDefault(); - return this.requestFile(item); - }; - - TemplateSelector.prototype.requestFile = function(item) { - // This `requestFile` method is an abstract method that should - // be added by all subclasses. - }; - - // To be implemented on the extending class - // e.g. - // Api.gitignoreText item.name, @requestFileSuccess.bind(@) - TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) { - this.editor.setValue(file.content, 1); - if (!skipFocus) this.editor.focus(); - - if (this.editor instanceof jQuery) { - this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); - } - }; - - TemplateSelector.prototype.startLoadingSpinner = function() { - this.dropdownIcon - .addClass('fa-spinner fa-spin') - .removeClass('fa-chevron-down'); - }; - - TemplateSelector.prototype.stopLoadingSpinner = function() { - this.dropdownIcon - .addClass('fa-chevron-down') - .removeClass('fa-spinner fa-spin'); - }; - - return TemplateSelector; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6 new file mode 100644 index 00000000000..4e309e480b0 --- /dev/null +++ b/app/assets/javascripts/blob/template_selector.js.es6 @@ -0,0 +1,102 @@ +((global) => { + class TemplateSelector { + constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) { + this.onClick = this.onClick.bind(this); + this.dropdown = dropdown; + this.data = data; + this.pattern = pattern; + this.wrapper = wrapper; + this.editor = editor; + this.fileEndpoint = fileEndpoint; + this.$input = $input || $('#file_name'); + this.dropdownIcon = $('.fa-chevron-down', this.dropdown); + this.buildDropdown(); + this.bindEvents(); + this.onFilenameUpdate(); + + this.autosizeUpdateEvent = document.createEvent('Event'); + this.autosizeUpdateEvent.initEvent('autosize:update', true, false); + } + + buildDropdown() { + return this.dropdown.glDropdown({ + data: this.data, + filterable: true, + selectable: true, + toggleLabel: this.toggleLabel, + search: { + fields: ['name'] + }, + clicked: this.onClick, + text: function(item) { + return item.name; + } + }); + } + + bindEvents() { + return this.$input.on('keyup blur', (e) => this.onFilenameUpdate()); + } + + toggleLabel(item) { + return item.name; + } + + onFilenameUpdate() { + var filenameMatches; + if (!this.$input.length) { + return; + } + filenameMatches = this.pattern.test(this.$input.val().trim()); + if (!filenameMatches) { + this.wrapper.addClass('hidden'); + return; + } + return this.wrapper.removeClass('hidden'); + } + + onClick(item, el, e) { + e.preventDefault(); + return this.requestFile(item); + } + + requestFile(item) { + // This `requestFile` method is an abstract method that should + // be added by all subclasses. + } + + // To be implemented on the extending class + // e.g. + // Api.gitignoreText item.name, @requestFileSuccess.bind(@) + requestFileSuccess(file, { skipFocus, append } = {}) { + const oldValue = this.editor.getValue(); + let newValue = file.content; + + if (append && oldValue.length && oldValue !== newValue) { + newValue = oldValue + '\n\n' + newValue; + } + + this.editor.setValue(newValue, 1); + if (!skipFocus) this.editor.focus(); + + if (this.editor instanceof jQuery) { + this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); + } + } + + startLoadingSpinner() { + this.dropdownIcon + .addClass('fa-spinner fa-spin') + .removeClass('fa-chevron-down'); + } + + stopLoadingSpinner() { + this.dropdownIcon + .addClass('fa-chevron-down') + .removeClass('fa-spinner fa-spin'); + } + } + + global.TemplateSelector = TemplateSelector; + })(window.gl || ( window.gl = {})); + diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index de6cdd851be..8db4f6a3b28 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -23,13 +23,13 @@ })(this)); this.initModePanesAndLinks(); this.initSoftWrap(); - new BlobLicenseSelectors({ + new gl.BlobLicenseSelectors({ editor: this.editor }); new BlobGitignoreSelectors({ editor: this.editor }); - new BlobCiYamlSelectors({ + new gl.BlobCiYamlSelectors({ editor: this.editor }); } diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 index 7e86f001f44..cacb36a897f 100644 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -21,7 +21,8 @@ }, data () { return { - filters: Store.state.filters + filters: Store.state.filters, + showIssueForm: false }; }, watch: { @@ -33,6 +34,11 @@ deep: true } }, + methods: { + showNewIssueForm() { + this.showIssueForm = !this.showIssueForm; + } + }, ready () { const options = gl.issueBoards.getBoardSortableDefaultOptions({ disabled: this.disabled, 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 63d72d857d9..ff90f2d6d75 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.js.es6 +++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6 @@ -8,10 +8,8 @@ data () { return { predefinedLabels: [ - new ListLabel({ title: 'Development', color: '#5CB85C' }), - new ListLabel({ title: 'Testing', color: '#F0AD4E' }), - new ListLabel({ title: 'Production', color: '#FF5F00' }), - new ListLabel({ title: 'Ready', color: '#FF0000' }) + new ListLabel({ title: 'To Do', color: '#F0AD4E' }), + new ListLabel({ title: 'Doing', color: '#5CB85C' }) ] } }, diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 474805c1437..7022a29e818 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -1,4 +1,5 @@ //= require ./board_card +//= require ./board_new_issue (() => { const Store = gl.issueBoards.BoardsStore; @@ -8,14 +9,16 @@ gl.issueBoards.BoardList = Vue.extend({ components: { - 'board-card': gl.issueBoards.BoardCard + 'board-card': gl.issueBoards.BoardCard, + 'board-new-issue': gl.issueBoards.BoardNewIssue }, props: { disabled: Boolean, list: Object, issues: Array, loading: Boolean, - issueLinkBase: String + issueLinkBase: String, + showIssueForm: Boolean }, data () { return { @@ -73,7 +76,7 @@ group: 'issues', sort: false, disabled: this.disabled, - filter: '.board-list-count', + filter: '.board-list-count, .is-disabled', onStart: (e) => { const card = this.$refs.issue[e.oldIndex]; diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6 new file mode 100644 index 00000000000..a4fad422eca --- /dev/null +++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6 @@ -0,0 +1,58 @@ +(() => { + window.gl = window.gl || {}; + + gl.issueBoards.BoardNewIssue = Vue.extend({ + props: { + list: Object, + showIssueForm: Boolean + }, + data() { + return { + title: '', + error: false + }; + }, + watch: { + showIssueForm () { + this.$els.input.focus(); + } + }, + methods: { + submit(e) { + e.preventDefault(); + if (this.title.trim() === '') return; + + this.error = false; + + const labels = this.list.label ? [this.list.label] : []; + const issue = new ListIssue({ + title: this.title, + labels + }); + + this.list.newIssue(issue) + .then((data) => { + // Need this because our jQuery very kindly disables buttons on ALL form submissions + $(this.$els.submitButton).enable(); + }) + .catch(() => { + // Need this because our jQuery very kindly disables buttons on ALL form submissions + $(this.$els.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 = ''; + } + } + }); +})(); 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 1a4d8157970..6ccd83e2d84 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 @@ -3,8 +3,7 @@ $(() => { $('.js-new-board-list').each(function () { const $this = $(this); - - new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('project-id')); + new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path')); $this.glDropdown({ data(term, callback) { 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 44addb3ea98..f629d45c587 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 @@ -21,7 +21,7 @@ fallbackClass: 'is-dragging', fallbackOnBody: true, ghostClass: 'is-ghost', - filter: '.has-tooltip', + filter: '.has-tooltip, .btn', delay: gl.issueBoards.touchEnabled ? 100 : 0, scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, scrollSpeed: 20, diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index 91fd620fdb3..5d0a561cdba 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -87,6 +87,17 @@ class List { }); } + newIssue (issue) { + this.addIssue(issue); + this.issuesSize++; + + return gl.boardService.newIssue(this.id, issue) + .then((resp) => { + const data = resp.json(); + issue.id = data.iid; + }); + } + createIssues (data) { data.forEach((issueObj) => { this.addIssue(new ListIssue(issueObj)); diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index 9b80fb2e99f..2b825c3949f 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -58,4 +58,10 @@ class BoardService { to_list_id }); } + + newIssue (id, issue) { + return this.issues.save({ id }, { + issue + }); + } }; diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 78d21c0552a..f336bfc36d6 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -146,7 +146,7 @@ $date = $('.js-artifacts-remove'); if ($date.length) { date = $date.text(); - return $date.text($.timefor(new Date(date.replace(/-/g, '/')), ' ')); + return $date.text($.timefor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' ')); } }; diff --git a/app/assets/javascripts/commit/image-file.js b/app/assets/javascripts/commit/image_file.js index e893491b19b..e893491b19b 100644 --- a/app/assets/javascripts/commit/image-file.js +++ b/app/assets/javascripts/commit/image_file.js diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js index 3e20db7e308..e23bda2fa4e 100644 --- a/app/assets/javascripts/copy_to_clipboard.js +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -26,15 +26,15 @@ }; showTooltip = function(target, title) { - return $(target).tooltip({ - container: 'body', - html: 'true', - placement: 'auto bottom', - title: title, - trigger: 'manual' - }).tooltip('show').one('mouseleave', function() { - return $(this).tooltip('hide'); - }); + var $target = $(target); + var originalTitle = $target.data('original-title'); + + $target + .attr('title', 'Copied!') + .tooltip('fixTitle') + .tooltip('show') + .attr('title', originalTitle) + .tooltip('fixTitle'); }; $(function() { diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6 index 46d1c3f00c1..c5f8c29242d 100644 --- a/app/assets/javascripts/create_label.js.es6 +++ b/app/assets/javascripts/create_label.js.es6 @@ -1,8 +1,9 @@ (function (w) { class CreateLabelDropdown { - constructor ($el, projectId) { + constructor ($el, namespacePath, projectPath) { this.$el = $el; - this.projectId = projectId; + this.namespacePath = namespacePath; + this.projectPath = projectPath; this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown')); this.$cancelButton = $('.js-cancel-label-btn', this.$el); this.$newLabelField = $('#new_label_name', this.$el); @@ -91,8 +92,8 @@ e.preventDefault(); e.stopPropagation(); - Api.newLabel(this.projectId, { - name: this.$newLabelField.val(), + Api.newLabel(this.namespacePath, this.projectPath, { + title: this.$newLabelField.val(), color: this.$newColorField.val() }, (label) => { this.$newLabelCreateButton.enable(); diff --git a/app/assets/javascripts/cycle-analytics.js.es6 b/app/assets/javascripts/cycle_analytics.js.es6 index cd9886ba58d..cd9886ba58d 100644 --- a/app/assets/javascripts/cycle-analytics.js.es6 +++ b/app/assets/javascripts/cycle_analytics.js.es6 diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index c8634b78f2b..8086c10ad6b 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -7,6 +7,9 @@ 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) { @@ -52,6 +55,10 @@ })(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]; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index ddf11ecf34c..adff73af79c 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -26,7 +26,7 @@ case 'projects:merge_requests:index': case 'projects:issues:index': Issuable.init(); - new IssuableBulkActions(); + new gl.IssuableBulkActions(); shortcut_handler = new ShortcutsNavigation(); break; case 'projects:issues:show': @@ -40,7 +40,7 @@ new Milestone(); break; case 'dashboard:todos:index': - new Todos(); + new gl.Todos(); break; case 'projects:milestones:new': case 'projects:milestones:edit': @@ -59,7 +59,9 @@ shortcut_handler = new ShortcutsNavigation(); new GLForm($('.issue-form')); new IssuableForm($('.issue-form')); - new IssuableTemplateSelectors(); + new LabelsSelect(); + new MilestoneSelect(); + new gl.IssuableTemplateSelectors(); break; case 'projects:merge_requests:new': case 'projects:merge_requests:edit': @@ -67,7 +69,9 @@ shortcut_handler = new ShortcutsNavigation(); new GLForm($('.merge-request-form')); new IssuableForm($('.merge-request-form')); - new IssuableTemplateSelectors(); + new LabelsSelect(); + new MilestoneSelect(); + new gl.IssuableTemplateSelectors(); break; case 'projects:tags:new': new ZenMode(); @@ -122,6 +126,9 @@ new TreeView(); } break; + case 'projects:pipelines:show': + new gl.Pipelines(); + break; case 'groups:activity': new Activities(); break; @@ -165,7 +172,7 @@ break; case 'projects:labels:index': if ($('.prioritized-labels').length) { - new LabelManager(); + new gl.LabelManager(); } break; case 'projects:network:show': @@ -279,7 +286,7 @@ Dispatcher.prototype.initSearch = function() { // Only when search form is present if ($('.search').length) { - return new SearchAutocomplete(); + return new gl.SearchAutocomplete(); } }; diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index d0786bf0053..845313b6b38 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -52,37 +52,27 @@ } } }, - setup: function(input) { + setup: _.debounce(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) { - if (!this.dataLoading && !this.cachedData) { - this.dataLoading = true; - setTimeout((function(_this) { - return function() { - var fetch; - fetch = _this.fetchData(_this.dataSource); - return fetch.done(function(data) { - _this.dataLoading = false; - return _this.loadData(data); - }); - }; - // We should wait until initializations are done - // and only trigger the last .setup since - // The previous .dataSource belongs to the previous issuable - // and the last one will have the **proper** .dataSource property - // TODO: Make this a singleton and turn off events when moving to another page - })(this), 1000); - } - if (this.cachedData != null) { - return this.loadData(this.cachedData); - } + + 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() { // Emoji this.input.atwho({ diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 1b6db641200..e034ca68645 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -443,6 +443,7 @@ var contentHtml; this.resetRows(); this.addArrowKeyEvent(); + if (this.options.setIndeterminateIds) { this.options.setIndeterminateIds.call(this); } @@ -460,9 +461,21 @@ if (this.options.filterable) { this.filterInput.focus(); } + + if (this.options.showMenuAbove) { + this.positionMenuAbove(); + } + return this.dropdown.trigger('shown.gl.dropdown'); }; + GitLabDropdown.prototype.positionMenuAbove = function() { + var $button = $(this.el); + var $menu = this.dropdown.find('.dropdown-menu'); + + $menu.css('top', ($button.height() + $menu.height()) * -1); + }; + GitLabDropdown.prototype.hidden = function(e) { var $input; this.resetRows(); @@ -725,6 +738,7 @@ return false; } if (currentKeyCode === 13 && currentIndex !== -1) { + e.preventDefault(); _this.selectRowAtIndex(); } }; diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 7c2eebcdd44..5f06186504b 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -5,14 +5,15 @@ function GroupsSelect() { $('.ajax-groups-select').each((function(_this) { return function(i, select) { - var skip_ldap; + var skip_ldap, skip_groups; skip_ldap = $(select).hasClass('skip_ldap'); + skip_groups = $(select).data('skip-groups') || []; return $(select).select2({ placeholder: "Search for a group", multiple: $(select).hasClass('multiselect'), minimumInputLength: 0, query: function(query) { - return Api.groups(query.term, skip_ldap, function(groups) { + return Api.groups(query.term, skip_ldap, skip_groups, function(groups) { var data; data = { results: groups diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 73e2664e9c0..57f7e4ef230 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -51,7 +51,6 @@ }).remove(); // Submit the form to get new data Issuable.filterResults($('.filter-form')); - return $('.js-label-select').trigger('update.label'); }); }, filterResults: (function(_this) { diff --git a/app/assets/javascripts/issues-bulk-assignment.js b/app/assets/javascripts/issues_bulk_assignment.js.es6 index 62a7fc9a06c..0808f538f01 100644 --- a/app/assets/javascripts/issues-bulk-assignment.js +++ b/app/assets/javascripts/issues_bulk_assignment.js.es6 @@ -1,13 +1,10 @@ -(function() { - this.IssuableBulkActions = (function() { - function IssuableBulkActions(opts) { - // Set defaults - var ref, ref1, ref2; - if (opts == null) { - opts = {}; - } - this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issuable-list > li'); - // Save instance +((global) => { + + class IssuableBulkActions { + constructor({ container, form, issues } = {}) { + this.container = container || $('.content'), + this.form = form || this.getElement('.bulk-update'); + this.issues = issues || this.getElement('.issues-list .issue'); this.form.data('bulkActions', this); this.willUpdateLabels = false; this.bindEvents(); @@ -15,53 +12,46 @@ Issuable.initChecks(); } - IssuableBulkActions.prototype.getElement = function(selector) { + getElement(selector) { return this.container.find(selector); - }; + } - IssuableBulkActions.prototype.bindEvents = function() { + bindEvents() { return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); - }; + } - IssuableBulkActions.prototype.onFormSubmit = function(e) { + onFormSubmit(e) { e.preventDefault(); return this.submit(); - }; + } - IssuableBulkActions.prototype.submit = function() { - var _this, xhr; - _this = this; - xhr = $.ajax({ + submit() { + const _this = this; + const xhr = $.ajax({ url: this.form.attr('action'), method: this.form.attr('method'), dataType: 'JSON', data: this.getFormDataAsObject() }); - xhr.done(function(response, status, xhr) { - return location.reload(); - }); - xhr.fail(function() { - return new Flash("Issue update failed"); - }); + xhr.done(() => window.location.reload()); + xhr.fail(() => new Flash("Issue update failed")); return xhr.always(this.onFormSubmitAlways.bind(this)); - }; + } - IssuableBulkActions.prototype.onFormSubmitAlways = function() { + onFormSubmitAlways() { return this.form.find('[type="submit"]').enable(); - }; + } - IssuableBulkActions.prototype.getSelectedIssues = function() { + getSelectedIssues() { return this.issues.has('.selected_issue:checked'); - }; + } - IssuableBulkActions.prototype.getLabelsFromSelection = function() { - var labels; - labels = []; + getLabelsFromSelection() { + const labels = []; this.getSelectedIssues().map(function() { - var _labels; - _labels = $(this).data('labels'); - if (_labels) { - return _labels.map(function(labelId) { + const labelsData = $(this).data('labels'); + if (labelsData) { + return labelsData.map(function(labelId) { if (labels.indexOf(labelId) === -1) { return labels.push(labelId); } @@ -69,7 +59,7 @@ } }); return labels; - }; + } /** @@ -77,25 +67,21 @@ * @return {Array} Label IDs */ - IssuableBulkActions.prototype.getUnmarkedIndeterminedLabels = function() { - var el, i, id, j, labelsToKeep, len, len1, ref, ref1, result; - result = []; - labelsToKeep = []; - ref = this.getElement('.labels-filter .is-indeterminate'); - for (i = 0, len = ref.length; i < len; i++) { - el = ref[i]; - labelsToKeep.push($(el).data('labelId')); - } - ref1 = this.getLabelsFromSelection(); - for (j = 0, len1 = ref1.length; j < len1; j++) { - id = ref1[j]; - // Only the ones that we are not going to keep + getUnmarkedIndeterminedLabels() { + const result = []; + const labelsToKeep = []; + + this.getElement('.labels-filter .is-indeterminate') + .each((i, el) => labelsToKeep.push($(el).data('labelId'))); + + this.getLabelsFromSelection().forEach((id) => { if (labelsToKeep.indexOf(id) === -1) { result.push(id); } - } + }); + return result; - }; + } /** @@ -103,9 +89,8 @@ * Returns key/value pairs from form data */ - IssuableBulkActions.prototype.getFormDataAsObject = function() { - var formData; - formData = { + getFormDataAsObject() { + const formData = { update: { state_event: this.form.find('input[name="update[state_event]"]').val(), assignee_id: this.form.find('input[name="update[assignee_id]"]').val(), @@ -125,19 +110,18 @@ }); } return formData; - }; + } - IssuableBulkActions.prototype.getLabelsToApply = function() { - var $labels, labelIds; - labelIds = []; - $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]'); + getLabelsToApply() { + 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())); } }); return labelIds; - }; + } /** @@ -145,11 +129,10 @@ * @return {Array} Array of labels IDs */ - IssuableBulkActions.prototype.getLabelsToRemove = function() { - var indeterminatedLabels, labelsToApply, result; - result = []; - indeterminatedLabels = this.getUnmarkedIndeterminedLabels(); - labelsToApply = this.getLabelsToApply(); + 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 @@ -158,10 +141,9 @@ } }); return result; - }; - - return IssuableBulkActions; + } + } - })(); + global.IssuableBulkActions = IssuableBulkActions; -}).call(this); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6 new file mode 100644 index 00000000000..bc68e53504f --- /dev/null +++ b/app/assets/javascripts/label_manager.js.es6 @@ -0,0 +1,106 @@ +((global) => { + + class LabelManager { + constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) { + this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority'); + this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels'); + this.otherLabels = otherLabels || $('.js-other-labels'); + this.errorMessage = 'Unable to update label prioritization at this time'; + this.prioritizedLabels.sortable({ + items: 'li', + placeholder: 'list-placeholder', + axis: 'y', + update: this.onPrioritySortUpdate.bind(this) + }); + this.bindEvents(); + } + + bindEvents() { + return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); + } + + onTogglePriorityClick(e) { + e.preventDefault(); + const _this = e.data; + const $btn = $(e.currentTarget); + const $label = $(`#${$btn.data('domId')}`); + const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; + const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`); + $tooltip.tooltip('destroy'); + return _this.toggleLabelPriority($label, action); + } + + toggleLabelPriority($label, action, persistState) { + if (persistState == null) { + persistState = true; + } + let xhr; + const _this = this; + const url = $label.find('.js-toggle-priority').data('url'); + let $target = this.prioritizedLabels; + let $from = this.otherLabels; + if (action === 'remove') { + $target = this.otherLabels; + $from = this.prioritizedLabels; + } + if ($from.find('li').length === 1) { + $from.find('.empty-message').removeClass('hidden'); + } + if (!$target.find('li').length) { + $target.find('.empty-message').addClass('hidden'); + } + $label.detach().appendTo($target); + // Return if we are not persisting state + if (!persistState) { + return; + } + if (action === 'remove') { + xhr = $.ajax({ + url, + type: 'DELETE' + }); + // Restore empty message + if (!$from.find('li').length) { + $from.find('.empty-message').removeClass('hidden'); + } + } else { + xhr = this.savePrioritySort($label, action); + } + return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); + } + + onPrioritySortUpdate() { + const xhr = this.savePrioritySort(); + return xhr.fail(function() { + return new Flash(this.errorMessage, 'alert'); + }); + } + + savePrioritySort() { + return $.post({ + url: this.prioritizedLabels.data('url'), + data: { + label_ids: this.getSortedLabelsIds() + } + }); + } + + rollbackLabelPosition($label, originalAction) { + const action = originalAction === 'remove' ? 'add' : 'remove'; + this.toggleLabelPriority($label, action, false); + return new Flash(this.errorMessage, 'alert'); + } + + getSortedLabelsIds() { + const sortedIds = []; + this.prioritizedLabels.find('li').each(function() { + sortedIds.push($(this).data('id')); + }); + return sortedIds; + } + } + + gl.LabelManager = LabelManager; + +})(window.gl || (window.gl = {})); + diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index ce79e2e348a..e356872624a 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -4,9 +4,11 @@ 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, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected; + 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; $dropdown = $(dropdown); - projectId = $dropdown.data('project-id'); + $toggleText = $dropdown.find('.dropdown-toggle-text'); + namespacePath = $dropdown.data('namespace-path'); + projectPath = $dropdown.data('project-path'); labelUrl = $dropdown.data('labels'); issueUpdateURL = $dropdown.data('issueUpdate'); selectedLabel = $dropdown.data('selected'); @@ -15,6 +17,7 @@ } showNo = $dropdown.data('show-no'); showAny = $dropdown.data('show-any'); + showMenuAbove = $dropdown.data('showMenuAbove'); defaultLabel = $dropdown.data('default-label'); abilityName = $dropdown.data('ability-name'); $selectbox = $dropdown.closest('.selectbox'); @@ -24,6 +27,9 @@ $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip'); $value = $block.find('.value'); $loading = $block.find('.block-loading').fadeOut(); + fieldName = $dropdown.data('field-name'); + useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown'); + propertyName = useId ? 'id' : 'title'; initialSelected = $selectbox .find('input[name="' + $dropdown.data('field-name') + '"]') .map(function () { @@ -40,12 +46,12 @@ $sidebarLabelTooltip.tooltip(); if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) { - new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId); + new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath); } saveLabelData = function() { var data, selected; - selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() { + selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() { return this.value; }).get(); @@ -75,7 +81,8 @@ if (data.labels.length) { template = labelHTMLTemplate(data); labelCount = data.labels.length; - } else { + } + else { template = labelNoneHTMLTemplate; } $value.removeAttr('style').html(template); @@ -92,7 +99,8 @@ } labelTooltipTitle = labelTitles.join(', '); - } else { + } + else { labelTooltipTitle = ''; $sidebarLabelTooltip.tooltip('destroy'); } @@ -114,6 +122,7 @@ }); }; return $dropdown.glDropdown({ + showMenuAbove: showMenuAbove, data: function(term, callback) { return $.ajax({ url: labelUrl @@ -133,23 +142,29 @@ }; }).value(); if ($dropdown.hasClass('js-extra-options')) { + var extraData = []; if (showNo) { - data.unshift({ + extraData.unshift({ id: 0, title: 'No Label' }); } if (showAny) { - data.unshift({ + extraData.unshift({ isAny: true, title: 'Any Label' }); } - if (data.length > 2) { - data.splice(2, 0, 'divider'); + if (extraData.length) { + extraData.push('divider'); + data = extraData.concat(data); } } - return callback(data); + + callback(data); + if (showMenuAbove) { + $dropdown.data('glDropdown').positionMenuAbove(); + } }); }, renderRow: function(label, instance) { @@ -157,7 +172,7 @@ $li = $('<li>'); $a = $('<a href="#">'); selectedClass = []; - removesAll = label.id === 0 || (label.id == null); + removesAll = label.id <= 0 || (label.id == null); if ($dropdown.hasClass('js-filter-bulk-update')) { indeterminate = instance.indeterminateIds; active = instance.activeIds; @@ -194,14 +209,16 @@ return color + " " + percentFirst + "%," + color + " " + percentSecond + "% "; }).join(','); color = "linear-gradient(" + color + ")"; - } else { + } + else { if (label.color != null) { color = label.color[0]; } } if (color) { colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>"; - } else { + } + else { colorEl = ''; } // We need to identify which items are actually labels @@ -219,30 +236,46 @@ }, selectable: true, filterable: true, + selected: $dropdown.data('selected') || [], toggleLabel: function(selected, el) { - var selected_labels; - selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active'); - if (selected && (selected.title != null)) { - if (selected_labels.length > 1) { - return selected.title + " +" + (selected_labels.length - 1) + " more"; - } else { - return selected.title; - } - } else if (!selected && selected_labels.length !== 0) { - if (selected_labels.length > 1) { - return ($(selected_labels[0]).text()) + " +" + (selected_labels.length - 1) + " more"; - } else if (selected_labels.length === 1) { - return $(selected_labels).text(); - } - } else { + var isSelected = el !== null ? el.hasClass('is-active') : false; + var title = selected.title; + var selectedLabels = this.selected; + + if (selected.id === 0) { + this.selected = []; + return 'No Label'; + } + else if (isSelected) { + this.selected.push(title); + } + else { + var index = this.selected.indexOf(title); + this.selected.splice(index, 1); + } + + if (selectedLabels.length === 1) { + return selectedLabels; + } + else if (selectedLabels.length) { + return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more"; + } + else { return defaultLabel; } }, fieldName: $dropdown.data('field-name'), id: function(label) { + if (label.id <= 0) return; + + if ($dropdown.hasClass('js-issuable-form-dropdown')) { + return label.id; + } + if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) { return label.title; - } else { + } + else { return label.id; } }, @@ -254,6 +287,11 @@ $selectbox.hide(); // display:block overrides the hide-collapse rule $value.removeAttr('style'); + + if ($dropdown.hasClass('js-issuable-form-dropdown')) { + return; + } + if (page === 'projects:boards:show') { return; } @@ -261,9 +299,11 @@ if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']"); Issuable.filterResults($dropdown.closest('form')); - } else if ($dropdown.hasClass('js-filter-submit')) { + } + else if ($dropdown.hasClass('js-filter-submit')) { $dropdown.closest('form').submit(); - } else { + } + else { if (!$dropdown.hasClass('js-filter-bulk-update')) { saveLabelData(); } @@ -280,18 +320,28 @@ clicked: function(label, $el, e) { var isIssueIndex, isMRIndex, page; _this.enableBulkLabelDropdown(); - if ($dropdown.hasClass('js-filter-bulk-update')) { + + if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) { + $dropdown.parent() + .find('.dropdown-clear-active') + .removeClass('is-active') + } + + if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { return; } + page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = page === 'projects:merge_requests:index'; if (page === 'projects:boards:show') { if (label.isAny) { gl.issueBoards.BoardsStore.state.filters['label_name'] = []; - } else if ($el.hasClass('is-active')) { + } + else if ($el.hasClass('is-active')) { gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title); - } else { + } + else { var filters = gl.issueBoards.BoardsStore.state.filters['label_name']; filters = filters.filter(function (filteredLabel) { return filteredLabel !== label.title; @@ -302,17 +352,21 @@ gl.issueBoards.BoardsStore.updateFiltersUrl(); e.preventDefault(); return; - } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + } + else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { if (!$dropdown.hasClass('js-multiselect')) { selectedLabel = label.title; return Issuable.filterResults($dropdown.closest('form')); } - } else if ($dropdown.hasClass('js-filter-submit')) { + } + else if ($dropdown.hasClass('js-filter-submit')) { return $dropdown.closest('form').submit(); - } else { + } + else { if ($dropdown.hasClass('js-multiselect')) { - } else { + } + else { return saveLabelData(); } } diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 9299d0eabd2..b170e26eebf 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -38,6 +38,11 @@ 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; + }; return jQuery.timefor = function(time, suffix, expiredLabel) { var suffixFromNow, timefor; if (!time) { diff --git a/app/assets/javascripts/merge_conflict_data_provider.js.es6 b/app/assets/javascripts/merge_conflict_data_provider.js.es6 index cd92df8ddc5..13ee794ba38 100644 --- a/app/assets/javascripts/merge_conflict_data_provider.js.es6 +++ b/app/assets/javascripts/merge_conflict_data_provider.js.es6 @@ -7,13 +7,16 @@ const ORIGIN_BUTTON_TITLE = 'Use theirs'; class MergeConflictDataProvider { getInitialData() { + // TODO: remove reliance on jQuery and DOM state introspection const diffViewType = $.cookie('diff_view'); + const fixedLayout = $('.content-wrapper .container-fluid').hasClass('container-limited'); return { isLoading : true, hasError : false, isParallel : diffViewType === 'parallel', diffViewType : diffViewType, + fixedLayout : fixedLayout, isSubmitting : false, conflictsData : {}, resolutionData : {} @@ -192,14 +195,17 @@ class MergeConflictDataProvider { updateViewType(newType) { const vi = this.vueInstance; - if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) { + if (newType === vi.diffViewType || !(newType === 'parallel' || newType === 'inline')) { return; } - vi.diffView = newType; - vi.isParallel = newType === 'parallel'; - $.cookie('diff_view', newType); // TODO: Make sure that cookie path added. - $('.content-wrapper .container-fluid').toggleClass('container-limited'); + vi.diffViewType = newType; + vi.isParallel = newType === 'parallel'; + $.cookie('diff_view', newType, { + path: (gon && gon.relative_url_root) || '/' + }); + $('.content-wrapper .container-fluid') + .toggleClass('container-limited', !vi.isParallel && vi.fixedLayout); } diff --git a/app/assets/javascripts/merge_conflict_resolver.js.es6 b/app/assets/javascripts/merge_conflict_resolver.js.es6 index b56fd5aa658..7e756433bf5 100644 --- a/app/assets/javascripts/merge_conflict_resolver.js.es6 +++ b/app/assets/javascripts/merge_conflict_resolver.js.es6 @@ -60,9 +60,8 @@ class MergeConflictResolver { $('#conflicts .js-syntax-highlight').syntaxHighlight(); }); - if (this.vue.diffViewType === 'parallel') { - $('.content-wrapper .container-fluid').removeClass('container-limited'); - } + $('.content-wrapper .container-fluid') + .toggleClass('container-limited', !this.vue.isParallel && this.vue.fixedLayout); }) } diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 05644b3d03c..02ff5a382e2 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -36,13 +36,10 @@ }; MergeRequest.prototype.initTabs = function() { - if (this.opts.action !== 'new') { - // `MergeRequests#new` has no tab-persisting or lazy-loading behavior - window.mrTabs = new MergeRequestTabs(this.opts); - } else { - // Show the first tab (Commits) - return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show'); + if (window.mrTabs) { + window.mrTabs.unbindEvents(); } + window.mrTabs = new MergeRequestTabs(this.opts); }; MergeRequest.prototype.showAllCommits = function() { diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 18bbfa7a459..8045d24a1bb 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -56,9 +56,14 @@ 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); @@ -70,7 +75,12 @@ MergeRequestTabs.prototype.bindEvents = function() { $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); - return $(document).on('click', '.js-show-tab', this.showTab); + $(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) { @@ -85,11 +95,15 @@ if (action === 'commits') { this.loadCommits($target.attr('href')); this.expandView(); - } else if (action === 'diffs') { + 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 @@ -97,11 +111,14 @@ } 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); @@ -126,7 +143,7 @@ if (action === 'show') { action = 'notes'; } - return $(".merge-request-tabs a[data-action='" + action + "']").tab('show'); + $(".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 @@ -156,8 +173,9 @@ action = 'notes'; } this.currentAction = action; - // Remove a trailing '/commits' or '/diffs' - new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, ''); + // 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; @@ -196,8 +214,13 @@ 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: (source + ".json") + this._location.search, + url: (url.pathname + ".json") + this._location.search, success: (function(_this) { return function(data) { $('#diffs').html(data.html); @@ -209,7 +232,7 @@ gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); $('#diffs .js-syntax-highlight').syntaxHighlight(); $('#diffs .diff-file').singleFileDiff(); - if (_this.diffViewType() === 'parallel') { + if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) { _this.expandViewContainer(); } _this.diffsLoaded = true; @@ -308,11 +331,25 @@ MergeRequestTabs.prototype.diffViewType = function() { return $('.inline-parallel-buttons a.active').data('view-type'); - // Returns diff view type + }; + + MergeRequestTabs.prototype.isDiffAction = function(action) { + return action === 'diffs' || action === 'new/diffs' }; MergeRequestTabs.prototype.expandViewContainer = function() { - return $('.container-fluid').removeClass('container-limited'); + 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() { diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index c8031174dd2..26cc6eb0e96 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -7,7 +7,7 @@ this.currentProject = JSON.parse(currentProject); } $('.js-milestone-select').each(function(i, dropdown) { - var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId; + var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove; $dropdown = $(dropdown); projectId = $dropdown.data('project-id'); milestonesUrl = $dropdown.data('milestones'); @@ -15,6 +15,7 @@ selectedMilestone = $dropdown.data('selected'); showNo = $dropdown.data('show-no'); showAny = $dropdown.data('show-any'); + showMenuAbove = $dropdown.data('showMenuAbove'); showUpcoming = $dropdown.data('show-upcoming'); useId = $dropdown.data('use-id'); defaultLabel = $dropdown.data('default-label'); @@ -31,12 +32,12 @@ collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>'); } return $dropdown.glDropdown({ + showMenuAbove: showMenuAbove, data: function(term, callback) { return $.ajax({ url: milestonesUrl }).done(function(data) { - var extraOptions; - extraOptions = []; + var extraOptions = []; if (showAny) { extraOptions.push({ id: 0, @@ -58,10 +59,14 @@ title: 'Upcoming' }); } - if (extraOptions.length > 2) { + if (extraOptions.length) { extraOptions.push('divider'); } - return callback(extraOptions.concat(data)); + + callback(extraOptions.concat(data)); + if (showMenuAbove) { + $dropdown.data('glDropdown').positionMenuAbove(); + } }); }, filterable: true, @@ -69,19 +74,20 @@ fields: ['title'] }, selectable: true, - toggleLabel: function(selected) { - if (selected && 'id' in selected) { + toggleLabel: function(selected, el, e) { + if (selected && 'id' in selected && $(el).hasClass('is-active')) { return selected.title; } else { return defaultLabel; } }, + defaultLabel: defaultLabel, fieldName: $dropdown.data('field-name'), text: function(milestone) { return _.escape(milestone.title); }, id: function(milestone) { - if (!useId) { + if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) { return milestone.name; } else { return milestone.id; @@ -100,7 +106,8 @@ page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = (page === page && page === 'projects:merge_requests:index'); - if ($dropdown.hasClass('js-filter-bulk-update')) { + if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { + e.preventDefault(); return; } if (page === 'projects:boards:show') { diff --git a/app/assets/javascripts/network/branch-graph.js b/app/assets/javascripts/network/branch_graph.js index 91132af273a..91132af273a 100644 --- a/app/assets/javascripts/network/branch-graph.js +++ b/app/assets/javascripts/network/branch_graph.js diff --git a/app/assets/javascripts/pipeline.js.es6 b/app/assets/javascripts/pipeline.js.es6 index bf33eb10100..6bf63ee6979 100644 --- a/app/assets/javascripts/pipeline.js.es6 +++ b/app/assets/javascripts/pipeline.js.es6 @@ -1,15 +1,40 @@ -(function() { - function toggleGraph() { - const $pipelineBtn = $(this).closest('.toggle-pipeline-btn'); - const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph'); - const $btnText = $(this).find('.toggle-btn-text'); +((global) => { - $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed'); + class Pipelines { + constructor() { + $(document).off('click', '.toggle-pipeline-btn').on('click', '.toggle-pipeline-btn', this.toggleGraph); + this.addMarginToBuildColumns(); + } - const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed'); + toggleGraph() { + const $pipelineBtn = $(this).closest('.toggle-pipeline-btn'); + const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph'); + const $btnText = $(this).find('.toggle-btn-text'); + const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed'); - graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide') + $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed'); + + + graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide') + } + + addMarginToBuildColumns() { + const $secondChildBuildNode = $('.build:nth-child(2)'); + if ($secondChildBuildNode.length) { + const $firstChildBuildNode = $secondChildBuildNode.prev('.build'); + const $multiBuildColumn = $secondChildBuildNode.closest('.stage-column'); + const $previousColumn = $multiBuildColumn.prev('.stage-column'); + $multiBuildColumn.addClass('left-margin'); + $firstChildBuildNode.addClass('left-connector'); + $previousColumn.each(function() { + $this = $(this); + if ($('.build', $this).length === 1) $this.addClass('no-margin'); + }); + } + $('.pipeline-graph').removeClass('hidden'); + } } - $(document).on('click', '.toggle-pipeline-btn', toggleGraph); -})(); + global.Pipelines = Pipelines; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js.es6 index 30cd6f6e470..a1b0126e857 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js.es6 @@ -1,47 +1,45 @@ -(function() { - var GitLabCrop, - bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; +((global) => { - GitLabCrop = (function() { - var FILENAMEREGEX; + // Matches everything but the file name + const FILENAMEREGEX = /^.*[\\\/]/; - // Matches everything but the file name - FILENAMEREGEX = /^.*[\\\/]/; + class GitLabCrop { + constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg, + exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) { - function GitLabCrop(input, opts) { - var ref, ref1, ref2, ref3, ref4; - if (opts == null) { - opts = {}; - } - this.onUploadImageBtnClick = bind(this.onUploadImageBtnClick, this); - this.onModalHide = bind(this.onModalHide, this); - this.onModalShow = bind(this.onModalShow, this); - this.onPickImageClick = bind(this.onPickImageClick, this); + this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this); + this.onModalHide = this.onModalHide.bind(this); + this.onModalShow = this.onModalShow.bind(this); + this.onPickImageClick = this.onPickImageClick.bind(this); this.fileInput = $(input); - // We should rename to avoid spec to fail - // Form will submit the proper input filed with a file using FormData - this.fileInput.attr('name', (this.fileInput.attr('name')) + "-trigger").attr('id', (this.fileInput.attr('id')) + "-trigger"); - // Set defaults - this.exportWidth = (ref = opts.exportWidth) != null ? ref : 200, this.exportHeight = (ref1 = opts.exportHeight) != null ? ref1 : 200, this.cropBoxWidth = (ref2 = opts.cropBoxWidth) != null ? ref2 : 200, this.cropBoxHeight = (ref3 = opts.cropBoxHeight) != null ? ref3 : 200, this.form = (ref4 = opts.form) != null ? ref4 : this.fileInput.parents('form'), this.filename = opts.filename, this.previewImage = opts.previewImage, this.modalCrop = opts.modalCrop, this.pickImageEl = opts.pickImageEl, this.uploadImageBtn = opts.uploadImageBtn, this.modalCropImg = opts.modalCropImg; - // Required params - // Ensure needed elements are jquery objects - // If selector is provided we will convert them to a jQuery Object - this.filename = this.getElement(this.filename); - this.previewImage = this.getElement(this.previewImage); - this.pickImageEl = this.getElement(this.pickImageEl); - // Modal elements usually are outside the @form element - this.modalCrop = _.isString(this.modalCrop) ? $(this.modalCrop) : this.modalCrop; - this.uploadImageBtn = _.isString(this.uploadImageBtn) ? $(this.uploadImageBtn) : this.uploadImageBtn; this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg; + this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `this.fileInput.attr('id')-trigger`); + this.exportWidth = exportWidth; + this.exportHeight = exportHeight; + this.cropBoxWidth = cropBoxWidth; + this.cropBoxHeight = cropBoxHeight; + this.form = this.fileInput.parents('form'); + this.filename = filename; + this.previewImage = previewImage; + this.modalCrop = modalCrop; + this.pickImageEl = pickImageEl; + this.uploadImageBtn = uploadImageBtn; + this.modalCropImg = modalCropImg; + this.filename = this.getElement(filename); + this.previewImage = this.getElement(previewImage); + this.pickImageEl = this.getElement(pickImageEl); + this.modalCrop = _.isString(modalCrop) ? $(modalCrop) : modalCrop; + this.uploadImageBtn = _.isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn; + this.modalCropImg = _.isString(modalCropImg) ? $(modalCropImg) : modalCropImg; this.cropActionsBtn = this.modalCrop.find('[data-method]'); this.bindEvents(); } - GitLabCrop.prototype.getElement = function(selector) { + getElement(selector) { return $(selector, this.form); - }; + } - GitLabCrop.prototype.bindEvents = function() { + bindEvents() { var _this; _this = this; this.fileInput.on('change', function(e) { @@ -57,13 +55,13 @@ return _this.onActionBtnClick(btn); }); return this.croppedImageBlob = null; - }; + } - GitLabCrop.prototype.onPickImageClick = function() { + onPickImageClick() { return this.fileInput.trigger('click'); - }; + } - GitLabCrop.prototype.onModalShow = function() { + onModalShow() { var _this; _this = this; return this.modalCropImg.cropper({ @@ -95,44 +93,44 @@ }); } }); - }; + } - GitLabCrop.prototype.onModalHide = function() { + onModalHide() { return this.modalCropImg.attr('src', '').cropper('destroy'); - }; + } - GitLabCrop.prototype.onUploadImageBtnClick = function(e) { // Remove attached image - e.preventDefault(); // Destroy cropper instance + onUploadImageBtnClick(e) { + e.preventDefault(); this.setBlob(); this.setPreview(); this.modalCrop.modal('hide'); return this.fileInput.val(''); - }; + } - GitLabCrop.prototype.onActionBtnClick = function(btn) { + onActionBtnClick(btn) { var data, result; data = $(btn).data(); if (this.modalCropImg.data('cropper') && data.method) { return result = this.modalCropImg.cropper(data.method, data.option); } - }; + } - GitLabCrop.prototype.onFileInputChange = function(e, input) { + onFileInputChange(e, input) { return this.readFile(input); - }; + } - GitLabCrop.prototype.readFile = function(input) { + readFile(input) { var _this, reader; _this = this; reader = new FileReader; - reader.onload = function() { + reader.onload = () => { _this.modalCropImg.attr('src', reader.result); return _this.modalCrop.modal('show'); }; return reader.readAsDataURL(input.files[0]); - }; + } - GitLabCrop.prototype.dataURLtoBlob = function(dataURL) { + dataURLtoBlob(dataURL) { var array, binary, i, k, len, v; binary = atob(dataURL.split(',')[1]); array = []; @@ -143,35 +141,32 @@ return new Blob([new Uint8Array(array)], { type: 'image/png' }); - }; + } - GitLabCrop.prototype.setPreview = function() { + setPreview() { var filename; this.previewImage.attr('src', this.dataURL); filename = this.fileInput.val().replace(FILENAMEREGEX, ''); return this.filename.text(filename); - }; + } - GitLabCrop.prototype.setBlob = function() { + setBlob() { this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', { width: 200, height: 200 }).toDataURL('image/png'); return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL); - }; + } - GitLabCrop.prototype.getBlob = function() { + getBlob() { return this.croppedImageBlob; - }; - - return GitLabCrop; - - })(); + } + } $.fn.glCrop = function(opts) { return this.each(function() { return $(this).data('glcrop', new GitLabCrop(this, opts)); }); - }; + } -}).call(this); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js deleted file mode 100644 index 60f9fba5777..00000000000 --- a/app/assets/javascripts/profile/profile.js +++ /dev/null @@ -1,106 +0,0 @@ -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.Profile = (function() { - function Profile(opts) { - var cropOpts, ref; - if (opts == null) { - opts = {}; - } - this.onSubmitForm = bind(this.onSubmitForm, this); - this.form = (ref = opts.form) != null ? ref : $('.edit-user'); - $('.js-preferences-form').on('change.preference', 'input[type=radio]', function() { - return $(this).parents('form').submit(); - // Automatically submit the Preferences form when any of its radio buttons change - }); - $('#user_notification_email').on('change', function() { - return $(this).parents('form').submit(); - // Automatically submit email form when it changes - }); - $('.update-username').on('ajax:before', function() { - $('.loading-username').show(); - $(this).find('.update-success').hide(); - return $(this).find('.update-failed').hide(); - }); - $('.update-username').on('ajax:complete', function() { - $('.loading-username').hide(); - $(this).find('.btn-save').enable(); - return $(this).find('.loading-gif').hide(); - }); - $('.update-notifications').on('ajax:success', function(e, data) { - if (data.saved) { - return new Flash("Notification settings saved", "notice"); - } else { - return new Flash("Failed to save new settings", "alert"); - } - }); - this.bindEvents(); - cropOpts = { - filename: '.js-avatar-filename', - previewImage: '.avatar-image .avatar', - modalCrop: '.modal-profile-crop', - pickImageEl: '.js-choose-user-avatar-button', - uploadImageBtn: '.js-upload-user-avatar', - modalCropImg: '.modal-profile-crop-image' - }; - this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop'); - } - - Profile.prototype.bindEvents = function() { - return this.form.on('submit', this.onSubmitForm); - }; - - Profile.prototype.onSubmitForm = function(e) { - e.preventDefault(); - return this.saveForm(); - }; - - Profile.prototype.saveForm = function() { - var avatarBlob, formData, self; - self = this; - formData = new FormData(this.form[0]); - avatarBlob = this.avatarGlCrop.getBlob(); - if (avatarBlob != null) { - formData.append('user[avatar]', avatarBlob, 'avatar.png'); - } - return $.ajax({ - url: this.form.attr('action'), - type: this.form.attr('method'), - data: formData, - dataType: "json", - processData: false, - contentType: false, - success: function(response) { - return new Flash(response.message, 'notice'); - }, - error: function(jqXHR) { - return new Flash(jqXHR.responseJSON.message, 'alert'); - }, - complete: function() { - window.scrollTo(0, 0); - // Enable submit button after requests ends - return self.form.find(':input[disabled]').enable(); - } - }); - }; - - return Profile; - - })(); - - $(function() { - $(document).on('focusout.ssh_key', '#key_key', function() { - var $title, comment; - $title = $('#key_title'); - comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/); - if (comment && comment.length > 1 && $title.val() === '') { - return $title.val(comment[1]).change(); - } - // Extract the SSH Key title from its comment - }); - if (gl.utils.getPagePath() === 'profiles') { - return new Profile(); - } - }); - -}).call(this); diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6 new file mode 100644 index 00000000000..b2307be73ad --- /dev/null +++ b/app/assets/javascripts/profile/profile.js.es6 @@ -0,0 +1,100 @@ +((global) => { + + class Profile { + constructor({ form } = {}) { + this.onSubmitForm = this.onSubmitForm.bind(this); + this.form = form || $('.edit-user'); + this.bindEvents(); + this.initAvatarGlCrop(); + } + + initAvatarGlCrop() { + const cropOpts = { + filename: '.js-avatar-filename', + previewImage: '.avatar-image .avatar', + modalCrop: '.modal-profile-crop', + pickImageEl: '.js-choose-user-avatar-button', + uploadImageBtn: '.js-upload-user-avatar', + modalCropImg: '.modal-profile-crop-image' + }; + this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop'); + } + + bindEvents() { + $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); + $('#user_notification_email').on('change', this.submitForm); + $('.update-username').on('ajax:before', this.beforeUpdateUsername); + $('.update-username').on('ajax:complete', this.afterUpdateUsername); + $('.update-notifications').on('ajax:success', this.onUpdateNotifs); + this.form.on('submit', this.onSubmitForm); + } + + submitForm() { + return $(this).parents('form').submit(); + } + + onSubmitForm(e) { + e.preventDefault(); + return this.saveForm(); + } + + beforeUpdateUsername() { + $('.loading-username').show(); + $(this).find('.update-success').hide(); + return $(this).find('.update-failed').hide(); + } + + afterUpdateUsername() { + $('.loading-username').hide(); + $(this).find('.btn-save').enable(); + return $(this).find('.loading-gif').hide(); + } + + onUpdateNotifs(e, data) { + return data.saved ? + new Flash("Notification settings saved", "notice") : + new Flash("Failed to save new settings", "alert"); + } + + saveForm() { + const self = this; + const formData = new FormData(this.form[0]); + const avatarBlob = this.avatarGlCrop.getBlob(); + + if (avatarBlob != null) { + formData.append('user[avatar]', avatarBlob, 'avatar.png'); + } + + return $.ajax({ + url: this.form.attr('action'), + type: this.form.attr('method'), + data: formData, + dataType: "json", + processData: false, + contentType: false, + success: response => new Flash(response.message, 'notice'), + error: jqXHR => new Flash(jqXHR.responseJSON.message, 'alert'), + complete: () => { + window.scrollTo(0, 0); + // Enable submit button after requests ends + return self.form.find(':input[disabled]').enable(); + } + }); + } + } + + $(function() { + $(document).on('focusout.ssh_key', '#key_key', function() { + const $title = $('#key_title'); + const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/); + if (comment && comment.length > 1 && $title.val() === '') { + return $title.val(comment[1]).change(); + } + // Extract the SSH Key title from its comment + }); + if (global.utils.getPagePath() === 'profiles') { + return new Profile(); + } + }); + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 20b147500cf..4239ed2f889 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -23,7 +23,7 @@ data = groups.concat(projects); return finalCallback(data); }; - return Api.groups(term, false, groupsCallback); + return Api.groups(term, false, false, groupsCallback); }; } else { projectsCallback = finalCallback; @@ -72,7 +72,7 @@ data = groups.concat(projects); return finalCallback(data); }; - return Api.groups(query.term, false, groupsCallback); + return Api.groups(query.term, false, false, groupsCallback); }; } else { projectsCallback = finalCallback; diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index d34346f862b..8074a94f33e 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -10,7 +10,7 @@ filterable: true, fieldName: 'group_id', data: function(term, callback) { - return Api.groups(term, null, function(data) { + return Api.groups(term, false, false, function(data) { data.unshift({ name: 'Any' }); diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js.es6 index 678d836f56f..b4c6226dc68 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js.es6 @@ -1,30 +1,21 @@ -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.SearchAutocomplete = (function() { - var KEYCODE; - - KEYCODE = { - ESCAPE: 27, - BACKSPACE: 8, - ENTER: 13, - UP: 38, - DOWN: 40 - }; - - function SearchAutocomplete(opts) { - var ref, ref1, ref2, ref3, ref4; - if (opts == null) { - opts = {}; - } - this.onSearchInputBlur = bind(this.onSearchInputBlur, this); - this.onClearInputClick = bind(this.onClearInputClick, this); - this.onSearchInputFocus = bind(this.onSearchInputFocus, this); - this.onSearchInputClick = bind(this.onSearchInputClick, this); - this.onSearchInputKeyUp = bind(this.onSearchInputKeyUp, this); - this.onSearchInputKeyDown = bind(this.onSearchInputKeyDown, this); - this.wrap = (ref = opts.wrap) != null ? ref : $('.search'), this.optsEl = (ref1 = opts.optsEl) != null ? ref1 : this.wrap.find('.search-autocomplete-opts'), this.autocompletePath = (ref2 = opts.autocompletePath) != null ? ref2 : this.optsEl.data('autocomplete-path'), this.projectId = (ref3 = opts.projectId) != null ? ref3 : this.optsEl.data('autocomplete-project-id') || '', this.projectRef = (ref4 = opts.projectRef) != null ? ref4 : this.optsEl.data('autocomplete-project-ref') || ''; - // Dropdown Element +((global) => { + + const KEYCODE = { + ESCAPE: 27, + BACKSPACE: 8, + ENTER: 13, + UP: 38, + DOWN: 40 + }; + + class SearchAutocomplete { + constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) { + this.bindEventContext(); + this.wrap = wrap || $('.search'); + this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts'); + this.autocompletePath = autocompletePath || this.optsEl.data('autocomplete-path'); + this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || ''); + this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || ''); this.dropdown = this.wrap.find('.dropdown'); this.dropdownContent = this.dropdown.find('.dropdown-content'); this.locationBadgeEl = this.getElement('.location-badge'); @@ -46,19 +37,27 @@ } // Finds an element inside wrapper element - SearchAutocomplete.prototype.getElement = function(selector) { + bindEventContext() { + this.onSearchInputBlur = this.onSearchInputBlur.bind(this); + this.onClearInputClick = this.onClearInputClick.bind(this); + this.onSearchInputFocus = this.onSearchInputFocus.bind(this); + this.onSearchInputClick = this.onSearchInputClick.bind(this); + this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this); + this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this); + } + getElement(selector) { return this.wrap.find(selector); - }; + } - SearchAutocomplete.prototype.saveOriginalState = function() { + saveOriginalState() { return this.originalState = this.serializeState(); - }; + } - SearchAutocomplete.prototype.saveTextLength = function() { + saveTextLength() { return this.lastTextLength = this.searchInput.val().length; - }; + } - SearchAutocomplete.prototype.createAutocomplete = function() { + createAutocomplete() { return this.searchInput.glDropdown({ filterInputBlur: false, filterable: true, @@ -73,9 +72,9 @@ selectable: true, clicked: this.onClick.bind(this) }); - }; + } - SearchAutocomplete.prototype.getData = function(term, callback) { + getData(term, callback) { var _this, contents, jqXHR; _this = this; if (!term) { @@ -138,9 +137,9 @@ }).always(function() { return _this.loadingSuggestions = false; }); - }; + } - SearchAutocomplete.prototype.getCategoryContents = function() { + getCategoryContents() { var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils; userId = gon.current_user_id; utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; @@ -173,9 +172,9 @@ items.splice(0, 1); } return items; - }; + } - SearchAutocomplete.prototype.serializeState = function() { + serializeState() { return { // Search Criteria search_project_id: this.projectInputEl.val(), @@ -186,9 +185,9 @@ // Location badge _location: this.locationBadgeEl.text() }; - }; + } - SearchAutocomplete.prototype.bindEvents = function() { + bindEvents() { this.searchInput.on('keydown', this.onSearchInputKeyDown); this.searchInput.on('keyup', this.onSearchInputKeyUp); this.searchInput.on('click', this.onSearchInputClick); @@ -200,9 +199,9 @@ return _this.searchInput.focus(); }; })(this)); - }; + } - SearchAutocomplete.prototype.enableAutocomplete = function() { + enableAutocomplete() { var _this; // No need to enable anything if user is not logged in if (!gon.current_user_id) { @@ -216,12 +215,12 @@ } }; - SearchAutocomplete.prototype.onSearchInputKeyDown = function() { // Saves last length of the entered text + onSearchInputKeyDown() { return this.saveTextLength(); - }; + } - SearchAutocomplete.prototype.onSearchInputKeyUp = function(e) { + onSearchInputKeyUp(e) { switch (e.keyCode) { case KEYCODE.BACKSPACE: // when trying to remove the location badge @@ -259,54 +258,53 @@ } } this.wrap.toggleClass('has-value', !!e.target.value); - }; + } // Avoid falsy value to be returned - SearchAutocomplete.prototype.onSearchInputClick = function(e) { - // Prevents closing the dropdown menu + onSearchInputClick(e) { return e.stopImmediatePropagation(); - }; + } - SearchAutocomplete.prototype.onSearchInputFocus = function() { + onSearchInputFocus() { this.isFocused = true; this.wrap.addClass('search-active'); if (this.getValue() === '') { return this.getData(); } - }; + } - SearchAutocomplete.prototype.getValue = function() { + getValue() { return this.searchInput.val(); - }; + } - SearchAutocomplete.prototype.onClearInputClick = function(e) { + onClearInputClick(e) { e.preventDefault(); return this.searchInput.val('').focus(); - }; + } - SearchAutocomplete.prototype.onSearchInputBlur = function(e) { + onSearchInputBlur(e) { this.isFocused = false; this.wrap.removeClass('search-active'); // If input is blank then restore state if (this.searchInput.val() === '') { return this.restoreOriginalState(); } - }; + } - SearchAutocomplete.prototype.addLocationBadge = function(item) { + addLocationBadge(item) { var badgeText, category, value; category = item.category != null ? item.category + ": " : ''; value = item.value != null ? item.value : ''; badgeText = "" + category + value; this.locationBadgeEl.text(badgeText).show(); return this.wrap.addClass('has-location-badge'); - }; + } - SearchAutocomplete.prototype.hasLocationBadge = function() { + hasLocationBadge() { return this.wrap.is('.has-location-badge'); }; - SearchAutocomplete.prototype.restoreOriginalState = function() { + restoreOriginalState() { var i, input, inputs, len; inputs = Object.keys(this.originalState); for (i = 0, len = inputs.length; i < len; i++) { @@ -320,13 +318,13 @@ value: this.originalState._location }); } - }; + } - SearchAutocomplete.prototype.badgePresent = function() { + badgePresent() { return this.locationBadgeEl.length; - }; + } - SearchAutocomplete.prototype.resetSearchState = function() { + resetSearchState() { var i, input, inputs, len, results; inputs = Object.keys(this.originalState); results = []; @@ -339,30 +337,30 @@ results.push(this.getElement("#" + input).val('')); } return results; - }; + } - SearchAutocomplete.prototype.removeLocationBadge = function() { + removeLocationBadge() { this.locationBadgeEl.hide(); this.resetSearchState(); this.wrap.removeClass('has-location-badge'); return this.disableAutocomplete(); - }; + } - SearchAutocomplete.prototype.disableAutocomplete = function() { + disableAutocomplete() { if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) { this.searchInput.addClass('disabled'); this.dropdown.removeClass('open').trigger('hidden.bs.dropdown'); this.restoreMenu(); } - }; + } - SearchAutocomplete.prototype.restoreMenu = function() { + restoreMenu() { var html; html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>"; return this.dropdownContent.html(html); }; - SearchAutocomplete.prototype.onClick = function(item, $el, e) { + onClick(item, $el, e) { if (location.pathname.indexOf(item.url) !== -1) { e.preventDefault(); if (!this.badgePresent) { @@ -385,9 +383,9 @@ } }; - return SearchAutocomplete; + } - })(); + global.SearchAutocomplete = SearchAutocomplete; $(function() { var $projectOptionsDataEl = $('.js-search-project-options'); @@ -408,16 +406,16 @@ if ($groupOptionsDataEl.length) { gl.groupOptions = gl.groupOptions || {}; - + var groupPath = $groupOptionsDataEl.data('group-path'); - + gl.groupOptions[groupPath] = { name: $groupOptionsDataEl.data('name'), issuesPath: $groupOptionsDataEl.data('issues-path'), mrPath: $groupOptionsDataEl.data('mr-path') }; } - + if ($dashboardOptionsDataEl.length) { gl.dashboardOptions = { issuesPath: $dashboardOptionsDataEl.data('issues-path'), @@ -426,4 +424,4 @@ } }); -}).call(this); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6 index c32ddf80219..2ecf3b18975 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6 @@ -1,7 +1,7 @@ /*= require ../blob/template_selector */ ((global) => { - class IssuableTemplateSelector extends TemplateSelector { + class IssuableTemplateSelector extends gl.TemplateSelector { constructor(...args) { super(...args); this.projectPath = this.dropdown.data('project-path'); @@ -16,7 +16,7 @@ if (initialQuery.name) this.requestFile(initialQuery); $('.reset-template', this.dropdown.parent()).on('click', () => { - if (this.currentTemplate) this.setInputValueToTemplateContent(); + if (this.currentTemplate) this.setInputValueToTemplateContent(false); }); } @@ -26,26 +26,28 @@ this.currentTemplate = currentTemplate; if (err) return; // Error handled by global AJAX error handler this.stopLoadingSpinner(); - this.setInputValueToTemplateContent(); + this.setInputValueToTemplateContent(true); }); return; } - setInputValueToTemplateContent() { + setInputValueToTemplateContent(append) { // `this.requestFileSuccess` sets the value of the description input field - // to the content of the template selected. + // to the content of the template selected. If `append` is true, the + // template content will be appended to the previous value of the field, + // separated by a blank line if the previous value is non-empty. if (this.titleInput.val() === '') { // If the title has not yet been set, focus the title input and - // skip focusing the description input by setting `true` as the 2nd - // argument to `requestFileSuccess`. - this.requestFileSuccess(this.currentTemplate, true); + // skip focusing the description input by setting `true` as the + // `skipFocus` option to `requestFileSuccess`. + this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append}); this.titleInput.focus(); } else { - this.requestFileSuccess(this.currentTemplate); + this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append}); } return; } } global.IssuableTemplateSelector = IssuableTemplateSelector; -})(window); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6 index bd8cdde033e..4e8247b89e1 100644 --- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6 @@ -1,12 +1,12 @@ ((global) => { class IssuableTemplateSelectors { - constructor(opts = {}) { - this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector'); - this.editor = opts.editor || this.initEditor(); + constructor({ $dropdowns, editor } = {}) { + this.$dropdowns = $dropdowns || $('.js-issuable-selector'); + this.editor = editor || this.initEditor(); this.$dropdowns.each((i, dropdown) => { - let $dropdown = $(dropdown); - new IssuableTemplateSelector({ + const $dropdown = $(dropdown); + new gl.IssuableTemplateSelector({ pattern: /(\.md)/, data: $dropdown.data('data'), wrapper: $dropdown.closest('.js-issuable-selector-wrap'), @@ -26,4 +26,4 @@ } global.IssuableTemplateSelectors = IssuableTemplateSelectors; -})(window); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js.es6 index 93421649ac7..055228c5df8 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/todos.js.es6 @@ -1,34 +1,29 @@ -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.Todos = (function() { - function Todos(opts) { - var ref; - if (opts == null) { - opts = {}; - } - this.allDoneClicked = bind(this.allDoneClicked, this); - this.doneClicked = bind(this.doneClicked, this); - this.el = (ref = opts.el) != null ? ref : $('.js-todos-options'); +((global) => { + + class Todos { + constructor({ el } = {}) { + this.allDoneClicked = this.allDoneClicked.bind(this); + this.doneClicked = this.doneClicked.bind(this); + this.el = el || $('.js-todos-options'); this.perPage = this.el.data('perPage'); this.clearListeners(); this.initBtnListeners(); this.initFilters(); } - Todos.prototype.clearListeners = function() { + clearListeners() { $('.done-todo').off('click'); $('.js-todos-mark-all').off('click'); return $('.todo').off('click'); - }; + } - Todos.prototype.initBtnListeners = function() { + initBtnListeners() { $('.done-todo').on('click', this.doneClicked); $('.js-todos-mark-all').on('click', this.allDoneClicked); return $('.todo').on('click', this.goToTodoUrl); - }; + } - Todos.prototype.initFilters = function() { + initFilters() { new UsersSelect(); this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']); this.initFilterDropdown($('.js-type-search'), 'type'); @@ -38,125 +33,117 @@ event.preventDefault(); Turbolinks.visit(this.action + '&' + $(this).serialize()); }); - }; + } - Todos.prototype.initFilterDropdown = function($dropdown, fieldName, searchFields) { + initFilterDropdown($dropdown, fieldName, searchFields) { $dropdown.glDropdown({ + fieldName, selectable: true, filterable: searchFields ? true : false, - fieldName: fieldName, search: { fields: searchFields }, data: $dropdown.data('data'), clicked: function() { return $dropdown.closest('form.filter-form').submit(); } }) - }; + } - Todos.prototype.doneClicked = function(e) { - var $this; + doneClicked(e) { e.preventDefault(); e.stopImmediatePropagation(); - $this = $(e.currentTarget); - $this.disable(); + const $target = $(e.currentTarget); + $target.disable(); return $.ajax({ type: 'POST', - url: $this.attr('href'), + url: $target.attr('href'), dataType: 'json', data: { '_method': 'delete' }, - success: (function(_this) { - return function(data) { - _this.redirectIfNeeded(data.count); - _this.clearDone($this.closest('li')); - return _this.updateBadges(data); - }; - })(this) + success: (data) => { + this.redirectIfNeeded(data.count); + this.clearDone($target.closest('li')); + return this.updateBadges(data); + } }); - }; + } - Todos.prototype.allDoneClicked = function(e) { - var $this; + allDoneClicked(e) { e.preventDefault(); e.stopImmediatePropagation(); - $this = $(e.currentTarget); - $this.disable(); + $target = $(e.currentTarget); + $target.disable(); return $.ajax({ type: 'POST', - url: $this.attr('href'), + url: $target.attr('href'), dataType: 'json', data: { '_method': 'delete' }, - success: (function(_this) { - return function(data) { - $this.remove(); - $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>'); - return _this.updateBadges(data); - }; - })(this) + success: (data) => { + $target.remove(); + $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>'); + return this.updateBadges(data); + } }); - }; + } - Todos.prototype.clearDone = function($row) { - var $ul; - $ul = $row.closest('ul'); + clearDone($row) { + const $ul = $row.closest('ul'); $row.remove(); if (!$ul.find('li').length) { return $ul.parents('.panel').remove(); } - }; + } - Todos.prototype.updateBadges = function(data) { + updateBadges(data) { $('.todos-pending .badge, .todos-pending-count').text(data.count); return $('.todos-done .badge').text(data.done_count); - }; + } - Todos.prototype.getTotalPages = function() { + getTotalPages() { return this.el.data('totalPages'); - }; + } - Todos.prototype.getCurrentPage = function() { + getCurrentPage() { return this.el.data('currentPage'); - }; + } - Todos.prototype.getTodosPerPage = function() { + getTodosPerPage() { return this.el.data('perPage'); - }; + } + + redirectIfNeeded(total) { + const currPages = this.getTotalPages(); + const currPage = this.getCurrentPage(); - Todos.prototype.redirectIfNeeded = function(total) { - var currPage, currPages, newPages, pageParams, url; - currPages = this.getTotalPages(); - currPage = this.getCurrentPage(); // Refresh if no remaining Todos if (!total) { - location.reload(); + window.location.reload(); return; } // Do nothing if no pagination if (!currPages) { return; } - newPages = Math.ceil(total / this.getTodosPerPage()); - // Includes query strings - url = location.href; - // If new total of pages is different than we have now + + const newPages = Math.ceil(total / this.getTodosPerPage()); + let url = location.href; + if (newPages !== currPages) { // Redirect to previous page if there's one available if (currPages > 1 && currPage === currPages) { - pageParams = { + const pageParams = { page: currPages - 1 }; url = gl.utils.mergeUrlParams(pageParams, url); } return Turbolinks.visit(url); } - }; + } - Todos.prototype.goToTodoUrl = function(e) { - var todoLink; - todoLink = $(this).data('url'); + goToTodoUrl(e) { + const todoLink = $(this).data('url'); if (!todoLink) { return; } @@ -167,10 +154,8 @@ } else { return Turbolinks.visit(todoLink); } - }; - - return Todos; - - })(); + } + } -}).call(this); + global.Todos = Todos; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6 index 6889d3a7491..0f97924d94e 100644 --- a/app/assets/javascripts/user.js.es6 +++ b/app/assets/javascripts/user.js.es6 @@ -1,7 +1,7 @@ -(global => { +((global) => { global.User = class { - constructor(opts) { - this.opts = opts; + constructor({ action }) { + this.action = action; this.placeProfileAvatarsToTop(); this.initTabs(); this.hideProjectLimitMessage(); @@ -14,9 +14,9 @@ } initTabs() { - return new UserTabs({ + return new global.UserTabs({ parentEl: '.user-profile', - action: this.opts.action + action: this.action }); } diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js deleted file mode 100644 index 8a657780eb6..00000000000 --- a/app/assets/javascripts/user_tabs.js +++ /dev/null @@ -1,188 +0,0 @@ -// UserTabs -// -// Handles persisting and restoring the current tab selection and lazily-loading -// content on the Users#show page. -// -// ### Example Markup -// -// <ul class="nav-links"> -// <li class="activity-tab active"> -// <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username"> -// Activity -// </a> -// </li> -// <li class="groups-tab"> -// <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups"> -// Groups -// </a> -// </li> -// <li class="contributed-tab"> -// <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed"> -// Contributed projects -// </a> -// </li> -// <li class="projects-tab"> -// <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects"> -// Personal projects -// </a> -// </li> -// <li class="snippets-tab"> -// <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets"> -// </a> -// </li> -// </ul> -// -// <div class="tab-content"> -// <div class="tab-pane" id="activity"> -// Activity Content -// </div> -// <div class="tab-pane" id="groups"> -// Groups Content -// </div> -// <div class="tab-pane" id="contributed"> -// Contributed projects content -// </div> -// <div class="tab-pane" id="projects"> -// Projects content -// </div> -// <div class="tab-pane" id="snippets"> -// Snippets content -// </div> -// </div> -// -// <div class="loading-status"> -// <div class="loading"> -// Loading Animation -// </div> -// </div> -// -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.UserTabs = (function() { - function UserTabs(opts) { - this.tabShown = bind(this.tabShown, this); - var i, item, len, ref, ref1, ref2, ref3; - this.action = (ref = opts.action) != null ? ref : 'activity', this.defaultAction = (ref1 = opts.defaultAction) != null ? ref1 : 'activity', this.parentEl = (ref2 = opts.parentEl) != null ? ref2 : $(document); - // Make jQuery object if selector is provided - if (typeof this.parentEl === 'string') { - this.parentEl = $(this.parentEl); - } - // Store the `location` object, allowing for easier stubbing in tests - this._location = location; - // Set tab states - this.loaded = {}; - ref3 = this.parentEl.find('.nav-links a'); - for (i = 0, len = ref3.length; i < len; i++) { - item = ref3[i]; - this.loaded[$(item).attr('data-action')] = false; - } - // Actions - this.actions = Object.keys(this.loaded); - this.bindEvents(); - // Set active tab - if (this.action === 'show') { - this.action = this.defaultAction; - } - this.activateTab(this.action); - } - - UserTabs.prototype.bindEvents = function() { - // Toggle event listeners - return this.parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]').on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', this.tabShown); - }; - - UserTabs.prototype.tabShown = function(event) { - var $target, action, source; - $target = $(event.target); - action = $target.data('action'); - source = $target.attr('href'); - this.setTab(source, action); - return this.setCurrentAction(action); - }; - - UserTabs.prototype.activateTab = function(action) { - return this.parentEl.find(".nav-links .js-" + action + "-tab a").tab('show'); - }; - - UserTabs.prototype.setTab = function(source, action) { - if (this.loaded[action] === true) { - return; - } - if (action === 'activity') { - this.loadActivities(source); - } - if (action === 'groups' || action === 'contributed' || action === 'projects' || action === 'snippets') { - return this.loadTab(source, action); - } - }; - - UserTabs.prototype.loadTab = function(source, action) { - return $.ajax({ - beforeSend: (function(_this) { - return function() { - return _this.toggleLoading(true); - }; - })(this), - complete: (function(_this) { - return function() { - return _this.toggleLoading(false); - }; - })(this), - dataType: 'json', - type: 'GET', - url: source + ".json", - success: (function(_this) { - return function(data) { - var tabSelector; - tabSelector = 'div#' + action; - _this.parentEl.find(tabSelector).html(data.html); - _this.loaded[action] = true; - // Fix tooltips - return gl.utils.localTimeAgo($('.js-timeago', tabSelector)); - }; - })(this) - }); - }; - - UserTabs.prototype.loadActivities = function(source) { - var $calendarWrap; - if (this.loaded['activity'] === true) { - return; - } - $calendarWrap = this.parentEl.find('.user-calendar'); - $calendarWrap.load($calendarWrap.data('href')); - new Activities(); - return this.loaded['activity'] = true; - }; - - UserTabs.prototype.toggleLoading = function(status) { - return this.parentEl.find('.loading-status .loading').toggle(status); - }; - - UserTabs.prototype.setCurrentAction = function(action) { - var new_state, regExp; - // Remove possible actions from URL - regExp = new RegExp('\/(' + this.actions.join('|') + ')(\.html)?\/?$'); - new_state = this._location.pathname; - // remove trailing slashes - new_state = new_state.replace(/\/+$/, ""); - new_state = new_state.replace(regExp, ''); - // Append the new action if we're on a tab other than 'activity' - if (action !== this.defaultAction) { - 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 - }, document.title, new_state); - return new_state; - }; - - return UserTabs; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6 new file mode 100644 index 00000000000..dfdfa1e7f75 --- /dev/null +++ b/app/assets/javascripts/user_tabs.js.es6 @@ -0,0 +1,157 @@ +/* +UserTabs + +Handles persisting and restoring the current tab selection and lazily-loading +content on the Users#show page. + +### Example Markup + + <ul class="nav-links"> + <li class="activity-tab active"> + <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username"> + Activity + </a> + </li> + <li class="groups-tab"> + <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups"> + Groups + </a> + </li> + <li class="contributed-tab"> + <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed"> + Contributed projects + </a> + </li> + <li class="projects-tab"> + <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects"> + Personal projects + </a> + </li> + <li class="snippets-tab"> + <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets"> + </a> + </li> + </ul> + + <div class="tab-content"> + <div class="tab-pane" id="activity"> + Activity Content + </div> + <div class="tab-pane" id="groups"> + Groups Content + </div> + <div class="tab-pane" id="contributed"> + Contributed projects content + </div> + <div class="tab-pane" id="projects"> + Projects content + </div> + <div class="tab-pane" id="snippets"> + Snippets content + </div> + </div> + + <div class="loading-status"> + <div class="loading"> + Loading Animation + </div> + </div> +*/ +((global) => { + class UserTabs { + constructor ({ defaultAction, action, parentEl }) { + this.loaded = {}; + this.defaultAction = defaultAction || 'activity'; + this.action = action || this.defaultAction; + this.$parentEl = $(parentEl) || $(document); + this._location = window.location; + this.$parentEl.find('.nav-links a') + .each((i, navLink) => { + this.loaded[$(navLink).attr('data-action')] = false; + }); + this.actions = Object.keys(this.loaded); + this.bindEvents(); + + if (this.action === 'show') { + this.action = this.defaultAction; + } + + this.activateTab(this.action); + } + + bindEvents() { + return this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]') + .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event)); + } + + tabShown(event) { + const $target = $(event.target); + const action = $target.data('action'); + const source = $target.attr('href'); + this.setTab(source, action); + return this.setCurrentAction(source, action); + } + + activateTab(action) { + return this.$parentEl.find(`.nav-links .js-${action}-tab a`) + .tab('show'); + } + + setTab(source, action) { + if (this.loaded[action]) { + return; + } + if (action === 'activity') { + this.loadActivities(source); + } + + const loadableActions = [ 'groups', 'contributed', 'projects', 'snippets' ]; + if (loadableActions.indexOf(action) > -1) { + return this.loadTab(source, action); + } + } + + loadTab(source, action) { + return $.ajax({ + beforeSend: () => this.toggleLoading(true), + complete: () => this.toggleLoading(false), + dataType: 'json', + type: 'GET', + url: `${source}.json`, + success: (data) => { + const tabSelector = `div#${action}`; + this.$parentEl.find(tabSelector).html(data.html); + this.loaded[action] = true; + return gl.utils.localTimeAgo($('.js-timeago', tabSelector)); + } + }); + } + + loadActivities(source) { + if (this.loaded['activity']) { + return; + } + const $calendarWrap = this.$parentEl.find('.user-calendar'); + $calendarWrap.load($calendarWrap.data('href')); + new Activities(); + return this.loaded['activity'] = true; + } + + toggleLoading(status) { + return this.$parentEl.find('.loading-status .loading') + .toggle(status); + } + + setCurrentAction(source, action) { + let new_state = source + new_state = new_state.replace(/\/+$/, ''); + new_state += this._location.search + this._location.hash; + history.replaceState({ + turbolinks: true, + url: new_state + }, document.title, new_state); + return new_state; + } + } + global.UserTabs = UserTabs; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 9c277998db4..bcabda3ceb2 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -14,11 +14,12 @@ $('.js-user-search').each((function(_this) { return function(i, dropdown) { var options = {}; - var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser; + var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove; $dropdown = $(dropdown); options.projectId = $dropdown.data('project-id'); options.showCurrentUser = $dropdown.data('current-user'); showNullUser = $dropdown.data('null-user'); + showMenuAbove = $dropdown.data('showMenuAbove'); showAnyUser = $dropdown.data('any-user'); firstUser = $dropdown.data('first-user'); options.authorId = $dropdown.data('author-id'); @@ -70,9 +71,10 @@ return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); }); }; - collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>'); - assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/u/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>'); + collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>'); + assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>'); return $dropdown.glDropdown({ + showMenuAbove: showMenuAbove, data: function(term, callback) { var isAuthorFilter; isAuthorFilter = $('.js-author-search'); @@ -116,8 +118,11 @@ if (showDivider) { users.splice(showDivider, 0, "divider"); } - // Send the data back - return callback(users); + + callback(users); + if (showMenuAbove) { + $dropdown.data('glDropdown').positionMenuAbove(); + } }); }, filterable: true, @@ -127,8 +132,8 @@ }, selectable: true, fieldName: $dropdown.data('field-name'), - toggleLabel: function(selected) { - if (selected && 'id' in selected) { + toggleLabel: function(selected, el) { + if (selected && 'id' in selected && $(el).hasClass('is-active')) { if (selected.text) { return selected.text; } else { @@ -138,6 +143,7 @@ return defaultLabel; } }, + defaultLabel: defaultLabel, inputId: 'issue_assignee_id', hidden: function(e) { $selectbox.hide(); @@ -149,7 +155,9 @@ page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = (page === page && page === 'projects:merge_requests:index'); - if ($dropdown.hasClass('js-filter-bulk-update')) { + if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { + e.preventDefault(); + selectedId = user.id; return; } if (page === 'projects:boards:show') { @@ -167,6 +175,9 @@ return assignTo(selected); } }, + id: function (user) { + return user.id; + }, renderRow: function(user) { var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username; username = user.username ? "@" + user.username : ""; diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index c79b22d4d21..98e301d3799 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -4,7 +4,7 @@ width: 40px; height: 40px; padding: 0; - @include border-radius($avatar_radius); + border-radius: $avatar_radius; border: 1px solid rgba(0, 0, 0, .1); &.avatar-inline { @@ -17,7 +17,7 @@ } &.avatar-tile { - @include border-radius(0); + border-radius: 0; border: none; } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index d315db4cb32..8002e56724b 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -133,7 +133,7 @@ } .identicon { - @include border-radius(50%); + border-radius: 50%; } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index ce489f7c3de..a7c8d782e9b 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -1,5 +1,5 @@ @mixin btn-default { - @include border-radius(3px); + border-radius: 3px; font-size: $gl-font-size; font-weight: 500; padding: $gl-vert-padding $gl-btn-padding; @@ -8,7 +8,7 @@ &:active { outline: none; background-color: $btn-active-gray; - @include box-shadow($gl-btn-active-background); + box-shadow: $gl-btn-active-background; } } @@ -43,7 +43,7 @@ &:active, &.active { - @include box-shadow ($gl-btn-active-background); + box-shadow: $gl-btn-active-background; background-color: $dark; border-color: $border-dark; @@ -194,10 +194,17 @@ pointer-events: none !important; } - .caret { + .fa-caret-down, + .fa-caret-up { margin-left: 5px; } + &.dropdown-toggle { + .fa-caret-down { + margin-left: 3px; + } + } + svg { height: 15px; width: 15px; @@ -272,7 +279,7 @@ } .active { - @include box-shadow($gl-btn-active-background); + box-shadow: $gl-btn-active-background; border: 1px solid #c6cacf !important; background-color: #e4e7ed !important; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index b0ba112476b..baa95711329 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -1,20 +1,3 @@ -.caret { - display: inline-block; - width: 0; - height: 0; - margin-left: 2px; - vertical-align: middle; - border-top: $caret-width-base dashed; - border-right: $caret-width-base solid transparent; - border-left: $caret-width-base solid transparent; -} - -.btn-group { - .caret { - margin-left: 0; - } -} - .dropdown { position: relative; @@ -604,3 +587,9 @@ display: block; color: $gl-placeholder-color; } + +.dropdown-toggle-text { + &.is-default { + color: $gl-placeholder-color; + } +} diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index 3ac1678dd05..a55dcf4a699 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -21,7 +21,8 @@ .flash-notice, .flash-alert { border-radius: $border-radius-default; - .container-fluid.container-limited.flash-text { + .container-fluid, + .container-fluid.container-limited { background: transparent; } } @@ -35,12 +36,6 @@ } } -.content-wrapper { - .flash-notice .container-fluid { - background-color: transparent; - } -} - @media (max-width: $screen-md-min) { ul.notes { .flash-container.timeline-content { diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 37ff7e22ed1..05e8ee0190d 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -73,7 +73,7 @@ label { } .form-control { - @include box-shadow(none); + box-shadow: none; border-radius: 3px; padding: $gl-vert-padding $gl-input-padding; } @@ -81,10 +81,10 @@ label { .select-wrapper { position: relative; - .caret { + .fa-caret-down { position: absolute; right: 10px; - top: $gl-padding; + top: 10px; color: $gray-darkest; pointer-events: none; } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index c748f856501..9823abdde1f 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -57,6 +57,10 @@ header { &:hover, &:focus, &:active { background-color: $background-color; } + + .fa-caret-down { + font-size: 15px; + } } .navbar-toggle { diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 8bfc0d583c5..ba3930e03bd 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -16,7 +16,7 @@ margin-top: 5px; } - @include border-radius(3px); + border-radius: 3px; display: block; float: left; margin-right: 10px; diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index edea4ad00eb..6d28d98b283 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -86,7 +86,7 @@ } .markdown-area { - @include border-radius(0); + border-radius: 0; background: #fff; border: 1px solid #ddd; min-height: 140px; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 1ec08cdef23..7c207969b0a 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -1,15 +1,4 @@ /** - * Generic mixins - */ -@mixin box-shadow($shadow) { - box-shadow: $shadow; -} - -@mixin border-radius($radius) { - border-radius: $radius; -} - -/** * Prefilled mixins * Mixins with fixed values */ diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 76b93b23b95..9fe390eb09d 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -133,5 +133,5 @@ font-size: 20px; color: #777; z-index: 100; - @include box-shadow(0 1px 2px #ddd); + box-shadow: 0 1px 2px #ddd; } diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index c75dacf95d9..79cd26714a3 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -21,7 +21,14 @@ padding-right: 10px; b { - @extend .caret; + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: $caret-width-base dashed; + border-right: $caret-width-base solid transparent; + border-left: $caret-width-base solid transparent; color: $gray-darkest; } } @@ -39,8 +46,8 @@ } .select2-drop { - @include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0); - @include border-radius ($border-radius-default); + box-shadow: rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0; + border-radius: $border-radius-default; border: none; min-width: 175px; } @@ -65,7 +72,7 @@ .select2-container-active { .select2-choice, .select2-choices { - @include box-shadow(none); + box-shadow: none; } } @@ -75,13 +82,13 @@ outline: 0; background-image: none; background-color: $white-dark; - @include box-shadow($gl-btn-active-gradient); + box-shadow: $gl-btn-active-gradient; } } .select2-container-multi { .select2-choices { - @include border-radius($border-radius-default); + border-radius: $border-radius-default; border-color: $input-border; background: none; @@ -116,7 +123,7 @@ &.select2-container-active .select2-choices, &.select2-dropdown-open .select2-choices { border-color: $border-white-normal; - @include box-shadow($gl-btn-active-gradient); + box-shadow: $gl-btn-active-gradient; } } @@ -150,7 +157,7 @@ background-repeat: no-repeat; background-position: right 0 bottom 6px; border: 1px solid $input-border; - @include border-radius($border-radius-default); + border-radius: $border-radius-default; transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; &:focus { diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 557ef7291cf..ec52f326eb9 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -4,7 +4,7 @@ &.page-sidebar-pinned { .sidebar-wrapper { - @include box-shadow(none); + box-shadow: none; } } @@ -17,7 +17,7 @@ width: 0; overflow: hidden; transition: width $sidebar-transition-duration; - @include box-shadow(2px 0 16px 0 $black-transparent); + box-shadow: 2px 0 16px 0 $black-transparent; } } @@ -100,7 +100,7 @@ .count { float: right; padding: 0 8px; - @include border-radius(6px); + border-radius: 6px; } } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 9f2d53d5206..d099a884f54 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -116,7 +116,7 @@ font-size: 13px; line-height: 1.6em; overflow-x: auto; - @include border-radius(2px); + border-radius: 2px; } p > code { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 14ec310de2d..4c34ed3ebf7 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -17,8 +17,10 @@ $white-normal: #ededed; $white-dark: #ececec; $gray-light: #fafafa; +$gray-lighter: #f9f9f9; $gray-normal: #f5f5f5; $gray-dark: #ededed; +$gray-darker: #eee; $gray-darkest: #c9c9c9; $green-light: #38ae67; @@ -33,6 +35,8 @@ $blue-medium-light: #3498cb; $blue-medium: #2f8ebf; $blue-medium-dark: #2d86b4; +$blue-light-transparent: rgba(44, 159, 216, 0.05); + $orange-light: #fc8a51; $orange-normal: #e75e40; $orange-dark: #ce5237; @@ -91,6 +95,7 @@ $table-text-gray: #8f8f8f; $gl-font-size: 15px; $gl-title-color: #333; $gl-text-color: #5c5c5c; +$gl-text-color-light: #8c8c8c; $gl-text-green: #4a2; $gl-text-red: #d12f19; $gl-text-orange: #d90; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index ecc5b24e360..6e81c12aa55 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -162,6 +162,10 @@ lex list-style: none; overflow-y: scroll; overflow-x: hidden; + + &.is-smaller { + height: calc(100% - 185px); + } } .board-list-loading { @@ -233,3 +237,31 @@ lex margin-right: 5px; } } + +.board-new-issue-form { + margin: 5px; +} + +.board-issue-count-holder { + margin-top: -3px; + + .btn { + line-height: 12px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } +} + +.board-issue-count { + padding-right: 10px; + padding-left: 10px; + line-height: 21px; + border-radius: $border-radius-base; + border: 1px solid $border-color; + + &.has-btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-width: 1px 0 1px 1px; + } +} diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 778471a34d7..d732008de3d 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -50,7 +50,7 @@ .bordered-box { border: 1px solid $border-color; - @include border-radius($border-radius-default); + border-radius: $border-radius-default; } diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index e1304335271..fcc5f32c738 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -1,7 +1,7 @@ .file-editor { #editor { border: none; - @include border-radius(0); + border-radius: 0; height: 500px; margin: 0; padding: 0; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index d01c60ee6ab..3f19e920166 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -1,4 +1,15 @@ +.environments-container, +.deployments-container { + width: 100%; + overflow: auto; +} + .environments { + .deployment-column { + .avatar { + float: none; + } + } .commit-title { margin: 0; @@ -9,6 +20,7 @@ width: 12px; } + .external-url, .dropdown-new { color: $table-text-gray; } @@ -21,16 +33,35 @@ } } + .build-link, .branch-name { color: $gl-dark-link-color; } + + .deployment { + .build-column { + + .build-link { + color: $gl-dark-link-color; + } + + .avatar { + float: none; + } + } + } } .table.builds.environments { - min-width: 500px; .icon-container { width: 20px; text-align: center; } + + .branch-commit { + .commit-id { + margin-right: 0; + } + } } diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 1d00da1266c..789d6237df8 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -91,7 +91,7 @@ float: right; border: 1px solid #eee; padding: 5px; - @include border-radius(5px); + border-radius: 5px; background: $gray-light; margin-left: 10px; top: -6px; diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 38c7cd98e41..701c29a3986 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -1,7 +1,7 @@ .suggest-colors { margin-top: 5px; a { - @include border-radius(4px); + border-radius: 4px; width: 30px; height: 30px; display: inline-block; @@ -17,7 +17,7 @@ overflow: hidden; a { - @include border-radius(0); + border-radius: 0; width: (100% / 7); margin-right: 0; margin-bottom: -5px; @@ -59,6 +59,13 @@ width: 200px; margin-bottom: 0; } + + .label { + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; + max-width: 100%; + } } .label-description { diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 403171d4532..a5ca509163d 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -73,12 +73,12 @@ height: auto; &.top { - @include border-radius(5px 5px 0 0); + border-radius: 5px 5px 0 0; margin-bottom: 0; } &.bottom { - @include border-radius(0 0 5px 5px); + border-radius: 0 0 5px 5px; border-top: 0; margin-bottom: 20px; } @@ -86,7 +86,7 @@ &.middle { border-top: 0; margin-bottom: 0; - @include border-radius(0); + border-radius: 0; } &:active, &:focus { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 926247e5e87..1006b3e62e8 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -6,7 +6,7 @@ background: $background-color; color: $gl-gray; border: 1px solid $border-color; - @include border-radius(2px); + border-radius: 2px; form { margin-bottom: 0; @@ -70,7 +70,8 @@ &.ci-success { color: $gl-success; - a.environment { + a.environment, + a.pipeline { color: inherit; } } @@ -203,6 +204,18 @@ word-break: break-all; } +.commits-empty { + text-align: center; + + h4 { + padding-top: 20px; + padding-bottom: 10px; + } + svg { + width: 230px; + } +} + .mr-list { .merge-request { padding: 10px 15px; @@ -349,6 +362,10 @@ .issuable-form-select-holder { display: inline-block; width: 250px; + + .dropdown-menu-toggle { + width: 100%; + } } .table-holder { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 54124a3d658..d399f84a2ff 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -334,7 +334,7 @@ ul.notes { .add-diff-note { margin-top: -4px; - @include border-radius(40px); + border-radius: 40px; background: #fff; padding: 4px; font-size: 16px; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index b035bfc9f3c..05f59279637 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -22,6 +22,11 @@ .table.builds { min-width: 1200px; + + .branch-commit { + width: 33%; + } + } } @@ -224,9 +229,12 @@ .fa { color: $table-text-gray; - margin-right: 6px; font-size: 14px; } + + svg, .fa { + margin-right: 0; + } } .btn-remove { @@ -267,18 +275,8 @@ .toggle-pipeline-btn { background-color: $gray-dark; - .caret { - border-top: none; - border-bottom: 4px solid; - } - &.graph-collapsed { background-color: $white-light; - - .caret { - border-bottom: none; - border-top: 4px solid; - } } } @@ -305,16 +303,41 @@ .stage-column { display: inline-block; vertical-align: top; - margin-right: 65px; + + &:not(:last-child) { + margin-right: 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; + } + } + } + } + + &.no-margin { + margin: 0; + } li { list-style: none; } .stage-name { - margin-bottom: 15px; + margin: 0 0 15px 10px; font-weight: bold; - width: 150px; + width: 176px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -323,17 +346,23 @@ .build { border: 1px solid $border-color; position: relative; - padding: 6px 10px; + padding: 7px 10px 8px; border-radius: 30px; - width: 150px; + width: 186px; margin-bottom: 10px; + &:hover { + background-color: $gray-lighter; + .dropdown-menu-toggle { + background-color: transparent; + } + } + &.playable { - background-color: $gray-light; svg { - height: 12px; - width: 12px; + height: 13px; + width: 20px; position: relative; top: 1px; @@ -344,10 +373,20 @@ } .build-content { - width: 130px; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + width: 164px; + + .ci-status-icon { + svg { + height: 20px; + width: 20px; + } + } .ci-status-text { - width: 110px; + width: 135px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -358,42 +397,53 @@ } a { - color: $layout-link-gray; + color: $gl-text-color-light; text-decoration: none; - - &:hover { - .ci-status-text { - text-decoration: underline; - } - } } .dropdown-menu-toggle { border: none; width: auto; padding: 0; - color: $layout-link-gray; + color: $gl-text-color-light; + flex-grow: 1; .ci-status-text { - width: 80px; + max-width: 112px; + width: auto; } } .grouped-pipeline-dropdown { padding: 8px 0; - width: 200px; + width: 186px; left: auto; - right: -214px; + right: -197px; top: -9px; + max-height: 245px; + overflow-y: scroll; + + a { + color: $gl-text-color; + padding: 7px 8px 8px; - a:hover { - .ci-status-text { - text-decoration: none; + &:hover { + background-color: $blue-light-transparent; + border-radius: 3px; + + .ci-status-text { + text-decoration: none; + } } } + svg { + width: 14px; + height: 14px; + } + .ci-status-text { - width: 145px; + width: 112px; } .arrow { @@ -426,9 +476,10 @@ } .badge { - background-color: $gray-dark; - color: $layout-link-gray; + background-color: $gray-darker; + color: $gl-text-color-light; font-weight: normal; + margin-left: $btn-xs-side-margin; } } @@ -442,10 +493,10 @@ &::after { content: ''; position: absolute; - top: 50%; - right: -69px; + top: 48%; + right: -48px; border-top: 2px solid $border-color; - width: 69px; + width: 48px; height: 1px; } } @@ -454,25 +505,25 @@ &:not(:first-child) { &::after, &::before { content: ''; - top: -47px; + top: -49px; position: absolute; border-bottom: 2px solid $border-color; - width: 20px; - height: 65px; + width: 25px; + height: 69px; } // Right connecting curves &::after { - right: -20px; + right: -25px; border-right: 2px solid $border-color; - border-radius: 0 0 15px; + border-radius: 0 0 20px; } // Left connecting curves &::before { - left: -20px; + left: -25px; border-left: 2px solid $border-color; - border-radius: 0 0 0 15px; + border-radius: 0 0 0 20px; } } @@ -480,7 +531,7 @@ &:nth-child(2) { &::after, &::before { height: 29px; - top: -10px; + top: -9px; } .curve { display: block; @@ -538,20 +589,20 @@ width: 21px; height: 25px; position: absolute; - top: -29px; + top: -32px; border-top: 2px solid $border-color; } &::after { - left: -39px; + left: -44px; border-right: 2px solid $border-color; - border-radius: 0 15px; + border-radius: 0 20px; } &::before { - right: -39px; + right: -44px; border-left: 2px solid $border-color; - border-radius: 15px 0 0; + border-radius: 20px 0 0; } } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 0fcdaf94a21..c7eac5cf4b9 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -94,7 +94,7 @@ .profile-user-bio { // Limits the width of the user bio for readability. max-width: 600px; - margin: 15px auto 0; + margin: 10px auto; padding: 0 16px; } @@ -213,29 +213,22 @@ } .user-profile { + .cover-controls a { margin-left: 5px; } + .profile-header { margin: 0 auto; + .avatar-holder { width: 90px; - display: inline-block; - } - .user-info { - display: inline-block; - text-align: left; - vertical-align: middle; - margin-left: 15px; - .handle { - color: $gl-gray-light; - } - .member-date { - margin-bottom: 4px; - } + margin: 0 auto 10px; } } + @media (max-width: $screen-xs-max) { + .cover-block { padding-top: 20px; } @@ -258,10 +251,6 @@ } } -.user-profile-nav { - margin-top: 15px; -} - table.u2f-registrations { th:not(:last-child), td:not(:last-child) { border-right: solid 1px transparent; diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss index e5859fe7384..f8da0983b77 100644 --- a/app/assets/stylesheets/pages/profiles/preferences.scss +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -4,7 +4,7 @@ text-align: center; .preview { - @include border-radius(4px); + border-radius: 4px; height: 80px; margin-bottom: 10px; @@ -47,7 +47,7 @@ width: 160px; img { - @include border-radius(4px); + border-radius: 4px; max-width: 100%; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 78bc4b79e86..530fb0c0d05 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -146,7 +146,8 @@ } .project-repo-btn-group, - .notification-dropdown { + .notification-dropdown, + .project-dropdown { margin-left: 10px; } @@ -353,7 +354,7 @@ a.deploy-project-label { justify-content: flex-start; .fork-thumbnail { - @include border-radius($border-radius-base); + border-radius: $border-radius-base; background-color: $white-light; border: 1px solid $border-white-light; height: 202px; @@ -370,7 +371,7 @@ a.deploy-project-label { background-color: $gray-light; border: 1px solid $gray-dark; margin: 0 auto; - @include border-radius(50%); + border-radius: 50%; i { font-size: 100px; color: $gray-dark; @@ -389,7 +390,7 @@ a.deploy-project-label { } img { - @include border-radius(50%); + border-radius: 50%; max-width: 100px; } } @@ -495,7 +496,7 @@ pre.light-well { } .light-well { - @include border-radius (2px); + border-radius: 2px; color: #5b6169; font-size: 13px; diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 0ee7ceecae5..c05f3d5ff32 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -4,7 +4,7 @@ margin-right: 10px; border: 1px solid #eee; white-space: nowrap; - @include border-radius(4px); + border-radius: 4px; &:hover { text-decoration: none; diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 68a5d1ae06c..ea76fe18876 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -51,6 +51,7 @@ -webkit-flex-direction: column; flex-direction: column; margin-left: 10px; + min-width: 55px; } .todo-item { @@ -120,6 +121,14 @@ } } +@media (max-width: $screen-sm-max) { + .todos-filters { + .dropdown-menu-toggle { + width: 135px; + } + } +} + @media (max-width: $screen-xs-max) { .todo { .avatar { @@ -141,4 +150,14 @@ padding-left: 10px; } } + + .todos-filters { + .row-content-block { + padding-bottom: 50px; + } + + .dropdown-menu-toggle { + width: 100%; + } + } } diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index 82055006ac0..762e36ee2e9 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -37,7 +37,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController end def preview - @message = broadcast_message_params[:message] + @broadcast_message = BroadcastMessage.new(broadcast_message_params) end protected diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0e895a7857e..a4774e4d077 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -173,11 +173,9 @@ class ApplicationController < ActionController::Base end def event_filter - if cookies['event_filter'].present? - filters = cookies['event_filter'].split(',') - else - filters = EventFilter.all - end + # Split using comma to maintain backward compatibility Ex/ "filter1,filter2" + filters = cookies['event_filter'].split(',')[0] if cookies['event_filter'].present? + @event_filter ||= EventFilter.new(filters) end diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb deleted file mode 100644 index 5bb7d499cdc..00000000000 --- a/app/controllers/ci/application_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Ci - class ApplicationController < ::ApplicationController - def self.railtie_helpers_paths - "app/helpers/ci" - end - end -end diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb index 78012960252..3eb485de9db 100644 --- a/app/controllers/ci/lints_controller.rb +++ b/app/controllers/ci/lints_controller.rb @@ -1,5 +1,5 @@ module Ci - class LintsController < ApplicationController + class LintsController < ::ApplicationController before_action :authenticate_user! def show diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index aa894fde36b..ff297d6ff13 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -1,5 +1,5 @@ module Ci - class ProjectsController < Ci::ApplicationController + class ProjectsController < ::ApplicationController before_action :project before_action :no_cache, only: [:badge] before_action :authorize_read_project!, except: [:badge, :index] diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb index d5a8a962662..4c497711fc0 100644 --- a/app/controllers/concerns/authenticates_with_two_factor.rb +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor # # Returns nil def prompt_for_two_factor(user) + return locked_user_redirect(user) if user.access_locked? + session[:otp_user_id] = user.id setup_u2f_authentication(user) render 'devise/sessions/two_factor' end + def locked_user_redirect(user) + flash.now[:alert] = 'Invalid Login or password' + render 'devise/sessions/new' + end + def authenticate_with_two_factor user = self.resource = find_user - if user_params[:otp_attempt].present? && session[:otp_user_id] + if user.access_locked? + locked_user_redirect(user) + elsif user_params[:otp_attempt].present? && session[:otp_user_id] authenticate_with_two_factor_via_otp(user) elsif user_params[:device_response].present? && session[:otp_user_id] authenticate_with_two_factor_via_u2f(user) @@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor remember_me(user) if user_params[:remember_me] == '1' sign_in(user) else + user.increment_failed_attempts! flash.now[:alert] = 'Invalid two-factor code.' - render :two_factor + prompt_for_two_factor(user) end end @@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor remember_me(user) if user_params[:remember_me] == '1' sign_in(user) else + user.increment_failed_attempts! flash.now[:alert] = 'Authentication via U2F device failed.' prompt_for_two_factor(user) end diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index b8ed2c159a7..c13333641d3 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -15,18 +15,17 @@ module MembershipActions end def leave - @member = membershipable.members.find_by(user_id: current_user) || - membershipable.requesters.find_by(user_id: current_user) - Members::DestroyService.new(@member, current_user).execute + member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id). + execute(:all) - source_type = @member.real_source_type.humanize(capitalize: false) + source_type = membershipable.class.to_s.humanize(capitalize: false) notice = - if @member.request? + if member.request? "Your access request to the #{source_type} has been withdrawn." else - "You left the \"#{@member.source.human_name}\" #{source_type}." + "You left the \"#{membershipable.human_name}\" #{source_type}." end - redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize] + redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize] redirect_to redirect_path, notice: notice end diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb index 29e243c66a3..99acd98ae13 100644 --- a/app/controllers/concerns/spammable_actions.rb +++ b/app/controllers/concerns/spammable_actions.rb @@ -7,7 +7,7 @@ module SpammableActions def mark_as_spam if SpamService.new(spammable).mark_as_spam! - redirect_to spammable, notice: "#{spammable.class.to_s} was submitted to Akismet successfully." + redirect_to spammable, notice: "#{spammable.class} was submitted to Akismet successfully." else redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.' end diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 88a0c18180b..a62c6211372 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -21,8 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController end def trending - @projects = TrendingProjectsFinder.new.execute(current_user) - @projects = filter_projects(@projects) + @projects = filter_projects(Project.trending) @projects = @projects.page(params[:page]) respond_to do |format| diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 9c323d7705a..18cd800c619 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -40,10 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController end def destroy - @group_member = @group.members.find_by(id: params[:id]) || - @group.requesters.find_by(id: params[:id]) - - Members::DestroyService.new(@group_member, current_user).execute + Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all) respond_to do |format| format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb deleted file mode 100644 index 83eec1bf4a2..00000000000 --- a/app/controllers/namespaces_controller.rb +++ /dev/null @@ -1,25 +0,0 @@ -class NamespacesController < ApplicationController - skip_before_action :authenticate_user! - - def show - namespace = Namespace.find_by(path: params[:id]) - - if namespace - if namespace.is_a?(Group) - group = namespace - else - user = namespace.owner - end - end - - if user - redirect_to user_path(user) - elsif group && can?(current_user, :read_group, group) - redirect_to group_path(group) - elsif current_user.nil? - authenticate_user! - else - render_404 - end - end -end diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index 4aa7982eab4..095af6c35eb 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -2,6 +2,7 @@ module Projects module Boards class IssuesController < Boards::ApplicationController before_action :authorize_read_issue!, only: [:index] + before_action :authorize_create_issue!, only: [:create] before_action :authorize_update_issue!, only: [:update] def index @@ -9,16 +10,23 @@ module Projects issues = issues.page(params[:page]) render json: { - issues: issues.as_json( - only: [:iid, :title, :confidential], - include: { - assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, - labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] } - }), + issues: serialize_as_json(issues), size: issues.total_count } end + def create + list = project.board.lists.find(params[:list_id]) + service = ::Boards::Issues::CreateService.new(project, current_user, issue_params) + issue = service.execute(list) + + if issue.valid? + render json: serialize_as_json(issue) + else + render json: issue.errors, status: :unprocessable_entity + end + end + def update service = ::Boards::Issues::MoveService.new(project, current_user, move_params) @@ -43,6 +51,10 @@ module Projects return render_403 unless can?(current_user, :read_issue, project) end + def authorize_create_issue! + return render_403 unless can?(current_user, :admin_issue, project) + end + def authorize_update_issue! return render_403 unless can?(current_user, :update_issue, issue) end @@ -54,6 +66,19 @@ module Projects def move_params params.permit(:id, :from_list_id, :to_list_id) end + + def issue_params + params.require(:issue).permit(:title).merge(request: request) + end + + def serialize_as_json(resource) + resource.as_json( + only: [:iid, :title, :confidential], + include: { + assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, + labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] } + }) + end end end end diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 33206717089..0035633b774 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -1,4 +1,6 @@ class Projects::BoardsController < Projects::ApplicationController + include IssuableCollections + respond_to :html before_action :authorize_read_board!, only: [:show] diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index d0c4550733c..7a7475a7345 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -4,17 +4,25 @@ class Projects::GroupLinksController < Projects::ApplicationController def index @group_links = project.project_group_links.all + + @skip_groups = @group_links.pluck(:group_id) + @skip_groups << project.group.try(:id) end def create - group = Group.find(params[:link_group_id]) - return render_404 unless can?(current_user, :read_group, group) - - project.project_group_links.create( - group: group, - group_access: params[:link_group_access], - expires_at: params[:expires_at] - ) + group = Group.find(params[:link_group_id]) if params[:link_group_id].present? + + if group + return render_404 unless can?(current_user, :read_group, group) + + project.project_group_links.create( + group: group, + group_access: params[:link_group_access], + expires_at: params[:expires_at] + ) + else + flash[:alert] = 'Please select a group.' + end redirect_to namespace_project_group_links_path(project.namespace, project) end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index ef13e0677d2..96041b07647 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -159,7 +159,8 @@ class Projects::IssuesController < Projects::ApplicationController protected def issue - @noteable = @issue ||= @project.issues.find_by(iid: params[:id]) || redirect_old + # The Sortable default scope causes performance issues when used with find_by + @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old end alias_method :subscribable_resource, :issue alias_method :issuable, :issue diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 28fa4a5b141..a6626df4826 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -30,9 +30,15 @@ class Projects::LabelsController < Projects::ApplicationController @label = @project.labels.create(label_params) if @label.valid? - redirect_to namespace_project_labels_path(@project.namespace, @project) + respond_to do |format| + format.html { redirect_to namespace_project_labels_path(@project.namespace, @project) } + format.json { render json: @label } + end else - render 'new' + respond_to do |format| + format.html { render 'new' } + format.json { render json: { message: @label.errors.messages }, status: 400 } + end end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 8c8c56228ad..869d96b86f4 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ :edit, :update, :show, :diffs, :commits, :conflicts, :builds, :pipelines, :merge, :merge_check, - :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts + :ci_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 :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :builds, :pipelines] @@ -19,6 +19,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :define_diff_comment_vars, only: [:diffs] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines] before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines] + before_action :apply_diff_view_cookie!, only: [:new_diffs] + before_action :build_merge_request, only: [:new, :new_diffs] # Allow read any merge_request before_action :authorize_read_merge_request! @@ -29,6 +31,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController # Allow modify merge_request before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort] + before_action :authenticate_user!, only: [:assign_related_issues] + before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts] def index @@ -210,29 +214,26 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def new - apply_diff_view_cookie! - - build_merge_request - @noteable = @merge_request - - @target_branches = if @merge_request.target_project - @merge_request.target_project.repository.branch_names - else - [] - end - - @target_project = merge_request.target_project - @source_project = merge_request.source_project - @commits = @merge_request.compare_commits.reverse - @commit = @merge_request.diff_head_commit - @base_commit = @merge_request.diff_base_commit - @diffs = @merge_request.diffs(diff_options) if @merge_request.compare - @diff_notes_disabled = true - @pipeline = @merge_request.pipeline - @statuses = @pipeline.statuses.relevant if @pipeline + define_new_vars + end - @note_counts = Note.where(commit_id: @commits.map(&:id)). - group(:commit_id).count + def new_diffs + respond_to do |format| + format.html do + define_new_vars + render "new" + end + format.json do + @diffs = if @merge_request.can_be_created + @merge_request.diffs(diff_options) + else + [] + end + @diff_notes_disabled = true + + render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) } + end + end end def create @@ -355,6 +356,25 @@ class Projects::MergeRequestsController < Projects::ApplicationController render layout: false end + def assign_related_issues + result = MergeRequests::AssignIssuesService.new(project, current_user, merge_request: @merge_request).execute + + respond_to do |format| + format.html do + case result[:count] + when 0 + flash[:error] = "Failed to assign you issues related to the merge request" + when 1 + flash[:notice] = "1 issue has been assigned to you" + else + flash[:notice] = "#{result[:count]} issues have been assigned to you" + end + + redirect_to(merge_request_path(@merge_request)) + end + end + end + def ci_status pipeline = @merge_request.pipeline if pipeline @@ -490,6 +510,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController ) end + def define_new_vars + @noteable = @merge_request + + @target_branches = if @merge_request.target_project + @merge_request.target_project.repository.branch_names + else + [] + end + + @target_project = merge_request.target_project + @source_project = merge_request.source_project + @commits = @merge_request.compare_commits.reverse + @commit = @merge_request.diff_head_commit + @base_commit = @merge_request.diff_base_commit + + @pipeline = @merge_request.pipeline + @statuses = @pipeline.statuses.relevant if @pipeline + @note_counts = Note.where(commit_id: @commits.map(&:id)). + group(:commit_id).count + end + def invalid_mr # Render special view for MR with removed target branch render 'invalid' @@ -521,7 +562,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def build_merge_request params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) - @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute + @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute end def compared_diff_version diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 2343c7d20ec..f56b256984b 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -55,10 +55,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def destroy - @project_member = @project.members.find_by(id: params[:id]) || - @project.requesters.find_by(id: params[:id]) - - Members::DestroyService.new(@project_member, current_user).execute + Members::DestroyService.new(@project, current_user, params). + execute(:all) respond_to do |format| format.html do diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 6ea8ee62bc5..8fea20cefef 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -20,6 +20,8 @@ class Projects::TagsController < Projects::ApplicationController def show @tag = @repository.find_tag(params[:id]) + return render_404 unless @tag + @release = @project.releases.find_or_initialize_by(tag: @tag.name) @commit = @repository.commit(@tag.target) end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index d198782138a..dee57e4a388 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,10 +1,10 @@ class SnippetsController < ApplicationController include ToggleAwardEmoji - before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] # Allow read snippet - before_action :authorize_read_snippet!, only: [:show, :raw] + before_action :authorize_read_snippet!, only: [:show, :raw, :download] # Allow modify snippet before_action :authorize_update_snippet!, only: [:edit, :update] @@ -12,7 +12,7 @@ class SnippetsController < ApplicationController # Allow destroy snippet before_action :authorize_admin_snippet!, only: [:destroy] - skip_before_action :authenticate_user!, only: [:index, :show, :raw] + skip_before_action :authenticate_user!, only: [:index, :show, :raw, :download] layout 'snippets' respond_to :html @@ -75,6 +75,14 @@ class SnippetsController < ApplicationController ) end + def download + send_data( + @snippet.content, + type: 'text/plain; charset=utf-8', + filename: @snippet.sanitized_file_name + ) + end + protected def snippet diff --git a/app/finders/trending_projects_finder.rb b/app/finders/trending_projects_finder.rb deleted file mode 100644 index 81a12403801..00000000000 --- a/app/finders/trending_projects_finder.rb +++ /dev/null @@ -1,11 +0,0 @@ -class TrendingProjectsFinder - def execute(current_user, start_date = 1.month.ago) - projects_for(current_user).trending(start_date) - end - - private - - def projects_for(current_user) - ProjectsFinder.new.execute(current_user) - end -end diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index de13e7a1fc2..16136d02530 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -16,7 +16,7 @@ module AppearancesHelper end def brand_text - markdown(brand_item.description) + markdown_field(brand_item, :description) end def brand_item diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 6de25bea654..6229384817b 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -11,18 +11,6 @@ module ApplicationSettingsHelper current_application_settings.signin_enabled? end - def extra_sign_in_text - current_application_settings.sign_in_text - end - - def after_sign_up_text - current_application_settings.after_sign_up_text - end - - def shared_runners_text - current_application_settings.shared_runners_text - end - def user_oauth_applications? current_application_settings.user_oauth_applications end diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index df41473543b..b7e0ff8ecd0 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -4,15 +4,18 @@ module AvatarsHelper user: commit_or_event.author, user_name: commit_or_event.author_name, user_email: commit_or_event.author_email, + css_class: 'hidden-xs' })) end def user_avatar(options = {}) avatar_size = options[:size] || 16 user_name = options[:user].try(:name) || options[:user_name] + css_class = options[:css_class] || '' + avatar = image_tag( avatar_icon(options[:user] || options[:user_email], avatar_size), - class: "avatar has-tooltip hidden-xs s#{avatar_size}", + class: "avatar has-tooltip s#{avatar_size} #{css_class}", alt: "#{user_name}'s avatar", title: user_name, data: { container: 'body' } diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb index 43a29c96bca..eb03ced67eb 100644 --- a/app/helpers/broadcast_messages_helper.rb +++ b/app/helpers/broadcast_messages_helper.rb @@ -3,7 +3,7 @@ module BroadcastMessagesHelper return unless message.present? content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do - icon('bullhorn') << ' ' << render_broadcast_message(message.message) + icon('bullhorn') << ' ' << render_broadcast_message(message) end end @@ -32,7 +32,7 @@ module BroadcastMessagesHelper end end - def render_broadcast_message(message) - Banzai.render(message, pipeline: :broadcast_message).html_safe + def render_broadcast_message(broadcast_message) + Banzai.render_field(broadcast_message, :message).html_safe end end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index b478580978b..a695aceea76 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -15,10 +15,11 @@ module ButtonHelper # # See http://clipboardjs.com/#usage def clipboard_button(data = {}) + css_class = data[:class] || 'btn-clipboard' data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) content_tag :button, icon('clipboard'), - class: "btn btn-clipboard", + class: "btn #{css_class}", data: data, type: :button, title: "Copy to Clipboard" diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 4566f3782cc..81e0b6bb5ae 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -40,8 +40,9 @@ module DropdownsHelper end def dropdown_toggle(toggle_text, data_attr, options = {}) + 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") + output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}") output << icon('chevron-down') output.html_safe end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 1a259656f31..0772d848289 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -13,14 +13,12 @@ module GitlabMarkdownHelper def link_to_gfm(body, url, html_options = {}) return "" if body.blank? - escaped_body = if body.start_with?('<img') - body - else - escape_once(body) - end - - user = current_user if defined?(current_user) - gfm_body = Banzai.render(escaped_body, project: @project, current_user: user, pipeline: :single_line) + context = { + project: @project, + current_user: (current_user if defined?(current_user)), + pipeline: :single_line, + } + gfm_body = Banzai.render(body, context) fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body) if fragment.children.size == 1 && fragment.children[0].name == 'a' @@ -51,17 +49,15 @@ module GitlabMarkdownHelper context[:project] ||= @project html = Banzai.render(text, context) + banzai_postprocess(html, context) + end - context.merge!( - current_user: (current_user if defined?(current_user)), - - # RelativeLinkFilter - requested_path: @path, - project_wiki: @project_wiki, - ref: @ref - ) + def markdown_field(object, field) + object = object.for_display if object.respond_to?(:for_display) + return "" unless object.present? - Banzai.post_process(html, context) + html = Banzai.render_field(object, field) + banzai_postprocess(html, object.banzai_render_context(field)) end def asciidoc(text) @@ -196,4 +192,18 @@ module GitlabMarkdownHelper icon(options[:icon]) end end + + # Calls Banzai.post_process with some common context options + def banzai_postprocess(html, context) + context.merge!( + current_user: (current_user if defined?(current_user)), + + # RelativeLinkFilter + requested_path: @path, + project_wiki: @project_wiki, + ref: @ref + ) + + Banzai.post_process(html, context) + end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 8c04200fab9..692fadd505f 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -8,18 +8,12 @@ module IssuablesHelper end def multi_label_name(current_labels, default_label) - # current_labels may be a string from before - if current_labels.is_a?(Array) - if current_labels.count > 1 - "#{current_labels[0]} +#{current_labels.count - 1} more" + if current_labels && current_labels.any? + title = current_labels.first.try(:title) + if current_labels.size > 1 + "#{title} +#{current_labels.size - 1} more" else - current_labels[0] - end - elsif current_labels.is_a?(String) - if current_labels.nil? || current_labels.empty? - default_label - else - current_labels + title end else default_label diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 8b212b0327a..1644c346dd8 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -113,14 +113,13 @@ module IssuesHelper end end - def award_user_list(awards, current_user) + def award_user_list(awards, current_user, limit: 10) names = awards.map do |award| award.user == current_user ? 'You' : award.user.name end - # Take first 9 OR current user + first 9 current_user_name = names.delete('You') - names = names.first(9).insert(0, current_user_name).compact + names = names.insert(0, current_user_name).compact.first(limit) names << "#{awards.size - names.size} more." if awards.size > names.size diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 5e9f5837101..b9f3d6c75c2 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -115,8 +115,9 @@ module LabelsHelper end def labels_filter_path - if @project - namespace_project_labels_path(@project.namespace, @project, :json) + project = @target_project || @project + if project + namespace_project_labels_path(project.namespace, project, :json) else dashboard_labels_path(:json) end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 8abe7865fed..b0a76765d97 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -72,6 +72,19 @@ module MergeRequestsHelper ) end + def mr_assign_issues_link + issues = MergeRequests::AssignIssuesService.new(@project, + current_user, + merge_request: @merge_request, + closes_issues: mr_closes_issues + ).assignable_issues + path = assign_related_issues_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) + if issues.present? + pluralize_this_issue = issues.count > 1 ? "these issues" : "this issue" + link_to "Assign yourself to #{pluralize_this_issue}", path, method: :post + end + end + def source_branch_with_namespace(merge_request) branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch)) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index a11c313a6b8..83a2a4ad3ec 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -71,8 +71,9 @@ module MilestonesHelper end def milestones_filter_dropdown_path - if @project - namespace_project_milestones_path(@project.namespace, @project, :json) + project = @target_project || @project + if project + namespace_project_milestones_path(project.namespace, project, :json) else dashboard_milestones_path(:json) end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 22387d66451..7d4d049101a 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -92,12 +92,8 @@ module PageLayoutHelper end end - def fluid_layout(enabled = false) - if @fluid_layout.nil? - @fluid_layout = (current_user && current_user.layout == "fluid") || enabled - else - @fluid_layout - end + def fluid_layout + current_user && current_user.layout == "fluid" end def blank_container(enabled = false) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 56477733ea2..e667c9e4e2e 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -139,7 +139,7 @@ module ProjectsHelper end options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field)) - content_tag(:select, options, name: "project[project_feature_attributes][#{field.to_s}]", id: "project_project_feature_attributes_#{field.to_s}", class: "pull-right form-control", data: { field: field }).html_safe + content_tag(:select, options, name: "project[project_feature_attributes][#{field}]", id: "project_project_feature_attributes_#{field}", class: "pull-right form-control", data: { field: field }).html_safe end private diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 8a7446b7cc7..aba3a3f9c5d 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -153,8 +153,18 @@ module SearchHelper search_path(options) end - # Sanitize html generated after parsing markdown from issue description or comment - def search_md_sanitize(html) + # Sanitize a HTML field for search display. Most tags are stripped out and the + # maximum length is set to 200 characters. + def search_md_sanitize(object, field) + html = markdown_field(object, field) + html = Truncato.truncate( + html, + count_tags: false, + count_tail: false, + max_length: 200 + ) + + # Truncato's filtered_tags and filtered_attributes are not quite the same sanitize(html, tags: %w(a p ol ul li pre code)) end end diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 5f27e33c6ad..8706876ae4a 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -49,12 +49,10 @@ module SelectsHelper end def select2_tag(id, opts = {}) - css_class = '' - css_class << 'multiselect ' if opts[:multiple] - css_class << (opts[:class] || '') + opts[:class] << ' multiselect' if opts[:multiple] value = opts[:selected] || '' - hidden_field_tag(id, value, class: css_class) + hidden_field_tag(id, value, opts) end private diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 1e86f648203..a9db8bb2b82 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -114,6 +114,26 @@ module TodosHelper selected_type ? selected_type[:text] : default_type end + def todo_due_date(todo) + return unless todo.target.try(:due_date) + + is_due_today = todo.target.due_date.today? + is_overdue = todo.target.overdue? + css_class = + if is_due_today + 'text-warning' + elsif is_overdue + 'text-danger' + else + '' + end + + html = "· ".html_safe + html << content_tag(:span, class: css_class) do + "Due #{is_due_today ? "today" : todo.target.due_date.to_s(:medium)}" + end + end + private def show_todo_state?(todo) diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb index 415f6e12885..f7ed61625f4 100644 --- a/app/mailers/devise_mailer.rb +++ b/app/mailers/devise_mailer.rb @@ -3,4 +3,12 @@ class DeviseMailer < Devise::Mailer default reply_to: Gitlab.config.gitlab.email_reply_to layout 'devise_mailer' + + protected + + def subject_for(key) + subject = super + subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present? + subject + end end diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index 45311690293..7b617b359ea 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -45,7 +45,7 @@ module Emails @token = token mail(to: member.invite_email, - subject: "Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}") + subject: subject("Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}")) end def member_invite_accepted_email(member_source_type, member_id) diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 29f1c527776..2444702104e 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -92,6 +92,7 @@ class Notify < BaseMailer subject = "" subject << "#{@project.name} | " if @project subject << extra.join(' | ') if extra.present? + subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present? subject end diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb index b01a244032d..2340453831e 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -1,4 +1,8 @@ class AbuseReport < ActiveRecord::Base + include CacheMarkdownField + + cache_markdown_field :message, pipeline: :single_line + belongs_to :reporter, class_name: 'User' belongs_to :user @@ -7,6 +11,9 @@ class AbuseReport < ActiveRecord::Base validates :message, presence: true validates :user_id, uniqueness: { message: 'has already been reported' } + # For CacheMarkdownField + alias_method :author, :reporter + def remove_user(deleted_by:) user.block DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true) diff --git a/app/models/appearance.rb b/app/models/appearance.rb index 4cf8dd9a8ce..e4106e1c2e9 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -1,4 +1,8 @@ class Appearance < ActiveRecord::Base + include CacheMarkdownField + + cache_markdown_field :description + validates :title, presence: true validates :description, presence: true validates :logo, file_size: { maximum: 1.megabyte } diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 55d2e07de08..c99aa7772bb 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -1,5 +1,7 @@ class ApplicationSetting < ActiveRecord::Base + include CacheMarkdownField include TokenAuthenticatable + add_authentication_token_field :runners_registration_token add_authentication_token_field :health_check_access_token @@ -17,6 +19,11 @@ class ApplicationSetting < ActiveRecord::Base serialize :domain_whitelist, Array serialize :domain_blacklist, Array + cache_markdown_field :sign_in_text + cache_markdown_field :help_page_text + cache_markdown_field :shared_runners_text, pipeline: :plain_markdown + cache_markdown_field :after_sign_up_text + attr_accessor :domain_whitelist_raw, :domain_blacklist_raw validates :session_expire_delay, diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index 61498140f27..cb40f33932a 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -1,6 +1,9 @@ class BroadcastMessage < ActiveRecord::Base + include CacheMarkdownField include Sortable + cache_markdown_field :message, pipeline: :broadcast_message + validates :message, presence: true validates :starts_at, presence: true validates :ends_at, presence: true diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 663c5b1e231..2cf9892edc5 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -196,7 +196,7 @@ module Ci end def has_warnings? - builds.latest.ignored.any? + builds.latest.failed_but_allowed.any? end def config_processor @@ -251,9 +251,8 @@ module Ci Ci::ProcessPipelineService.new(project, user).execute(self) end - def build_updated + def update_status with_lock do - reload case latest_builds_status when 'pending' then enqueue when 'running' then run diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index ed5d4b13b7e..44cb19ece3b 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -2,7 +2,7 @@ module Ci class Runner < ActiveRecord::Base extend Ci::Model - LAST_CONTACT_TIME = 2.hours.ago + LAST_CONTACT_TIME = 1.hour.ago AVAILABLE_SCOPES = %w[specific shared active paused online] FORM_EDITABLE = %i[description tag_list active run_untagged locked] diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 656a242c265..ac2477fd973 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -80,7 +80,7 @@ class CommitRange end def inspect - %(#<#{self.class}:#{object_id} #{to_s}>) + %(#<#{self.class}:#{object_id} #{self}>) end def to_s diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 736db1ab0f6..5d6d534cd31 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -1,6 +1,7 @@ class CommitStatus < ActiveRecord::Base include HasStatus include Importable + include AfterCommitQueue self.table_name = 'ci_builds' @@ -24,7 +25,22 @@ class CommitStatus < ActiveRecord::Base scope :retried, -> { where.not(id: latest) } scope :ordered, -> { order(:name) } - scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } + + scope :failed_but_allowed, -> do + where(allow_failure: true, status: [:failed, :canceled]) + 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') + end + scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) } scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) } @@ -69,21 +85,35 @@ class CommitStatus < ActiveRecord::Base commit_status.update_attributes finished_at: Time.now end - after_transition any => [:success, :failed, :canceled] do |commit_status| - commit_status.pipeline.try(:process!) - true - end - after_transition do |commit_status, transition| - commit_status.pipeline.try(:build_updated) unless transition.loopback? + return if transition.loopback? + + commit_status.run_after_commit do + pipeline.try do |pipeline| + if complete? + ProcessPipelineWorker.perform_async(pipeline.id) + else + UpdatePipelineWorker.perform_async(pipeline.id) + end + end + end end after_transition [:created, :pending, :running] => :success do |commit_status| - MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status) + commit_status.run_after_commit do + # TODO, temporary fix for race condition + UpdatePipelineWorker.new.perform(pipeline.id) + + MergeRequests::MergeWhenBuildSucceedsService + .new(pipeline.project, nil).trigger(self) + end end after_transition any => :failed do |commit_status| - MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status) + commit_status.run_after_commit do + MergeRequests::AddTodoWhenBuildFailsService + .new(pipeline.project, nil).execute(self) + end end end @@ -111,7 +141,7 @@ class CommitStatus < ActiveRecord::Base end end - def ignored? + def failed_but_allowed? allow_failure? && (failed? || canceled?) end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb new file mode 100644 index 00000000000..90bd6490a02 --- /dev/null +++ b/app/models/concerns/cache_markdown_field.rb @@ -0,0 +1,131 @@ +# This module takes care of updating cache columns for Markdown-containing +# fields. Use like this in the body of your class: +# +# include CacheMarkdownField +# cache_markdown_field :foo +# cache_markdown_field :bar +# cache_markdown_field :baz, pipeline: :single_line +# +# Corresponding foo_html, bar_html and baz_html fields should exist. +module CacheMarkdownField + # Knows about the relationship between markdown and html field names, and + # stores the rendering contexts for the latter + class FieldData + extend Forwardable + + def initialize + @data = {} + end + + def_delegators :@data, :[], :[]= + def_delegator :@data, :keys, :markdown_fields + + def html_field(markdown_field) + "#{markdown_field}_html" + end + + def html_fields + markdown_fields.map {|field| html_field(field) } + end + end + + # Dynamic registries don't really work in Rails as it's not guaranteed that + # every class will be loaded, so hardcode the list. + CACHING_CLASSES = %w[ + AbuseReport + Appearance + ApplicationSetting + BroadcastMessage + Issue + Label + MergeRequest + Milestone + Namespace + Note + Project + Release + Snippet + ] + + def self.caching_classes + CACHING_CLASSES.map(&:constantize) + end + + extend ActiveSupport::Concern + + included do + cattr_reader :cached_markdown_fields do + FieldData.new + end + + # Returns the default Banzai render context for the cached markdown field. + def banzai_render_context(field) + raise ArgumentError.new("Unknown field: #{field.inspect}") unless + cached_markdown_fields.markdown_fields.include?(field) + + # Always include a project key, or Banzai complains + project = self.project if self.respond_to?(:project) + context = cached_markdown_fields[field].merge(project: project) + + # Banzai is less strict about authors, so don't always have an author key + context[:author] = self.author if self.respond_to?(:author) + + context + end + + # Allow callers to look up the cache field name, rather than hardcoding it + def markdown_cache_field_for(field) + raise ArgumentError.new("Unknown field: #{field}") unless + cached_markdown_fields.markdown_fields.include?(field) + + cached_markdown_fields.html_field(field) + end + + # Always exclude _html fields from attributes (including serialization). + # They contain unredacted HTML, which would be a security issue + alias_method :attributes_before_markdown_cache, :attributes + def attributes + attrs = attributes_before_markdown_cache + + cached_markdown_fields.html_fields.each do |field| + attrs.delete(field) + end + + attrs + end + end + + class_methods do + private + + # Specify that a field is markdown. Its rendered output will be cached in + # a corresponding _html field. Any custom rendering options may be provided + # as a context. + def cache_markdown_field(markdown_field, context = {}) + raise "Add #{self} to CacheMarkdownField::CACHING_CLASSES" unless + CacheMarkdownField::CACHING_CLASSES.include?(self.to_s) + + cached_markdown_fields[markdown_field] = context + + html_field = cached_markdown_fields.html_field(markdown_field) + cache_method = "#{markdown_field}_cache_refresh".to_sym + invalidation_method = "#{html_field}_invalidated?".to_sym + + define_method(cache_method) do + html = Banzai::Renderer.cacheless_render_field(self, markdown_field) + __send__("#{html_field}=", html) + true + end + + # The HTML becomes invalid if any dependent fields change. For now, assume + # author and project invalidate the cache in all circumstances. + define_method(invalidation_method) do + changed_fields = changed_attributes.keys + invalidations = changed_fields & [markdown_field.to_s, "author", "project"] + !invalidations.empty? + end + + before_save cache_method, if: invalidation_method + end + end +end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 0fa4df0fb56..9f64f76721d 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -8,32 +8,32 @@ module HasStatus class_methods do def status_sql - scope = all + scope = if respond_to?(:exclude_ignored) + exclude_ignored + else + all + end builds = scope.select('count(*)').to_sql created = scope.created.select('count(*)').to_sql success = scope.success.select('count(*)').to_sql - ignored = scope.ignored.select('count(*)').to_sql if scope.respond_to?(:ignored) - ignored ||= '0' pending = scope.pending.select('count(*)').to_sql running = scope.running.select('count(*)').to_sql - canceled = scope.canceled.select('count(*)').to_sql skipped = scope.skipped.select('count(*)').to_sql + canceled = scope.canceled.select('count(*)').to_sql - deduce_status = "(CASE + "(CASE + WHEN (#{builds})=(#{success}) THEN 'success' WHEN (#{builds})=(#{created}) THEN 'created' - WHEN (#{builds})=(#{skipped}) THEN 'skipped' - WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success' - WHEN (#{builds})=(#{created})+(#{pending})+(#{skipped}) THEN 'pending' - WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored})+(#{skipped}) THEN 'canceled' + WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'skipped' + WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled' + WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending' WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running' ELSE 'failed' END)" - - deduce_status end def status - all.pluck(self.status_sql).first + all.pluck(status_sql).first end def started_at @@ -43,6 +43,10 @@ module HasStatus def finished_at all.maximum(:finished_at) end + + def all_state_names + state_machines.values.flat_map(&:states).flat_map { |s| s.map(&:name) } + end end included do diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index ff465d2c745..c4b42ad82c7 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -6,6 +6,7 @@ # module Issuable extend ActiveSupport::Concern + include CacheMarkdownField include Participable include Mentionable include Subscribable @@ -13,6 +14,9 @@ module Issuable include Awardable included do + cache_markdown_field :title, pipeline: :single_line + cache_markdown_field :description + belongs_to :author, class_name: "User" belongs_to :assignee, class_name: "User" belongs_to :updated_by, class_name: "User" diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index ec9e0f1b1d0..eb2ff0428f6 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -43,19 +43,15 @@ module Mentionable self end - def all_references(current_user = nil, text = nil, extractor: nil) + def all_references(current_user = nil, extractor: nil) extractor ||= Gitlab::ReferenceExtractor. new(project, current_user) - if text - extractor.analyze(text, author: author) - else - self.class.mentionable_attrs.each do |attr, options| - text = __send__(attr) - options = options.merge(cache_key: [self, attr], author: author) + self.class.mentionable_attrs.each do |attr, options| + text = __send__(attr) + options = options.merge(cache_key: [self, attr], author: author) - extractor.analyze(text, options) - end + extractor.analyze(text, options) end extractor @@ -66,8 +62,8 @@ module Mentionable end # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. - def referenced_mentionables(current_user = self.author, text = nil) - refs = all_references(current_user, text) + def referenced_mentionables(current_user = self.author) + refs = all_references(current_user) refs = (refs.issues + refs.merge_requests + refs.commits) # We're using this method instead of Array diffing because that requires @@ -77,8 +73,8 @@ module Mentionable end # Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+. - def create_cross_references!(author = self.author, without = [], text = nil) - refs = referenced_mentionables(author, text) + def create_cross_references!(author = self.author, without = []) + refs = referenced_mentionables(author) # We're using this method instead of Array diffing because that requires # both of the object's `hash` values to be the same, which may not be the @@ -97,10 +93,7 @@ module Mentionable return if changes.empty? - original_text = changes.collect { |_, vals| vals.first }.join(' ') - - preexisting = referenced_mentionables(author, original_text) - create_cross_references!(author, preexisting) + create_cross_references!(author) end private diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 07d7e19e70d..82b27b78229 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base delegate :name, to: :environment, prefix: true - after_save :keep_around_commit + after_save :create_ref def commit project.commit(sha) @@ -29,8 +29,8 @@ class Deployment < ActiveRecord::Base self == environment.last_deployment end - def keep_around_commit - project.repository.keep_around(self.sha) + def create_ref + project.repository.create_ref(ref, ref_path) end def manual_actions @@ -76,4 +76,10 @@ class Deployment < ActiveRecord::Base where.not(id: self.id). take end + + private + + def ref_path + File.join(environment.ref_path, 'deployments', id.to_s) + end end diff --git a/app/models/environment.rb b/app/models/environment.rb index 49e0a20640c..f0f3ee23223 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -47,4 +47,8 @@ class Environment < ActiveRecord::Base def update_merge_request_metrics? self.name == "production" end + + def ref_path + "refs/environments/#{Shellwords.shellescape(name)}" + end end diff --git a/app/models/event.rb b/app/models/event.rb index 55a76e26f3c..0764cb8cabd 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -68,8 +68,10 @@ class Event < ActiveRecord::Base true elsif issue? || issue_note? Ability.allowed?(user, :read_issue, note? ? note_target : target) + elsif merge_request? || merge_request_note? + Ability.allowed?(user, :read_merge_request, note? ? note_target : target) else - ((merge_request? || note?) && target.present?) || milestone? + milestone? end end @@ -280,6 +282,10 @@ class Event < ActiveRecord::Base note? && target && target.for_issue? end + def merge_request_note? + note? && target && target.for_merge_request? + end + def project_snippet_note? target.for_snippet? end @@ -328,13 +334,15 @@ class Event < ActiveRecord::Base def reset_project_activity return unless project - # Don't even bother obtaining a lock if the last update happened less than - # 60 minutes ago. + # Don't bother updating if we know the project was updated recently. return if recent_update? - return unless try_obtain_lease - - project.update_column(:last_activity_at, created_at) + # At this point it's possible for multiple threads/processes to try to + # update the project. Only one query should actually perform the update, + # hence we add the extra WHERE clause for last_activity_at. + Project.unscoped.where(id: project_id). + where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago). + update_all(last_activity_at: created_at) end private @@ -342,11 +350,4 @@ class Event < ActiveRecord::Base def recent_update? project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago end - - def try_obtain_lease - Gitlab::ExclusiveLease. - new("project:update_last_activity_at:#{project.id}", - timeout: RESET_PROJECT_ACTIVITY_INTERVAL.to_i). - try_obtain - end end diff --git a/app/models/global_label.rb b/app/models/global_label.rb index ddd4bad5c21..698a7bbd327 100644 --- a/app/models/global_label.rb +++ b/app/models/global_label.rb @@ -4,6 +4,10 @@ class GlobalLabel delegate :color, :description, to: :@first_label + def for_display + @first_label + end + def self.build_collection(labels) labels = labels.group_by(&:title) diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index bda2b5c5d5d..cde4a568577 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -4,6 +4,10 @@ class GlobalMilestone attr_accessor :title, :milestones alias_attribute :name, :title + def for_display + @first_milestone + end + def self.build_collection(milestones) milestones = milestones.group_by(&:title) @@ -17,6 +21,7 @@ class GlobalMilestone @title = title @name = title @milestones = milestones + @first_milestone = milestones.find {|m| m.description.present? } || milestones.first end def safe_title diff --git a/app/models/label.rb b/app/models/label.rb index a23140b7d64..e8e12e2904e 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -1,4 +1,5 @@ class Label < ActiveRecord::Base + include CacheMarkdownField include Referable include Subscribable @@ -8,6 +9,8 @@ class Label < ActiveRecord::Base None = LabelStruct.new('No Label', 'No Label') Any = LabelStruct.new('Any Label', '') + cache_markdown_field :description, pipeline: :single_line + DEFAULT_COLOR = '#428BCA' default_value_for :color, DEFAULT_COLOR diff --git a/app/models/member.rb b/app/models/member.rb index 38a278ea559..b89ba8ecbb8 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -103,7 +103,12 @@ class Member < ActiveRecord::Base } if member.request? - ::Members::ApproveAccessRequestService.new(source, current_user, id: member.id).execute + ::Members::ApproveAccessRequestService.new( + source, + current_user, + id: member.id, + access_level: access_level + ).execute else member.save end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index a431d46cc9e..a743bf313ae 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -31,7 +31,7 @@ class MergeRequest < ActiveRecord::Base # Temporary fields to store compare vars # when creating new merge request - attr_accessor :can_be_created, :compare_commits, :compare + attr_accessor :can_be_created, :compare_commits, :diff_options, :compare state_machine :state, initial: :opened do event :close do @@ -196,7 +196,7 @@ class MergeRequest < ActiveRecord::Base end def diff_size - merge_request_diff.size + diffs(diff_options).size end def diff_base_commit @@ -523,9 +523,13 @@ class MergeRequest < ActiveRecord::Base # `MergeRequestsClosingIssues` model. This is a performance optimization. # Calculating this information for a number of merge requests requires # running `ReferenceExtractor` on each of them separately. + # This optimization does not apply to issues from external sources. def cache_merge_request_closes_issues!(current_user = self.author) + return if project.has_external_issue_tracker? + transaction do self.merge_requests_closing_issues.delete_all + closes_issues(current_user).each do |issue| self.merge_requests_closing_issues.create!(issue: issue) end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 36b8b70870b..3f7e96186a1 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -6,6 +6,9 @@ class MergeRequestDiff < ActiveRecord::Base # Prevent store of diff if commits amount more then 500 COMMITS_SAFE_SIZE = 100 + # Valid types of serialized diffs allowed by Gitlab::Git::Diff + VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta] + belongs_to :merge_request state_machine :state, initial: :empty do @@ -170,6 +173,15 @@ class MergeRequestDiff < ActiveRecord::Base private + # Old GitLab implementations may have generated diffs as ["--broken-diff"]. + # Avoid an error 500 by ignoring bad elements. See: + # https://gitlab.com/gitlab-org/gitlab-ce/issues/20776 + def valid_raw_diff?(raw) + return false unless raw.respond_to?(:each) + + raw.any? { |element| VALID_CLASSES.include?(element.class) } + end + def dump_commits(commits) commits.map(&:to_hash) end @@ -200,7 +212,7 @@ class MergeRequestDiff < ActiveRecord::Base end def load_diffs(raw, options) - if raw.respond_to?(:each) + if valid_raw_diff?(raw) if paths = options[:paths] raw = raw.select do |diff| paths.include?(diff[:old_path]) || paths.include?(diff[:new_path]) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 44c3cbb2c73..23aecbfa3a6 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -6,12 +6,16 @@ class Milestone < ActiveRecord::Base Any = MilestoneStruct.new('Any Milestone', '', -1) Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2) + include CacheMarkdownField include InternalId include Sortable include Referable include StripAttribute include Milestoneish + cache_markdown_field :title, pipeline: :single_line + cache_markdown_field :description + belongs_to :project has_many :issues has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 919b3b1f095..b67049f0f55 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -1,9 +1,12 @@ class Namespace < ActiveRecord::Base acts_as_paranoid + include CacheMarkdownField include Sortable include Gitlab::ShellAdapter + cache_markdown_field :description, pipeline: :description + has_many :projects, dependent: :destroy belongs_to :owner, class_name: "User" @@ -58,15 +61,13 @@ class Namespace < ActiveRecord::Base def clean_path(path) path = path.dup # Get the email username by removing everything after an `@` sign. - path.gsub!(/@.*\z/, "") - # Usernames can't end in .git, so remove it. - path.gsub!(/\.git\z/, "") - # Remove dashes at the start of the username. - path.gsub!(/\A-+/, "") - # Remove periods at the end of the username. - path.gsub!(/\.+\z/, "") + path.gsub!(/@.*\z/, "") # Remove everything that's not in the list of allowed characters. - path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") + path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") + # Remove trailing violations ('.atom', '.git', or '.') + path.gsub!(/(\.atom|\.git|\.)*\z/, "") + # Remove leading violations ('-') + path.gsub!(/\A\-+/, "") # Users with the great usernames of "." or ".." would end up with a blank username. # Work around that by setting their username to "blank", followed by a counter. diff --git a/app/models/note.rb b/app/models/note.rb index f2656df028b..2d644b03e4d 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -6,10 +6,13 @@ class Note < ActiveRecord::Base include Awardable include Importable include FasterCacheKeys + include CacheMarkdownField + + cache_markdown_field :note, pipeline: :note # Attribute containing rendered and redacted Markdown as generated by # Banzai::ObjectRenderer. - attr_accessor :note_html + attr_accessor :redacted_note_html # An Array containing the number of visible references as generated by # Banzai::ObjectRenderer diff --git a/app/models/project.rb b/app/models/project.rb index 507228606df..74d54e69648 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -6,6 +6,7 @@ class Project < ActiveRecord::Base include Gitlab::VisibilityLevel include Gitlab::CurrentSettings include AccessRequestable + include CacheMarkdownField include Referable include Sortable include AfterCommitQueue @@ -17,6 +18,8 @@ class Project < ActiveRecord::Base UNKNOWN_IMPORT_URL = 'http://unknown.git' + cache_markdown_field :description, pipeline: :description + delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true default_value_for :archived, false @@ -372,18 +375,9 @@ class Project < ActiveRecord::Base %r{(?<project>#{name_pattern}/#{name_pattern})} end - def trending(since = 1.month.ago) - # By counting in the JOIN we don't expose the GROUP BY to the outer query. - # This means that calls such as "any?" and "count" just return a number of - # the total count, instead of the counts grouped per project as a Hash. - join_body = "INNER JOIN ( - SELECT project_id, COUNT(*) AS amount - FROM notes - WHERE created_at >= #{sanitize(since)} - GROUP BY project_id - ) join_note_counts ON projects.id = join_note_counts.project_id" - - joins(join_body).reorder('join_note_counts.amount DESC') + def trending + joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id'). + reorder('trending_projects.id ASC') end def cached_count diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 8c9534c3565..530f7d5a30e 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -20,7 +20,10 @@ class ProjectFeature < ActiveRecord::Base FEATURES = %i(issues merge_requests wiki snippets builds) - belongs_to :project + # Default scopes force us to unscope here since a service may need to check + # permissions for a project in pending_delete + # http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to + belongs_to :project, -> { unscope(where: :pending_delete) } default_value_for :builds_access_level, value: ENABLED, allows_nil: false default_value_for :issues_access_level, value: ENABLED, allows_nil: false diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb index 7613cbdea93..db46def11eb 100644 --- a/app/models/project_group_link.rb +++ b/app/models/project_group_link.rb @@ -10,7 +10,7 @@ class ProjectGroupLink < ActiveRecord::Base belongs_to :group validates :project_id, presence: true - validates :group_id, presence: true + validates :group, presence: true validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" } validates :group_access, presence: true validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb index 88e053ec192..cd87a79d0c6 100644 --- a/app/models/project_services/slack_service/issue_message.rb +++ b/app/models/project_services/slack_service/issue_message.rb @@ -11,7 +11,7 @@ class SlackService attr_reader :description def initialize(params) - @user_name = params[:user][:name] + @user_name = params[:user][:username] @project_name = params[:project_name] @project_url = params[:project_url] diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/slack_service/merge_message.rb index 11fc691022b..b7615c96068 100644 --- a/app/models/project_services/slack_service/merge_message.rb +++ b/app/models/project_services/slack_service/merge_message.rb @@ -10,7 +10,7 @@ class SlackService attr_reader :title def initialize(params) - @user_name = params[:user][:name] + @user_name = params[:user][:username] @project_name = params[:project_name] @project_url = params[:project_url] diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb index 89ba51cb662..9e84e90f38c 100644 --- a/app/models/project_services/slack_service/note_message.rb +++ b/app/models/project_services/slack_service/note_message.rb @@ -10,7 +10,7 @@ class SlackService def initialize(params) params = HashWithIndifferentAccess.new(params) - @user_name = params[:user][:name] + @user_name = params[:user][:username] @project_name = params[:project_name] @project_url = params[:project_url] diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/slack_service/wiki_page_message.rb index f336d9e7691..160ca3ac115 100644 --- a/app/models/project_services/slack_service/wiki_page_message.rb +++ b/app/models/project_services/slack_service/wiki_page_message.rb @@ -9,7 +9,7 @@ class SlackService attr_reader :description def initialize(params) - @user_name = params[:user][:name] + @user_name = params[:user][:username] @project_name = params[:project_name] @project_url = params[:project_url] diff --git a/app/models/release.rb b/app/models/release.rb index e196b84eb18..c936899799e 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -1,4 +1,8 @@ class Release < ActiveRecord::Base + include CacheMarkdownField + + cache_markdown_field :description + belongs_to :project validates :description, :project, :tag, presence: true diff --git a/app/models/repository.rb b/app/models/repository.rb index 51557228ab9..4da1933c189 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -111,8 +111,10 @@ class Repository def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0) ref ||= root_ref - # Limited to 1000 commits for now, could be parameterized? - args = %W(#{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset} --max-count #{limit} --grep=#{query}) + args = %W( + #{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset} + --max-count #{limit} --grep=#{query} --regexp-ignore-case + ) args = args.concat(%W(-- #{path})) if path.present? git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp) @@ -838,6 +840,52 @@ class Repository end end + def multi_action(user:, branch:, message:, actions:, author_email: nil, author_name: nil) + update_branch_with_hooks(user, branch) do |ref| + index = rugged.index + parents = [] + branch = find_branch(ref) + + if branch + last_commit = branch.target + index.read_tree(last_commit.raw_commit.tree) + parents = [last_commit.sha] + end + + actions.each do |action| + case action[:action] + when :create, :update, :move + mode = + case action[:action] + when :update + index.get(action[:file_path])[:mode] + when :move + index.get(action[:previous_path])[:mode] + end + mode ||= 0o100644 + + index.remove(action[:previous_path]) if action[:action] == :move + + content = action[:encoding] == 'base64' ? Base64.decode64(action[:content]) : action[:content] + oid = rugged.write(content, :blob) + + index.add(path: action[:file_path], oid: oid, mode: mode) + when :delete + index.remove(action[:file_path]) + end + end + + options = { + tree: index.write_tree(rugged), + message: message, + parents: parents + } + options.merge!(get_committer_and_author(user, email: author_email, name: author_name)) + + Rugged::Commit.create(rugged, options) + end + end + def get_committer_and_author(user, email: nil, name: nil) committer = user_to_committer(user) author = Gitlab::Git::committer_hash(email: email, name: name) || committer @@ -997,6 +1045,10 @@ class Repository Gitlab::Popen.popen(args, path_to_repo) end + def create_ref(ref, ref_path) + fetch_ref(path_to_repo, ref, ref_path) + end + def update_branch_with_hooks(current_user, branch) update_autocrlf_option diff --git a/app/models/service.rb b/app/models/service.rb index 80de7175565..66c804f2b06 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -136,6 +136,7 @@ class Service < ActiveRecord::Base end def #{arg}=(value) + self.properties ||= {} updated_properties['#{arg}'] = #{arg} unless #{arg}_changed? self.properties['#{arg}'] = value end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 8a1730f3f36..2373b445009 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -1,11 +1,21 @@ class Snippet < ActiveRecord::Base include Gitlab::VisibilityLevel include Linguist::BlobHelper + include CacheMarkdownField include Participable include Referable include Sortable include Awardable + cache_markdown_field :title, pipeline: :single_line + cache_markdown_field :content + + # If file_name changes, it invalidates content + alias_method :default_content_html_invalidator, :content_html_invalidated? + def content_html_invalidated? + default_content_html_invalidator || file_name_changed? + end + default_value_for :visibility_level, Snippet::PRIVATE belongs_to :author, class_name: 'User' diff --git a/app/models/trending_project.rb b/app/models/trending_project.rb new file mode 100644 index 00000000000..27e3732da17 --- /dev/null +++ b/app/models/trending_project.rb @@ -0,0 +1,35 @@ +class TrendingProject < ActiveRecord::Base + belongs_to :project + + # The number of months to include in the trending calculation. + MONTHS_TO_INCLUDE = 1 + + # The maximum number of projects to include in the trending set. + PROJECTS_LIMIT = 100 + + # Populates the trending projects table with the current list of trending + # projects. + def self.refresh! + # The calculation **must** run in a transaction. If the removal of data and + # insertion of new data were to run separately a user might end up with an + # empty list of trending projects for a short period of time. + transaction do + delete_all + + timestamp = connection.quote(MONTHS_TO_INCLUDE.months.ago) + + connection.execute <<-EOF.strip_heredoc + INSERT INTO #{table_name} (project_id) + SELECT project_id + FROM notes + INNER JOIN projects ON projects.id = notes.project_id + WHERE notes.created_at >= #{timestamp} + AND notes.system IS FALSE + AND projects.visibility_level = #{Gitlab::VisibilityLevel::PUBLIC} + GROUP BY project_id + ORDER BY count(*) DESC + LIMIT #{PROJECTS_LIMIT}; + EOF + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 6996740eebd..f367f4616fb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -279,6 +279,11 @@ class User < ActiveRecord::Base 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 @@ -584,6 +589,11 @@ class User < ActiveRecord::Base 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) + connection_default_value_defined = new_record? && !projects_limit_changed? return unless self.projects_limit.nil? || connection_default_value_defined @@ -827,6 +837,22 @@ class User < ActiveRecord::Base todos_pending_count(force: true) end + # This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth + # flow means we don't call that automatically (and can't conveniently do so). + # + # See: + # <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92> + # + def increment_failed_attempts! + self.failed_attempts ||= 0 + self.failed_attempts += 1 + if attempts_exceeded? + lock_access! unless access_locked? + else + save(validate: false) + end + end + private def projects_union(min_access_level = nil) @@ -881,7 +907,7 @@ class User < ActiveRecord::Base if domain_matches?(allowed_domains, self.email) valid = true else - error = "is not whitelisted. Email domains valid for registration are: #{allowed_domains.join(', ')}" + error = "domain is not authorized for sign-up" valid = false end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index be25c750d67..be4721d7a51 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -40,7 +40,6 @@ class ProjectPolicy < BasePolicy can! :read_milestone can! :read_project_snippet can! :read_project_member - can! :read_merge_request can! :read_note can! :create_project can! :create_issue @@ -63,6 +62,7 @@ class ProjectPolicy < BasePolicy can! :read_pipeline can! :read_environment can! :read_deployment + can! :read_merge_request end # Permissions given when an user is team member of a project @@ -98,7 +98,6 @@ class ProjectPolicy < BasePolicy can! :admin_milestone can! :admin_project_snippet can! :admin_project_member - can! :admin_merge_request can! :admin_note can! :admin_wiki can! :admin_project @@ -118,6 +117,7 @@ class ProjectPolicy < BasePolicy can! :read_container_image can! :build_download_code can! :build_read_container_image + can! :read_merge_request end def owner_access! @@ -139,11 +139,18 @@ class ProjectPolicy < BasePolicy def team_access!(user) access = project.team.max_member_access(user.id) - guest_access! if access >= Gitlab::Access::GUEST - reporter_access! if access >= Gitlab::Access::REPORTER - team_member_reporter_access! if access >= Gitlab::Access::REPORTER - developer_access! if access >= Gitlab::Access::DEVELOPER - master_access! if access >= Gitlab::Access::MASTER + return if access < Gitlab::Access::GUEST + guest_access! + + return if access < Gitlab::Access::REPORTER + reporter_access! + team_member_reporter_access! + + return if access < Gitlab::Access::DEVELOPER + developer_access! + + return if access < Gitlab::Access::MASTER + master_access! end def archived_access! diff --git a/app/services/base_service.rb b/app/services/base_service.rb index 0c208150fb8..1a2bad77a02 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -56,9 +56,8 @@ class BaseService result end - def success - { - status: :success - } + def success(pass_back = {}) + pass_back[:status] = :success + pass_back end end diff --git a/app/services/boards/issues/create_service.rb b/app/services/boards/issues/create_service.rb new file mode 100644 index 00000000000..3701afd441f --- /dev/null +++ b/app/services/boards/issues/create_service.rb @@ -0,0 +1,16 @@ +module Boards + module Issues + class CreateService < Boards::BaseService + def execute(list) + params.merge!(label_ids: [list.label_id]) + create_issue + end + + private + + def create_issue + ::Issues::CreateService.new(project, current_user, params).execute + end + end + end +end diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 34efd09ed9f..435a8c6e681 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -36,12 +36,7 @@ module Boards end def set_state - params[:state] = - case list.list_type.to_sym - when :backlog then 'opened' - when :done then 'closed' - else 'all' - end + params[:state] = list.done? ? 'closed' : 'opened' end def board_label_ids diff --git a/app/services/boards/lists/generate_service.rb b/app/services/boards/lists/generate_service.rb index 1c48b9786e4..830e386c98b 100644 --- a/app/services/boards/lists/generate_service.rb +++ b/app/services/boards/lists/generate_service.rb @@ -25,10 +25,8 @@ module Boards def label_params [ - { name: 'Development', color: '#5CB85C' }, - { name: 'Testing', color: '#F0AD4E' }, - { name: 'Production', color: '#FF5F00' }, - { name: 'Ready', color: '#FF0000' } + { name: 'To Do', color: '#F0AD4E' }, + { name: 'Doing', color: '#5CB85C' } ] end end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 36c93dddadb..d3dd30b2588 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -16,6 +16,8 @@ module Ci process_stage(index) end + @pipeline.update_status + # Return a flag if a when builds got enqueued new_builds.flatten.any? end diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index e8465729d06..9bd4bd464f7 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -27,8 +27,9 @@ module Files create_target_branch end - if commit - success + result = commit + if result + success(result: result) else error('Something went wrong. Your changes were not committed') end @@ -42,6 +43,12 @@ module Files @source_branch != @target_branch || @source_project != @project end + def file_has_changed? + return false unless @last_commit_sha && last_commit + + @last_commit_sha != last_commit.sha + end + def raise_error(message) raise ValidationError.new(message) end diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb new file mode 100644 index 00000000000..d28912e1301 --- /dev/null +++ b/app/services/files/multi_service.rb @@ -0,0 +1,124 @@ +require_relative "base_service" + +module Files + class MultiService < Files::BaseService + class FileChangedError < StandardError; end + + def commit + repository.multi_action( + user: current_user, + branch: @target_branch, + message: @commit_message, + actions: params[:actions], + author_email: @author_email, + author_name: @author_name + ) + end + + private + + def validate + super + + params[:actions].each_with_index do |action, index| + unless action[:file_path].present? + raise_error("You must specify a file_path.") + end + + regex_check(action[:file_path]) + regex_check(action[:previous_path]) if action[:previous_path] + + if project.empty_repo? && action[:action] != :create + raise_error("No files to #{action[:action]}.") + end + + validate_file_exists(action) + + case action[:action] + when :create + validate_create(action) + when :update + validate_update(action) + when :delete + validate_delete(action) + when :move + validate_move(action, index) + else + raise_error("Unknown action type `#{action[:action]}`.") + end + end + end + + def validate_file_exists(action) + return if action[:action] == :create + + file_path = action[:file_path] + file_path = action[:previous_path] if action[:action] == :move + + blob = repository.blob_at_branch(params[:branch_name], file_path) + + unless blob + raise_error("File to be #{action[:action]}d `#{file_path}` does not exist.") + end + end + + def last_commit + Gitlab::Git::Commit.last_for_path(repository, @source_branch, @file_path) + end + + def regex_check(file) + if file =~ Gitlab::Regex.directory_traversal_regex + raise_error( + 'Your changes could not be committed, because the file name, `' + + file + + '` ' + + Gitlab::Regex.directory_traversal_regex_message + ) + end + + unless file =~ Gitlab::Regex.file_path_regex + raise_error( + 'Your changes could not be committed, because the file name, `' + + file + + '` ' + + Gitlab::Regex.file_path_regex_message + ) + end + end + + def validate_create(action) + return if project.empty_repo? + + if repository.blob_at_branch(params[:branch_name], action[:file_path]) + raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.") + end + end + + def validate_delete(action) + end + + def validate_move(action, index) + if action[:previous_path].nil? + raise_error("You must supply the original file path when moving file `#{action[:file_path]}`.") + end + + blob = repository.blob_at_branch(params[:branch_name], action[:file_path]) + + if blob + raise_error("Move destination `#{action[:file_path]}` already exists.") + end + + if action[:content].nil? + blob = repository.blob_at_branch(params[:branch_name], action[:previous_path]) + blob.load_all_data!(repository) if blob.truncated? + params[:actions][index][:content] = blob.data + end + end + + def validate_update(action) + if file_has_changed? + raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.") + end + end + end +end diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index 9e9b5b63f26..c17fdb8d1f1 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -23,12 +23,6 @@ module Files end end - def file_has_changed? - return false unless @last_commit_sha && last_commit - - @last_commit_sha != last_commit.sha - end - def last_commit @last_commit ||= Gitlab::Git::Commit. last_for_path(@source_project.repository, @source_branch, @file_path) diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb index ca9db59cac7..b7a244c2029 100644 --- a/app/services/members/authorized_destroy_service.rb +++ b/app/services/members/authorized_destroy_service.rb @@ -14,6 +14,8 @@ module Members if member.request? && member.user != user notification_service.decline_access_request(member) end + + member end end end diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 9a2bf82ef51..431da8372c9 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -1,17 +1,42 @@ module Members class DestroyService < BaseService - attr_accessor :member, :current_user + include MembersHelper - def initialize(member, current_user) - @member = member + attr_accessor :source + + ALLOWED_SCOPES = %i[members requesters all] + + def initialize(source, current_user, params = {}) + @source = source @current_user = current_user + @params = params end - def execute - unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member) - raise Gitlab::Access::AccessDeniedError - end + def execute(scope = :members) + raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope) + + member = find_member!(scope) + + raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member) + AuthorizedDestroyService.new(member, current_user).execute end + + private + + def find_member!(scope) + condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] } + case scope + when :all + source.members.find_by(condition) || + source.requesters.find_by!(condition) + else + source.public_send(scope).find_by!(condition) + end + end + + def can_destroy_member?(member) + member && can?(current_user, action_member_permission(:destroy, member), member) + end end end diff --git a/app/services/merge_requests/assign_issues_service.rb b/app/services/merge_requests/assign_issues_service.rb new file mode 100644 index 00000000000..f636e5fec4f --- /dev/null +++ b/app/services/merge_requests/assign_issues_service.rb @@ -0,0 +1,35 @@ +module MergeRequests + class AssignIssuesService < BaseService + def assignable_issues + @assignable_issues ||= begin + if current_user == merge_request.author + closes_issues.select do |issue| + !issue.assignee_id? && can?(current_user, :admin_issue, issue) + end + else + [] + end + end + end + + def execute + assignable_issues.each do |issue| + Issues::UpdateService.new(issue.project, current_user, assignee_id: current_user.id).execute(issue) + end + + { + count: assignable_issues.count + } + end + + private + + def merge_request + params[:merge_request] + end + + def closes_issues + @closes_issues ||= params[:closes_issues] || merge_request.closes_issues(current_user) + end + end +end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index e57791f6818..404f75616b5 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -4,7 +4,7 @@ module MergeRequests merge_request = MergeRequest.new(params) # Set MR attributes - merge_request.can_be_created = false + merge_request.can_be_created = true merge_request.compare_commits = [] merge_request.source_project = project unless merge_request.source_project @@ -22,6 +22,12 @@ module MergeRequests return build_failed(merge_request, message) end + if merge_request.source_project == merge_request.target_project && + merge_request.target_branch == merge_request.source_branch + + return build_failed(merge_request, 'You must select different branches') + end + compare = CompareService.new.execute( merge_request.source_project, merge_request.source_branch, @@ -29,17 +35,8 @@ module MergeRequests merge_request.target_branch, ) - commits = compare.commits - - # At this point we decide if merge request can be created - # If we have at least one commit to merge -> creation allowed - if commits.present? - merge_request.compare_commits = commits - merge_request.can_be_created = true - merge_request.compare = compare - else - merge_request.can_be_created = false - end + merge_request.compare_commits = compare.commits + merge_request.compare = compare set_title_and_description(merge_request) end @@ -89,6 +86,8 @@ module MergeRequests end end + merge_request.title = merge_request.wip_title if commits.empty? + merge_request end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index de8049b8e2e..72712afc07e 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -475,10 +475,12 @@ class NotificationService end def reject_users_without_access(recipients, target) - return recipients unless target.is_a?(Issue) + return recipients unless target.is_a?(Issuable) + + ability = :"read_#{target.to_ability_name}" recipients.select do |user| - user.can?(:read_issue, target) + user.can?(ability, target) end end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index be749ba4a1c..15d7918e7fd 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -7,6 +7,8 @@ module Projects def execute forked_from_project_id = params.delete(:forked_from_project_id) import_data = params.delete(:import_data) + @skip_wiki = params.delete(:skip_wiki) + @project = Project.new(params) # Make sure that the user is allowed to use the specified visibility level @@ -15,6 +17,11 @@ module Projects return @project end + unless allowed_fork?(forked_from_project_id) + @project.errors.add(:forked_from_project_id, 'is forbidden') + return @project + end + # Set project name from path if @project.name.present? && @project.path.present? # if both name and path set - everything is ok @@ -71,6 +78,13 @@ module Projects @project.errors.add(:namespace, "is not valid") end + def allowed_fork?(source_project_id) + return true if source_project_id.nil? + + source_project = Project.find_by(id: source_project_id) + current_user.can?(:fork_project, source_project) + end + def allowed_namespace?(user, namespace_id) namespace = Namespace.find_by(id: namespace_id) current_user.can?(:create_projects, namespace) @@ -80,7 +94,7 @@ module Projects log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"") unless @project.gitlab_project_import? - @project.create_wiki if @project.feature_available?(:wiki, current_user) + @project.create_wiki unless skip_wiki? @project.build_missing_services @project.create_labels @@ -94,6 +108,10 @@ module Projects end end + def skip_wiki? + !@project.feature_available?(:wiki, current_user) || @skip_wiki + end + def save_project_and_import_data(import_data) Project.transaction do @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index a2de4dccece..a2b23ea6171 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -16,6 +16,8 @@ module Projects end new_project = CreateService.new(current_user, new_params).execute + return new_project unless new_project.persisted? + builds_access_level = @project.project_feature.builds_access_level new_project.project_feature.update_attributes(builds_access_level: builds_access_level) diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index 1725a30fae5..e4ae3dec8aa 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -122,7 +122,12 @@ module SlashCommands command :label do |labels_param| label_ids = find_label_ids(labels_param) - @updates[:add_label_ids] = label_ids unless label_ids.empty? + if label_ids.any? + @updates[:add_label_ids] ||= [] + @updates[:add_label_ids] += label_ids + + @updates[:add_label_ids].uniq! + end end desc 'Remove all or specific label(s)' @@ -136,7 +141,12 @@ module SlashCommands if labels_param.present? label_ids = find_label_ids(labels_param) - @updates[:remove_label_ids] = label_ids unless label_ids.empty? + if label_ids.any? + @updates[:remove_label_ids] ||= [] + @updates[:remove_label_ids] += label_ids + + @updates[:remove_label_ids].uniq! + end else @updates[:label_ids] = [] end @@ -152,7 +162,12 @@ module SlashCommands command :relabel do |labels_param| label_ids = find_label_ids(labels_param) - @updates[:label_ids] = label_ids unless label_ids.empty? + if label_ids.any? + @updates[:label_ids] ||= [] + @updates[:label_ids] += label_ids + + @updates[:label_ids].uniq! + end end desc 'Add a todo' diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index 1fb72cf89e9..a2bfa422c9d 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -72,7 +72,7 @@ class SystemHooksService return 'user_add_to_group' if event == :create return 'user_remove_from_group' if event == :destroy else - "#{model.class.name.downcase}_#{event.to_s}" + "#{model.class.name.downcase}_#{event}" end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 5ccaa5275b7..1ce66d50368 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -246,7 +246,7 @@ module SystemNoteService 'deleted' end - body = "#{verb} #{branch_type.to_s} branch `#{branch}`".capitalize + body = "#{verb} #{branch_type} branch `#{branch}`".capitalize create_note(noteable: noteable, project: project, author: author, note: body) end @@ -347,7 +347,7 @@ module SystemNoteService notes = notes.where(noteable_id: noteable.id) end - notes_for_mentioner(mentioner, noteable, notes).count > 0 + notes_for_mentioner(mentioner, noteable, notes).exists? end # Build an Array of lines detailing each commit added in a merge request diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 776530ac0a5..f8e6b2ef094 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -273,12 +273,12 @@ class TodoService end def reject_users_without_access(users, project, target) - if target.is_a?(Note) && target.for_issue? + if target.is_a?(Note) && (target.for_issue? || target.for_merge_request?) target = target.noteable end - if target.is_a?(Issue) - select_users(users, :read_issue, target) + if target.is_a?(Issuable) + select_users(users, :"read_#{target.to_ability_name}", target) else select_users(users, :read_project, project) end diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb index 4dc3b2ab9a0..2821ecf0a88 100644 --- a/app/validators/namespace_validator.rb +++ b/app/validators/namespace_validator.rb @@ -24,6 +24,7 @@ class NamespaceValidator < ActiveModel::EachValidator projects public repository + robots.txt s search services diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml index 56bf6194914..05f3d9a3b50 100644 --- a/app/views/admin/abuse_reports/_abuse_report.html.haml +++ b/app/views/admin/abuse_reports/_abuse_report.html.haml @@ -21,7 +21,7 @@ %td %strong.subheading.visible-xs-block.visible-sm-block Message .message - = markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter) + = markdown_field(abuse_report, :message) %td - if user = link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true), diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 0d79ca7dc52..c4c68cd7891 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -221,7 +221,11 @@ %fieldset %legend Metrics %p - These settings require a restart to take effect. + Setup InfluxDB to measure a wide variety of statistics like the time spent + in running SQL queries. These settings require a + = link_to 'restart', help_page_path('administration/restart_gitlab') + to take effect. + = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/introduction') .form-group .col-sm-offset-2.col-sm-10 .checkbox diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml index f952d2e9aa1..3132d157f29 100644 --- a/app/views/admin/broadcast_messages/_form.html.haml +++ b/app/views/admin/broadcast_messages/_form.html.haml @@ -1,7 +1,10 @@ .broadcast-message-preview{ style: broadcast_message_style(@broadcast_message) } = icon('bullhorn') .js-broadcast-message-preview - = render_broadcast_message(@broadcast_message.message.presence || "Your message here") + - if @broadcast_message.message.present? + = render_broadcast_message(@broadcast_message) + - else + = "Your message here" = form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f| = form_errors(@broadcast_message) diff --git a/app/views/admin/broadcast_messages/preview.js.haml b/app/views/admin/broadcast_messages/preview.js.haml index fbc9453c72e..c72e59640d7 100644 --- a/app/views/admin/broadcast_messages/preview.js.haml +++ b/app/views/admin/broadcast_messages/preview.js.haml @@ -1 +1 @@ -$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@message))}"); +$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@broadcast_message))}"); diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index e6687f43816..90798c47d97 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -63,6 +63,11 @@ Reply by email %span.light.pull-right = boolean_to_icon Gitlab::IncomingEmail.enabled? + %p + Container Registry + %span.light.pull-right + = boolean_to_icon Gitlab.config.registry.enabled + .col-md-4 %h4 Components diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index 77a11e49e20..adfa1eaafc9 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -23,4 +23,4 @@ - if group.description.present? .description - = markdown(group.description, pipeline: :description) + = markdown_field(group, :description) diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml index f417b2e44a4..be224d66855 100644 --- a/app/views/admin/labels/_label.html.haml +++ b/app/views/admin/labels/_label.html.haml @@ -1,7 +1,7 @@ %li{id: dom_id(label)} .label-row = render_colored_label(label, tooltip: false) - = markdown(label.description, pipeline: :single_line) + = markdown_field(label, :description) .pull-right = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm' = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"} diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 1e755785d90..339cfc613fe 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -87,7 +87,7 @@ - if project.description.present? .description - = markdown(project.description, pipeline: :description) + = markdown_field(project, :description) = paginate @projects, theme: 'gitlab' - else diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml index d5c21c6dffe..61c7cce20b2 100644 --- a/app/views/ci/lints/_create.html.haml +++ b/app/views/ci/lints/_create.html.haml @@ -16,8 +16,7 @@ %tr %td #{stage.capitalize} Job - #{build[:name]} %td - %pre - = simple_format build[:commands] + %pre= build[:commands] %br %b Tag list: diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index b40395c74de..cc077fad32a 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -19,6 +19,7 @@ (removed) · #{time_ago_with_tooltip(todo.created_at)} + = todo_due_date(todo) .todo-body .todo-note diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 9d31f31c639..2a0302638ba 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -55,7 +55,7 @@ = sort_options_hash[@sort] - else = sort_title_recently_created - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort %li = link_to todos_filter_path(sort: sort_value_priority) do diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml index 73c3a3dd2eb..20cd7b0179d 100644 --- a/app/views/devise/confirmations/almost_there.haml +++ b/app/views/devise/confirmations/almost_there.haml @@ -3,9 +3,9 @@ Almost there... %p.lead Please check your email to confirm your account -- if after_sign_up_text.present? +- if current_application_settings.after_sign_up_text.present? .well-confirmation.text-center - = markdown(after_sign_up_text) + = markdown_field(current_application_settings, :after_sign_up_text) %p.confirmation-content.text-center No confirmation email received? Please check your spam folder or .append-bottom-20.prepend-top-20.text-center diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index b8248a80a27..a1b39d9e1a0 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -23,7 +23,7 @@ = sort_options_hash[@sort] - else = sort_title_recently_created - %b.caret + = icon('caret-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 132bbe26fe0..4cff14b096b 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -7,7 +7,7 @@ = visibility_level_label(params[:visibility_level].to_i) - else Any - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_projects_path(visibility_level: nil) do @@ -27,7 +27,7 @@ = params[:tag] - else Any - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_projects_path(tag: nil) do diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index ca6c4326d1c..23d438b2aa1 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -33,8 +33,8 @@ .form-group = f.label :projects, "Projects", class: "control-label" .col-sm-10 - = f.collection_select :project_ids, @group.projects, :id, :name, - { selected: @group.projects.map(&:id) }, multiple: true, class: 'select2' + = f.collection_select :project_ids, @group.projects.non_archived, :id, :name, + { selected: @group.projects.non_archived.pluck(:id) }, multiple: true, class: 'select2' .col-md-6 .form-group diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 31db6ee0cad..fab61f447c2 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -21,7 +21,7 @@ - if @group.description.present? .cover-desc.description - = markdown(@group.description, pipeline: :description) + = markdown_field(@group, :description) %div.groups-header{ class: container_class } .top-area diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index 57601ae9be0..31631887317 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -20,7 +20,7 @@ Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}. - if current_application_settings.help_page_text.present? %hr - = markdown(current_application_settings.help_page_text) + = markdown_field(current_application_settings, :help_page_text) %hr diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 3d28eec84ef..a9a384bd5f3 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -25,8 +25,8 @@ Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki. - - if extra_sign_in_text.present? - = markdown(extra_sign_in_text) + - if current_application_settings.sign_in_text.present? + = markdown_field(current_application_settings, :sign_in_text) %hr .container diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 94c53882623..7faa8bded86 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,5 +1,5 @@ %header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } - %div{ class: fluid_layout ? "container-fluid" : "container-fluid" } + %div{ class: "container-fluid" } .header-content %button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" } %span.sr-only Toggle navigation @@ -41,7 +41,7 @@ %li.header-user.dropdown = link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do = image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar" - %span.caret + = icon('caret-down') .dropdown-menu-nav.dropdown-menu-align-right %ul %li diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb index 4433cab7782..8966dd3fd86 100644 --- a/app/views/profiles/preferences/update.js.erb +++ b/app/views/profiles/preferences/update.js.erb @@ -4,9 +4,9 @@ $('body').addClass('<%= user_application_theme %>') // Toggle container-fluid class if ('<%= current_user.layout %>' === 'fluid') { - $('.content-wrapper').find('.container-fluid').removeClass('container-limited') + $('.content-wrapper .container-fluid').removeClass('container-limited') } else { - $('.content-wrapper').find('.container-fluid').addClass('container-limited') + $('.content-wrapper .container-fluid').addClass('container-limited') } // Re-enable the "Save" button diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 8ef31ca3bda..5590198a20e 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -9,7 +9,7 @@ .project-home-desc - if @project.description.present? - = markdown(@project.description, pipeline: :description) + = markdown_field(@project, :description) - if forked_from_project = @project.forked_from_project %p diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index 73066150fb3..ba1502c97b6 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -12,8 +12,17 @@ %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) }" } {{ list.title }} - %span.pull-right{ "v-if" => "list.type !== 'blank'" } - {{ list.issuesSize }} + .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", @@ -26,12 +35,38 @@ ":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, + ":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" } + ":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" ) diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml index e8b60b54d80..d8f16022407 100644 --- a/app/views/projects/boards/components/_card.html.haml +++ b/app/views/projects/boards/components/_card.html.haml @@ -7,7 +7,7 @@ ":issue-link-base" => "issueLinkBase", ":disabled" => "disabled", "track-by" => "id" } - %li.card{ ":class" => "{ 'user-can-drag': !disabled }", + %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id }", ":index" => "index" } %h4.card-title = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential") @@ -15,7 +15,7 @@ ":title" => "issue.title" } {{ issue.title }} .card-footer - %span.card-number + %span.card-number{ "v-if" => "issue.id" } = precede '#' do {{ issue.id }} %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels", @@ -26,7 +26,7 @@ ":title" => "label.description", data: { container: 'body' } } {{ label.title }} - %a.has-tooltip{ ":href" => "'/u/' + issue.assignee.username", + %a.has-tooltip{ ":href" => "'/' + issue.assignee.username", ":title" => "'Assigned to ' + issue.assignee.name", "v-if" => "issue.assignee", data: { container: 'body' } } diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index e889f29c816..84f38575e84 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -15,7 +15,7 @@ %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} %span.light = projects_sort_options_hash[@sort] - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_branches_path(sort: sort_value_name) do diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index f5344091cae..966633f1f89 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -128,7 +128,7 @@ - builds.select{|build| build.status == build_status}.each do |build| .build-job{class: ('active' if build == @build), data: {stage: build.stage}} = link_to namespace_project_build_path(@project.namespace, @project, build) do - = icon('right-arrow') + = icon('arrow-right') = ci_icon_for_status(build.status) %span - if build.name diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml index c2bcfb773a6..f3747ba2a21 100644 --- a/app/views/projects/builds/_table.html.haml +++ b/app/views/projects/builds/_table.html.haml @@ -1,7 +1,7 @@ - admin = local_assigns.fetch(:admin, false) - if builds.blank? - %li + %div .nothing-here-block No builds to show - else .table-holder diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 5c60b7a7364..06070f12bbd 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 - %ul.content-list.builds-content-list + %div.content-list.builds-content-list = render "table", builds: @builds, project: @project diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 24de020917a..9089586a89d 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,9 +1,9 @@ - if !project.empty_repo? && can?(current_user, :download_code, project) - %span.btn-group{class: 'hidden-xs hidden-sm btn-grouped'} + %span{class: 'hidden-xs hidden-sm'} .dropdown.inline %button.btn{ 'data-toggle' => 'dropdown' } = icon('download') - %span.caret + = icon("caret-down") %span.sr-only Select Archive Format %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index ca907077c2b..6cd9b98a706 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -1,7 +1,8 @@ - if current_user - .btn-group + .dropdown.inline.project-dropdown %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} = icon('plus') + = icon("caret-down") %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown - can_create_issue = can?(current_user, :create_issue, @project) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 22db33498f1..29d549a60f5 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -5,10 +5,10 @@ = custom_icon('icon_fork') %span Fork - else - = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn has-tooltip' do = custom_icon('icon_fork') %span Fork %div.count-with-arrow %span.arrow - = link_to namespace_project_forks_path(@project.namespace, @project), class: "count" do + = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count has-tooltip' do = @project.forks_count diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 75192c48188..9248adfde80 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -13,45 +13,44 @@ - else = ci_status_with_icon(build.status) - %td - .branch-commit - - if can?(current_user, :read_build, build) - = link_to namespace_project_build_url(build.project.namespace, build.project, build) do - %span.build-link ##{build.id} - - else + %td.branch-commit + - if can?(current_user, :read_build, build) + = link_to namespace_project_build_url(build.project.namespace, build.project, build) do %span.build-link ##{build.id} + - else + %span.build-link ##{build.id} - - if ref - - if build.ref - .icon-container - = build.tag? ? icon('tag') : icon('code-fork') - = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name" - - else - .light none + - if ref + - if build.ref .icon-container - = custom_icon("icon_commit") + = build.tag? ? icon('tag') : icon('code-fork') + = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name" + - else + .light none + .icon-container + = custom_icon("icon_commit") - - if commit_sha - = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace" + - if commit_sha + = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace" - - if build.stuck? - = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') - - if retried - = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') + - if build.stuck? + = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') + - if retried + = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') - .label-container - - if build.tags.any? - - build.tags.each do |tag| - %span.label.label-primary - = tag - - if build.try(:trigger_request) - %span.label.label-info triggered - - if build.try(:allow_failure) - %span.label.label-danger allowed to fail - - if retried - %span.label.label-warning retried - - if build.manual? - %span.label.label-info manual + .label-container + - if build.tags.any? + - build.tags.each do |tag| + %span.label.label-primary + = tag + - if build.try(:trigger_request) + %span.label.label-info triggered + - if build.try(:allow_failure) + %span.label.label-danger allowed to fail + - if retried + %span.label.label-warning retried + - if build.manual? + %span.label.label-info manual - if admin %td diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml index 547bc0c9c19..017d3ff6af2 100644 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ b/app/views/projects/ci/builds/_build_pipeline.html.haml @@ -5,8 +5,10 @@ .ci-status-text= subject.name - elsif can?(current_user, :read_build, @project) = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do - = render_status_with_link('build', subject.status) + %span.ci-status-icon + = render_status_with_link('build', subject.status) .ci-status-text= subject.name - else - = render_status_with_link('build', subject.status) + %span.ci-status-icon + = render_status_with_link('build', subject.status) = 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 04e48a4dc17..36eadbd2bf1 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -9,33 +9,32 @@ = ci_icon_for_status(status) - else = ci_status_with_icon(status) - %td - .branch-commit - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do - %span ##{pipeline.id} - - if pipeline.ref && show_branch - .icon-container - = pipeline.tag? ? icon('tag') : icon('code-fork') - = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name" - - if show_commit - .icon-container - = custom_icon("icon_commit") - = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace" - - if pipeline.latest? - %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest - - if pipeline.triggered? - %span.label.label-primary triggered - - if pipeline.yaml_errors.present? - %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid - - if pipeline.builds.any?(&:stuck?) - %span.label.label-warning stuck + %td.branch-commit + = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do + %span ##{pipeline.id} + - if pipeline.ref && show_branch + .icon-container + = pipeline.tag? ? icon('tag') : icon('code-fork') + = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name" + - if show_commit + .icon-container + = custom_icon("icon_commit") + = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace" + - if pipeline.latest? + %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest + - if pipeline.triggered? + %span.label.label-primary triggered + - if pipeline.yaml_errors.present? + %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid + - if pipeline.builds.any?(&:stuck?) + %span.label.label-warning stuck - %p.commit-title - - if commit = pipeline.commit - = author_avatar(commit, size: 20) - = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message" - - else - Cant find HEAD commit for this branch + %p.commit-title + - if commit = pipeline.commit + = author_avatar(commit, size: 20) + = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message" + - else + Cant find HEAD commit for this branch - stages_status = pipeline.statuses.relevant.latest.stages_status @@ -58,8 +57,8 @@ = icon("calendar") #{time_ago_with_tooltip(pipeline.finished_at, short_format: false, skip_js: true)} - %td.pipeline-actions - .controls.hidden-xs.pull-right + %td.pipeline-actions.hidden-xs + .controls.pull-right - artifacts = pipeline.builds.latest.with_artifacts_not_expired - actions = pipeline.manual_actions - if artifacts.present? || actions.any? @@ -68,7 +67,7 @@ .btn-group %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} = custom_icon('icon_play') - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right - actions.each do |build| %li @@ -79,7 +78,7 @@ .btn-group %a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'} = icon("download") - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right - artifacts.each do |build| %li diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 29d767e7769..6c82a4e5600 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -14,7 +14,7 @@ .dropdown.inline %a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } } %span.hidden-xs Options - %span.caret.commit-options-dropdown-caret + = icon('caret-down', class: ".commit-options-dropdown-caret") %ul.dropdown-menu.dropdown-menu-align-right %li.visible-xs-block.visible-sm-block = link_to namespace_project_tree_path(@project.namespace, @project, @commit) do @@ -24,6 +24,8 @@ = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) %li.clearfix = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) + %li.clearfix + = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit) %li.divider %li.dropdown-header Download @@ -63,10 +65,10 @@ .commit-box.content-block %h3.commit-title - = markdown escape_once(@commit.title), pipeline: :single_line, author: @commit.author + = markdown(@commit.title, pipeline: :single_line, author: @commit.author) - if @commit.description.present? %pre.commit-description - = preserve(markdown(escape_once(@commit.description), pipeline: :single_line, author: @commit.author)) + = preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author)) :javascript $(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}"); diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 9258f4b3c25..288c06d9b67 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -1,45 +1,46 @@ -.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 +.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 - - 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-grouped btn-danger', 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-grouped btn-danger', method: :post - .oneline.clearfix - - if defined?(pipeline_details) && pipeline_details - Pipeline - = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace" - with - = pluralize pipeline.statuses.count(:id), "build" - - if pipeline.ref - for - = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace" - - if defined?(link_to_commit) && link_to_commit - for commit - = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace" - - if pipeline.duration - in - = time_interval_in_words pipeline.duration + .oneline.clearfix + - if defined?(pipeline_details) && pipeline_details + Pipeline + = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace" + with + = pluralize pipeline.statuses.count(:id), "build" + - if pipeline.ref + for + = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace" + - if defined?(link_to_commit) && link_to_commit + for commit + = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace" + - if pipeline.duration + in + = time_interval_in_words pipeline.duration -.row-content-block.build-content.middle-block.pipeline-graph - .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.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 - if pipeline.yaml_errors.present? diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml index 23c5c51fbc2..289aa5178b1 100644 --- a/app/views/projects/commit/_pipeline_stage.html.haml +++ b/app/views/projects/commit/_pipeline_stage.html.haml @@ -10,5 +10,5 @@ - else %li.build .curve - .build-content + .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 index 4e7a6f1af08..6ada719e006 100644 --- a/app/views/projects/commit/_pipeline_status_group.html.haml +++ b/app/views/projects/commit/_pipeline_status_group.html.haml @@ -1,11 +1,12 @@ - group_status = CommitStatus.where(id: subject).status -= render_status_with_link('build', group_status) -.dropdown.inline - %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } - %span.ci-status-text - = name - %span.badge= subject.size - %ul.dropdown-menu.grouped-pipeline-dropdown - .arrow - - subject.each do |status| +%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } + %span.ci-status-icon + = render_status_with_link('build', group_status) + %span.ci-status-text + = name + %span.badge= subject.size +%ul.dropdown-menu.grouped-pipeline-dropdown + %li.arrow + - subject.each do |status| + %li = render "projects/#{status.to_partial_path}_pipeline", subject: status diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 389477d0927..fb48aef0559 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -33,7 +33,7 @@ - if commit.description? %pre.commit-row-description.js-toggle-content - = preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author)) + = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author)) .commit-row-info = commit_author_link(commit, avatar: false, size: 24) diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index e9ff8e90dd5..45be6581cfc 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -4,7 +4,7 @@ %div{ class: container_class } .sub-header-block - Compare branches, tags or commit ranges. + Compare Git revisions. %br Fill input field with commit id like %code.label-branch 4eedf23 diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index 16d134eb6b6..22c4a75d213 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -1,12 +1,18 @@ - if can?(current_user, :create_deployment, deployment) && deployment.deployable .pull-right + + - external_url = deployment.environment.external_url + - if external_url + = link_to external_url, target: '_blank', class: 'btn external-url' do + = icon('external-link') + - actions = deployment.manual_actions - if actions.present? .inline .dropdown %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} = custom_icon('icon_play') - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right - actions.each do |action| %li diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index cd95841ca5a..ca0005abd0c 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -5,14 +5,16 @@ %td = render 'projects/deployments/commit', deployment: deployment - %td + %td.build-column - if deployment.deployable - = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable] do - = user_avatar(user: deployment.user, size: 20) + = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do = "#{deployment.deployable.name} (##{deployment.deployable.id})" + - if deployment.user + by + = user_avatar(user: deployment.user, size: 20) %td #{time_ago_with_tooltip(deployment.created_at)} - %td + %td.hidden-xs = render 'projects/deployments/actions', deployment: deployment, allow_rollback: true diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 62aff36aadd..067cf595da3 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,7 +1,6 @@ - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true) +- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project) - diff_files = diffs.diff_files -- if diff_view == :parallel - - fluid_layout true .content-block.oneline-block.files-changed .inline-parallel-buttons @@ -22,7 +21,7 @@ - if diff_files.overflow? = render 'projects/diffs/warning', diff_files: diff_files -.files{data: {can_create_note: (!@diff_notes_disabled && can?(current_user, :create_note, diffs.project))}} +.files{ data: { can_create_note: can_create_note } } - diff_files.each_with_index do |diff_file, index| - diff_commit = commit_for_diff(diff_file) - blob = diff_file.blob(diff_commit) diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index d07de45fdde..257e0a855bd 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -8,7 +8,7 @@ = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file", disabled: @diff_notes_disabled do = icon('comment') \ - + = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-file-option') - if editable_diff?(diff_file) - link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {} = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index a04d53e02bf..c8f84b96cb7 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -91,7 +91,7 @@ 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' + = 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 @@ -100,7 +100,8 @@ = f.check_box :container_registry_enabled %strong Container Registry %br - %span.descr Enable Container Registry for this repository + %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 %hr diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml index 36a6162a5a8..251694e897c 100644 --- a/app/views/projects/environments/_environment.html.haml +++ b/app/views/projects/environments/_environment.html.haml @@ -4,10 +4,17 @@ %td = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment) - %td + %td.deployment-column - if last_deployment - = user_avatar(user: last_deployment.user, size: 20) - %strong ##{last_deployment.id} + %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 @@ -20,5 +27,5 @@ - if last_deployment #{time_ago_with_tooltip(last_deployment.created_at)} - %td + %td.hidden-xs = render 'projects/deployments/actions', deployment: last_deployment diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index b3eb5b0011a..ab801409722 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -9,25 +9,27 @@ = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do New environment - - if @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.builds.environments - %tbody - %th Environment - %th Last Deployment - %th Commit - %th - %th - = render @environments + .environments-container + - if @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.builds.environments + %tbody + %th Environment + %th Last Deployment + %th Build + %th Commit + %th + %th.hidden-xs + = render @environments diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 8f8c1c4ce22..7a8d196cf4e 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -12,26 +12,27 @@ = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' = link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete - - if @deployments.blank? - .blank-state.blank-state-no-icon - %h2.blank-state-title - You don't have any deployments right now. - %p.blank-state-text - Define environments in the deploy stage(s) in - %code .gitlab-ci.yml - to track deployments here. - = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success" - - else - .table-holder - %table.table.builds.environments - %thead - %tr - %th ID - %th Commit - %th Build - %th - %th + .deployments-container + - if @deployments.blank? + .blank-state.blank-state-no-icon + %h2.blank-state-title + You don't have any deployments right now. + %p.blank-state-text + Define environments in the deploy stage(s) in + %code .gitlab-ci.yml + to track deployments here. + = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success" + - else + .table-holder + %table.table.builds.environments + %thead + %tr + %th ID + %th Commit + %th Build + %th + %th.hidden-xs - = render @deployments + = render @deployments - = paginate @deployments, theme: 'gitlab' + = paginate @deployments, theme: 'gitlab' diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index bacc5708e4b..abf4f697f86 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -15,7 +15,7 @@ = sort_options_hash[@sort] - else = sort_title_recently_created - %b.caret + = icon('caret-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 331dc1fcc29..80fe6be49b0 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 @@ -62,5 +62,3 @@ %td.coverage - if generic_commit_status.try(:coverage) #{generic_commit_status.coverage}% - - %td 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 index 409f4701e4b..0a66d60accc 100644 --- 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 @@ -1,7 +1,9 @@ - if subject.target_url = link_to subject.target_url do - = render_status_with_link('commit status', subject.status) + %span.ci-status-icon + = render_status_with_link('commit status', subject.status) %span.ci-status-text= subject.name - else - = render_status_with_link('commit status', subject.status) + %span.ci-status-icon + = render_status_with_link('commit status', subject.status) %span.ci-status-text= subject.name diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml index ca700cb3a3b..1b0dbbb8111 100644 --- a/app/views/projects/group_links/index.html.haml +++ b/app/views/projects/group_links/index.html.haml @@ -8,15 +8,15 @@ .col-lg-9 %h5.prepend-top-0 Set a group to share - = form_tag namespace_project_group_links_path(@project.namespace, @project), method: :post do + = form_tag namespace_project_group_links_path(@project.namespace, @project), class: 'js-requires-input', method: :post do .form-group = label_tag :link_group_id, "Group", class: "label-light" - = groups_select_tag(:link_group_id, skip_group: @project.group.try(:path)) + = groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, required: true) .form-group = label_tag :link_group_access, "Max access level", class: "label-light" .select-wrapper = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" - %span.caret + = icon('caret-down') .form-group = label_tag :expires_at, 'Access expiration date', class: 'label-light' .clearable-input diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml index 7cf1923456e..3a6fbbc7fbc 100644 --- a/app/views/projects/issues/edit.html.haml +++ b/app/views/projects/issues/edit.html.haml @@ -1,4 +1,4 @@ -- page_title "Edit", "#{@issue.title} (##{@issue.iid})", "Issues" +- page_title "Edit", "#{@issue.to_reference} #{@issue.title}", "Issues" %h3.page-title Edit Issue ##{@issue.iid} diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 3fb4191c60e..09347ad5fff 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,4 +1,4 @@ -- page_title "#{@issue.title} (##{@issue.iid})", "Issues" +- page_title "#{@issue.to_reference} #{@issue.title}", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes @@ -23,8 +23,8 @@ .issuable-actions .clearfix.issue-btn-group.dropdown %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } - %span.caret Options + = icon('caret-down') .dropdown-menu.dropdown-menu-align-right.hidden-lg %ul - if can?(current_user, :create_issue, @project) @@ -55,12 +55,12 @@ .issue-details.issuable-details .detail-page-description.content-block %h2.title - = markdown escape_once(@issue.title), pipeline: :single_line, author: @issue.author + = markdown_field(@issue, :title) - if @issue.description.present? .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' } .wiki = preserve do - = markdown(@issue.description, cache_key: [@issue, "description"], author: @issue.author) + = markdown_field(@issue, :description) %textarea.hidden.js-task-list-field = @issue.description = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 73c6f2a046c..71f7f354d72 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -5,7 +5,7 @@ .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown %button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } } Options - %span.caret + = icon('caret-down') .dropdown-menu.dropdown-menu-align-right %ul %li diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index de39964fca8..466ec1475d8 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -65,19 +65,6 @@ - if @merge_request.errors.any? = form_errors(@merge_request) - - elsif @merge_request.source_branch.present? && @merge_request.target_branch.present? - .light-well.append-bottom-default - .center - %h4 - There isn't anything to merge. - %p.slead - - if @merge_request.source_branch == @merge_request.target_branch - You'll need to use different branch names to get a valid comparison. - - else - %span.label-branch #{@merge_request.source_branch} - and - %span.label-branch #{@merge_request.target_branch} - are the same. = f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn" :javascript diff --git a/app/views/projects/merge_requests/_new_diffs.html.haml b/app/views/projects/merge_requests/_new_diffs.html.haml new file mode 100644 index 00000000000..74367ab9b7b --- /dev/null +++ b/app/views/projects/merge_requests/_new_diffs.html.haml @@ -0,0 +1 @@ += render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 00bd4e143df..da6927879a4 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -18,34 +18,38 @@ = f.hidden_field :target_branch .mr-compare.merge-request - %ul.merge-request-tabs.nav-links.no-top.no-bottom - %li.commits-tab - = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do - Commits - %span.badge= @commits.size - - if @pipeline - %li.builds-tab.active - = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do - Builds - %span.badge= @statuses.size - %li.diffs-tab.active - = link_to url_for(params), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do - Changes - %span.badge= @diffs.real_size + - if @commits.empty? + .commits-empty + %h4 + There are no commits yet. + = custom_icon ('illustration_no_commits') + - else + %ul.merge-request-tabs.nav-links.no-top.no-bottom + %li.commits-tab.active + = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do + Commits + %span.badge= @commits.size + - if @pipeline + %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 + %span.badge= @merge_request.diff_size - .tab-content - #commits.commits.tab-pane - = render "projects/merge_requests/show/commits" - #diffs.diffs.tab-pane.active - - if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE - .alert.alert-danger - %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. - %p To preserve performance the line changes are not shown. - - else - = render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false - - if @pipeline - #builds.builds.tab-pane - = render "projects/merge_requests/show/builds" + .tab-content + #commits.commits.tab-pane.active + = render "projects/merge_requests/show/commits" + #diffs.diffs.tab-pane + - # This tab is always loaded via AJAX + - if @pipeline + #builds.builds.tab-pane + = render "projects/merge_requests/show/builds" + + .mr-loading-status + = spinner :javascript $('.assign-to-me-link').on('click', function(e){ @@ -54,6 +58,6 @@ }); :javascript var merge_request = new MergeRequest({ - action: "#{(@show_changes_tab ? 'diffs' : 'new')}", - setUrl: false + action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}", + buildsLoaded: "#{@pipeline ? 'true' : 'false'}" }); diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index d03ff9ec7e8..47dd51639b5 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,12 +1,9 @@ -- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" +- page_title "#{@merge_request.to_reference} #{@merge_request.title}", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - content_for :page_specific_javascripts do = page_specific_javascript_tag('diff_notes/diff_notes_bundle.js') -- if diff_view == :parallel - - fluid_layout true - .merge-request{'data-url' => merge_request_path(@merge_request)} = render "projects/merge_requests/show/mr_title" @@ -25,7 +22,7 @@ %span.dropdown.inline.prepend-left-5 %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} } Download as - %span.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml index 03159f123f3..7c3ac6652ee 100644 --- a/app/views/projects/merge_requests/edit.html.haml +++ b/app/views/projects/merge_requests/edit.html.haml @@ -1,4 +1,4 @@ -- page_title "Edit", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" +- page_title "Edit", "#{@merge_request.to_reference} #{@merge_request.title}", "Merge Requests" %h3.page-title Edit Merge Request #{@merge_request.to_reference} 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 ebf18f6ac85..ed23d06ee5e 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -1,13 +1,13 @@ .detail-page-description.content-block %h2.title - = markdown escape_once(@merge_request.title), pipeline: :single_line, author: @merge_request.author + = markdown_field(@merge_request, :title) %div - if @merge_request.description.present? .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''} .wiki = preserve do - = markdown(@merge_request.description, cache_key: [@merge_request, "description"], author: @merge_request.author) + = markdown_field(@merge_request, :description) %textarea.hidden.js-task-list-field = @merge_request.description diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index e35291dff7d..e7c5bca6a37 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -19,8 +19,8 @@ .issuable-actions .clearfix.issue-btn-group.dropdown %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } - %span.caret Options + = icon('caret-down') .dropdown-menu.dropdown-menu-align-right.hidden-lg %ul %li{ class: merge_request_button_visibility(@merge_request, true) } diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml index 904452fcc4f..988ac0feae1 100644 --- a/app/views/projects/merge_requests/show/_versions.html.haml +++ b/app/views/projects/merge_requests/show/_versions.html.haml @@ -9,7 +9,7 @@ latest version - else version #{version_index(@merge_request_diff)} - %span.caret + = icon('caret-down') .dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-title %span Version: @@ -39,7 +39,7 @@ version #{version_index(@start_version)} - else #{@merge_request.target_branch} - %span.caret + = icon('caret-down') .dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-title %span Compared with: diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 44e645a7e81..5b7f83c344f 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -4,14 +4,15 @@ .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) } = ci_icon_for_status(status) %span - CI build + 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", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} + = 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 @@ -48,11 +49,12 @@ .mr-widget-heading .ci_widget.ci-success = ci_icon_for_status("success") - %span.hidden-sm + %span Deployed to = succeed '.' do = link_to environment.name, environment_path(environment), class: 'environment' - external_url = environment.external_url - if external_url = link_to external_url, target: '_blank' do - = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true) + %span.hidden-xs View on #{external_url.gsub(/\A.*?:\/\//, '')} + = icon('external-link', right: true) diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 6f5ee5f16c5..842b6df310d 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -35,3 +35,4 @@ 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 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 bf2e76f0083..ce43ca3a286 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -12,7 +12,7 @@ Merge When Build Succeeds - unless @project.only_allow_merge_if_build_succeeds? = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do - %span.caret + = icon('caret-down') %span.sr-only Select Merge Moment %ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' } diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 73772cc0e32..e62f810a521 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -30,13 +30,13 @@ .detail-page-description.milestone-detail %h2.title - = markdown escape_once(@milestone.title), pipeline: :single_line + = markdown_field(@milestone, :title) %div - if @milestone.description.present? .description .wiki = preserve do - = markdown @milestone.description + = markdown_field(@milestone, :description) - if @milestone.total_items_count(current_user).zero? .alert.alert-success.prepend-top-default diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index b2ece44d966..29df1bab04e 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -8,7 +8,7 @@ .project-network .controls = form_tag namespace_project_network_path(@project.namespace, @project, @id), method: :get, class: 'form-inline network-form' do |f| - = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Input an extended SHA1 syntax", class: 'search-input form-control input-mx-250 search-sha' + = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Git revision", class: 'search-input form-control input-mx-250 search-sha' = button_tag class: 'btn btn-success' do = icon('search') .inline.prepend-left-20 diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 788be4a0047..73fe6a715fa 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -61,7 +61,7 @@ .note-body{class: note_editable ? 'js-task-list-container' : ''} .note-text.md = preserve do - = note.note_html + = note.redacted_note_html = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - if note_editable = render 'projects/notes/edit_form', note: note diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 5800ef7de48..d288efc546f 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -33,7 +33,7 @@ - if @commit .commit-box.content-block %h3.commit-title - = markdown escape_once(@commit.title), pipeline: :single_line + = markdown(@commit.title, pipeline: :single_line) - if @commit.description.present? %pre.commit-description - = preserve(markdown(escape_once(@commit.description), pipeline: :single_line)) + = preserve(markdown(@commit.description, pipeline: :single_line)) diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 50c7e5044b2..2d1df095bfa 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -36,20 +36,20 @@ = link_to ci_lint_path, class: 'btn btn-default' do %span CI Lint - %ul.content-list.pipelines + %div.content-list.pipelines - stages = @pipelines.stages - if @pipelines.blank? - %li + %div .nothing-here-block No pipelines to show - else .table-holder %table.table.builds - %tbody - %th Status - %th Pipeline - %th Stages - %th - %th + %thead + %th.col-xs-1.col-sm-1 Status + %th.col-xs-2.col-sm-4 Pipeline + %th.col-xs-2.col-sm-2 Stages + %th.col-xs-2.col-sm-2 + %th.hidden-xs.col-sm-3 = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages = paginate @pipelines, theme: 'gitlab' diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml index 43a6fdfd103..d9c39fb87b7 100644 --- a/app/views/projects/repositories/_feed.html.haml +++ b/app/views/projects/repositories/_feed.html.haml @@ -12,7 +12,7 @@ = link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do %code= commit.short_id = image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: '' - = markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line, author: commit.author + = markdown(truncate(commit.title, length: 40), pipeline: :single_line, author: commit.author) %td %span.pull-right.cgray = time_ago_with_tooltip(commit.committed_date) diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml index 752b9e060d5..5afa193357e 100644 --- a/app/views/projects/runners/_shared_runners.html.haml +++ b/app/views/projects/runners/_shared_runners.html.haml @@ -1,8 +1,8 @@ %h3 Shared Runners .bs-callout.bs-callout-warning.shared-runners-description - - if shared_runners_text.present? - = markdown(shared_runners_text, pipeline: 'plain_markdown') + - if current_application_settings.shared_runners_text.present? + = markdown_field(current_application_settings, :shared_runners_text) - else GitLab Shared Runners execute code of different projects on the same Runner unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 9adce776c1c..ea4deb6cb28 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -71,9 +71,8 @@ = 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' + = render 'projects/buttons/download', project: @project, ref: @ref + = render 'projects/buttons/dropdown' = render 'shared/notifications/button', notification_setting: @notification_setting - if @repository.commit diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index 9773b8438ec..32e1f8a21b0 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -12,7 +12,7 @@ .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } Options - %span.caret + = icon('caret-down') .dropdown-menu.dropdown-menu-full-width %ul - if can?(current_user, :create_project_snippet, @project) diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index a156d98bab8..05fccb4f976 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -30,4 +30,4 @@ .description.prepend-top-default .wiki = preserve do - = markdown release.description + = markdown_field(release, :description) diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 6adbe9351dc..7a0d9dcc94f 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -14,7 +14,7 @@ %button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} } %span.light = @sort.humanize - %b.caret + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_tags_path(sort: nil) do diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 4dd7439b2d0..155af755759 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -33,6 +33,6 @@ .description .wiki = preserve do - = markdown @release.description + = markdown_field(@release, :description) - else This tag has no release notes. diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml index 8f68d6d1b87..e010f21de5a 100644 --- a/app/views/search/results/_issue.html.haml +++ b/app/views/search/results/_issue.html.haml @@ -7,7 +7,7 @@ - if issue.description.present? .description.term = preserve do - = search_md_sanitize(markdown(truncate(issue.description, length: 200, separator: " "), { project: issue.project, author: issue.author })) + = search_md_sanitize(issue, :description) %span.light #{issue.project.name_with_namespace} - if issue.closed? diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index 6331c2bd6b0..07b17bc69c0 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -6,7 +6,7 @@ - if merge_request.description.present? .description.term = preserve do - = search_md_sanitize(markdown(merge_request.description, { project: merge_request.project, author: merge_request.author })) + = search_md_sanitize(merge_request, :description) %span.light #{merge_request.project.name_with_namespace} .pull-right diff --git a/app/views/search/results/_milestone.html.haml b/app/views/search/results/_milestone.html.haml index b31595d8d1c..9664f65a36e 100644 --- a/app/views/search/results/_milestone.html.haml +++ b/app/views/search/results/_milestone.html.haml @@ -6,4 +6,4 @@ - if milestone.description.present? .description.term = preserve do - = search_md_sanitize(markdown(milestone.description)) + = search_md_sanitize(milestone, :description) diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index e0400083870..f3701b89bb4 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -23,4 +23,4 @@ .note-search-result .term = preserve do - = search_md_sanitize(markdown(note.note, {no_header_anchors: true, author: note.author})) + = search_md_sanitize(note, :note) diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index 77676454b57..6f593e8dff9 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -12,4 +12,4 @@ = link_to_label(label, tooltip: false) - if label.description %span.label-description - = markdown(label.description, pipeline: :single_line) + = markdown_field(label, :description) diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml index 51622931e24..fbbf6f358c5 100644 --- a/app/views/shared/_new_project_item_select.html.haml +++ b/app/views/shared/_new_project_item_select.html.haml @@ -3,7 +3,7 @@ = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at' } %a.btn.btn-new.new-project-item-select-button = local_assigns[:label] - %b.caret + = icon('caret-down') :javascript $('.new-project-item-select-button').on('click', function() { diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index 36bbac6fbf5..68e05cb72e1 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -5,7 +5,7 @@ = sort_options_hash[@sort] - else = sort_title_recently_created - %b.caret + = icon('caret-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/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index 1ad95351005..dc4ee3074d2 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -35,4 +35,4 @@ - if group.description.present? .description - = markdown(group.description, pipeline: :description) + = markdown_field(group, :description) diff --git a/app/views/shared/icons/_illustration_no_commits.svg b/app/views/shared/icons/_illustration_no_commits.svg new file mode 100644 index 00000000000..4f9d9add60d --- /dev/null +++ b/app/views/shared/icons/_illustration_no_commits.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 107" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="#eee" fill-rule="evenodd"><path d="m4.01 2h1.102c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-1.102c-2.218 0-4.01 1.788-4.01 4 0 .552.448 1 1 1 .552 0 1-.448 1-1 0-1.108.892-2 2.01-2m12.702 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m8.088 0c.822 0 1.554.503 1.86 1.254.208.512.791.758 1.303.55.512-.208.758-.791.55-1.303-.609-1.497-2.069-2.5-3.712-2.5h-2.188c-.552 0-1 .448-1 1 0 .552.448 1 1 1h2.188m2.01 12.518c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 6.282c0 1.108-.892 2-2.01 2h-.72c-.552 0-1 .448-1 1 0 .552.448 1 1 1h.72c2.218 0 4.01-1.788 4.01-4v-.382c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.382m-14.325 2c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-8.47 0c-.755 0-1.438-.424-1.782-1.085-.255-.49-.859-.681-1.349-.426-.49.255-.681.859-.426 1.349.684 1.316 2.046 2.162 3.556 2.162h2.57c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-2.57m-2.01-12.136c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-6.664c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.764c0 .552.448 1 1 1 .552 0 1-.448 1-1v-.764" id="0"/><circle cx="21" cy="24" r="10"/><rect width="33" height="3" x="37" y="18" rx="1.5" id="1"/><rect width="53" height="3" x="37" y="27" rx="1.5" id="2"/><path d="m131 29c0 .552.447.999.996.999h22.01c.545 0 .996-.451.996-.999v-9c0-.552-.447-.999-.996-.999h-22.01c-.545 0-.996.451-.996.999v9m.996-12h22.01c1.655 0 2.996 1.344 2.996 2.999v9c0 1.657-1.35 2.999-2.996 2.999h-22.01c-1.655 0-2.996-1.344-2.996-2.999v-9c0-1.657 1.35-2.999 2.996-2.999" id="3"/><g transform="translate(0 59)"><use xlink:href="#0"/><circle cx="21" cy="24" r="10"/><use xlink:href="#1"/><use xlink:href="#2"/><use xlink:href="#3"/></g></g></svg>
\ No newline at end of file diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index cf26197f7d7..31620297be0 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -1,3 +1,4 @@ +- finder = controller.controller_name == 'issues' || controller.controller_name == 'boards' ? issues_finder : merge_requests_finder - boards_page = controller.controller_name == 'boards' .issues-filters @@ -14,19 +15,19 @@ - if params[:author_id].present? = hidden_field_tag(:author_id, params[:author_id]) = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit", - placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) + placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user.try(:username), current_user: true, project_id: @project.try(:id), selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) .filter-item.inline - if params[:assignee_id].present? = hidden_field_tag(:assignee_id, params[:assignee_id]) = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", - placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) + placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user.try(:username), null_user: true, current_user: true, project_id: @project.try(:id), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) .filter-item.inline.milestone-filter - = render "shared/issuable/milestone_dropdown" + = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true .filter-item.inline.labels-filter - = render "shared/issuable/label_dropdown" + = 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[]" } .filter-item.inline.reset-filters %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters @@ -37,7 +38,7 @@ %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, project_id: @project.try(:id) } } + %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 .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" } diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 04373684ee9..c3f4e10c954 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -1,3 +1,4 @@ +- project = @target_project || @project = form_errors(issuable) - if @conflict @@ -82,38 +83,22 @@ = 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 - = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", - placeholder: 'Select assignee', class: 'custom-form-control', null_user: true, - selected: issuable.assignee_id, project: @target_project || @project, - first_user: true, current_user: true, include_blank: true) - %div - = link_to 'Assign to me', '#', class: 'assign-to-me-link prepend-top-5 inline' + - 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: "Filter by 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", show_menu_above: true } }) .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) } - - if milestone_options(issuable).present? - .issuable-form-select-holder - = f.select(:milestone_id, milestone_options(issuable), - { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } }) - - else - .prepend-top-10 - %span.light No open milestones available. - - if can? current_user, :admin_milestone, issuable.project - %div - = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline" + .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_menu_above: true, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input" .form-group - has_labels = issuable.project.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}" } - - if has_labels - .issuable-form-select-holder - = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, - { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } - - else - %span.light No labels yet. - - if can? current_user, :admin_label, issuable.project - %div - = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline" + .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, show_menu_above: 'true' } - if has_due_date .col-lg-6 .form-group diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index d34d28f6736..6d307611640 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,25 +1,29 @@ +- project = @target_project || @project - show_create = local_assigns.fetch(:show_create, true) - extra_options = local_assigns.fetch(:extra_options, true) - filter_submit = local_assigns.fetch(:filter_submit, true) - show_footer = local_assigns.fetch(:show_footer, true) +- use_id = local_assigns.fetch(:use_id, true) - data_options = local_assigns.fetch(:data_options, {}) - classes = local_assigns.fetch(:classes, []) -- dropdown_data = {toggle: 'dropdown', field_name: 'label_name[]', show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"} +- selected = local_assigns.fetch(:selected, nil) +- selected_toggle = local_assigns.fetch(:selected_toggle, nil) +- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"} - dropdown_data.merge!(data_options) - classes << 'js-extra-options' if extra_options - classes << 'js-filter-submit' if filter_submit -- if params[:label_name].present? - - if params[:label_name].respond_to?('any?') - - params[:label_name].each do |label| - = hidden_field_tag "label_name[]", label, id: nil +- if selected + - selected.each do |label| + = 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} - %span.dropdown-toggle-text - = h(multi_label_name(params[:label_name], "Label")) + %span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) } + = multi_label_name(selected, "Labels") = 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: "Filter by label", show_footer: show_footer, show_create: show_create } - - if show_create and @project and can?(current_user, :admin_label, @project) + - if show_create && project && can?(current_user, :admin_label, project) = render partial: "shared/issuable/label_page_create" = dropdown_loading diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index 2fcf40ece99..ab3cc33d18f 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -1,16 +1,20 @@ -- if params[:milestone_title].present? - = hidden_field_tag(:milestone_title, params[:milestone_title]) -= dropdown_tag(milestone_dropdown_label(params[:milestone_title]), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable", - placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, show_upcoming: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do - - if @project +- project = @target_project || @project +- extra_class = extra_class || '' +- show_menu_above = show_menu_above || false +- selected_text = selected.try(:title) +- if selected.present? + = hidden_field_tag(name, name == :milestone_title ? selected.title : selected.id) += dropdown_tag(milestone_dropdown_label(selected_text), options: { title: "Filter by milestone", 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 + - if project %ul.dropdown-footer-list - - if can? current_user, :admin_milestone, @project + - if can? current_user, :admin_milestone, project %li - = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do + = link_to new_namespace_project_milestone_path(project.namespace, project), title: "New Milestone" do Create new %li - = link_to namespace_project_milestones_path(@project.namespace, @project) do - - if can? current_user, :admin_milestone, @project + = link_to namespace_project_milestones_path(project.namespace, project) do + - if can? current_user, :admin_milestone, project Manage milestones - else View milestones diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index b13daaf43c9..f8059988038 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -108,29 +108,30 @@ .js-due-date-calendar - if issuable.project.labels.any? + - selected_labels = issuable.labels .block.labels .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } } = icon('tags') %span - = issuable.labels_array.size + = selected_labels.size .title.hide-collapsed Labels = icon('spinner spin', class: 'block-loading') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' - .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels_array.any?) } - - if issuable.labels_array.any? - - issuable.labels_array.each do |label| + .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) } + - if selected_labels.any? + - selected_labels.each do |label| = link_to_label(label, type: issuable.to_ability_name) - else %span.no-value None .selectbox.hide-collapsed - - issuable.labels_array.each do |label| + - 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{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} - %span.dropdown-toggle-text - Label + %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 = render partial: "shared/issuable/label_page_default" diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml index b15e8ea73fe..33f93dccd3c 100644 --- a/app/views/shared/milestones/_labels_tab.html.haml +++ b/app/views/shared/milestones/_labels_tab.html.haml @@ -8,7 +8,7 @@ = link_to milestones_label_path(options) do - render_colored_label(label, tooltip: false) %span.prepend-description-left - = markdown(label.description, pipeline: :single_line) + = markdown_field(label, :description) .pull-info-right %span.append-right-20 diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index 7ff947a51db..548215243db 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -26,7 +26,7 @@ .detail-page-description.milestone-detail %h2.title - = markdown escape_once(milestone.title), pipeline: :single_line + = markdown_field(milestone, :title) - if milestone.complete?(current_user) && milestone.active? .alert.alert-success.prepend-top-default @@ -55,4 +55,3 @@ Open %td = ms.expires_at - diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml index ff1cf966a9b..feaa5570c21 100644 --- a/app/views/shared/notifications/_button.html.haml +++ b/app/views/shared/notifications/_button.html.haml @@ -11,7 +11,7 @@ = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } } - %span.caret + = icon('caret-down') .sr-only Toggle dropdown - else %button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } } diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 66c309644a7..e8668048703 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -50,4 +50,4 @@ class: "commit-row-message" - elsif project.description.present? .description - = markdown(project.description, pipeline: :description) + = markdown_field(project, :description) diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index 773ce8ac240..dcdba01aee9 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -1,9 +1,12 @@ - unless @snippet.content.empty? - if markup?(@snippet.file_name) %textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}} - = @snippet.data + = @snippet.content .file-content.wiki - = render_markup(@snippet.file_name, @snippet.data) + - if gitlab_markdown?(@snippet.file_name) + = preserve(markdown_field(@snippet, :content)) + - else + = render_markup(@snippet.file_name, @snippet.content) - else = render 'shared/file_highlight', blob: @snippet - else diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 7ae4211ddfd..d7506e07ff6 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -21,4 +21,4 @@ = render "snippets/actions" %h2.snippet-title.prepend-top-0.append-bottom-0 - = markdown escape_once(@snippet.title), pipeline: :single_line, author: @snippet.author + = markdown_field(@snippet, :title) diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index c446dc3bdc1..1d0e549ed3d 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -12,7 +12,7 @@ .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } Options - %span.caret + = icon('caret-down') .dropdown-menu.dropdown-menu-full-width %ul %li diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index cd89155c616..27d7a6c5bb6 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -9,6 +9,7 @@ .file-actions = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") = 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 diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 60fc0c0daf6..1e0752bd3c3 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -40,11 +40,11 @@ .user-info .cover-title = @user.name - %span.handle - @#{@user.username} .cover-desc.member-date %span.middle-dot-divider + @#{@user.username} + %span.middle-dot-divider Member since #{@user.created_at.to_s(:medium)} .cover-desc @@ -82,7 +82,7 @@ %ul.nav-links.center.user-profile-nav %li.js-activity-tab - = link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do + = link_to user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do Activity %li.js-groups-tab = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do diff --git a/app/workers/clear_database_cache_worker.rb b/app/workers/clear_database_cache_worker.rb new file mode 100644 index 00000000000..c541daba50e --- /dev/null +++ b/app/workers/clear_database_cache_worker.rb @@ -0,0 +1,23 @@ +# This worker clears all cache fields in the database, working in batches. +class ClearDatabaseCacheWorker + include Sidekiq::Worker + + BATCH_SIZE = 1000 + + def perform + CacheMarkdownField.caching_classes.each do |kls| + fields = kls.cached_markdown_fields.html_fields + clear_cache_fields = fields.each_with_object({}) do |field, memo| + memo[field] = nil + end + + Rails.logger.debug("Clearing Markdown cache for #{kls}: #{fields.inspect}") + + kls.unscoped.in_batches(of: BATCH_SIZE) do |relation| + relation.update_all(clear_cache_fields) + end + end + + nil + end +end diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb index c64ea108d52..174eabff9fd 100644 --- a/app/workers/expire_build_artifacts_worker.rb +++ b/app/workers/expire_build_artifacts_worker.rb @@ -2,12 +2,11 @@ class ExpireBuildArtifactsWorker include Sidekiq::Worker def perform - Rails.logger.info 'Cleaning old build artifacts' + Rails.logger.info 'Scheduling removal of build artifacts' - builds = Ci::Build.with_expired_artifacts - builds.find_each(batch_size: 50).each do |build| - Rails.logger.debug "Removing artifacts build #{build.id}..." - build.erase_artifacts! - end + build_ids = Ci::Build.with_expired_artifacts.pluck(:id) + build_ids = build_ids.map { |build_id| [build_id] } + + Sidekiq::Client.push_bulk('class' => ExpireBuildInstanceArtifactsWorker, 'args' => build_ids ) end end diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb new file mode 100644 index 00000000000..916c2e633c1 --- /dev/null +++ b/app/workers/expire_build_instance_artifacts_worker.rb @@ -0,0 +1,11 @@ +class ExpireBuildInstanceArtifactsWorker + include Sidekiq::Worker + + def perform(build_id) + build = Ci::Build.with_expired_artifacts.reorder(nil).find_by(id: build_id) + return unless build + + Rails.logger.info "Removing artifacts build #{build.id}..." + build.erase_artifacts! + end +end diff --git a/app/workers/process_pipeline_worker.rb b/app/workers/process_pipeline_worker.rb new file mode 100644 index 00000000000..26ea5f1c24d --- /dev/null +++ b/app/workers/process_pipeline_worker.rb @@ -0,0 +1,10 @@ +class ProcessPipelineWorker + include Sidekiq::Worker + + sidekiq_options queue: :default + + def perform(pipeline_id) + Ci::Pipeline.find_by(id: pipeline_id) + .try(:process!) + end +end diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb new file mode 100644 index 00000000000..df4c4a6628b --- /dev/null +++ b/app/workers/trending_projects_worker.rb @@ -0,0 +1,11 @@ +class TrendingProjectsWorker + include Sidekiq::Worker + + sidekiq_options queue: :trending_projects + + def perform + Rails.logger.info('Refreshing trending projects') + + TrendingProject.refresh! + end +end diff --git a/app/workers/update_pipeline_worker.rb b/app/workers/update_pipeline_worker.rb new file mode 100644 index 00000000000..6ef5678073e --- /dev/null +++ b/app/workers/update_pipeline_worker.rb @@ -0,0 +1,10 @@ +class UpdatePipelineWorker + include Sidekiq::Worker + + sidekiq_options queue: :default + + def perform(pipeline_id) + Ci::Pipeline.find_by(id: pipeline_id) + .try(:update_status) + end +end |