diff options
author | Lin Jen-Shin <godfat@godfat.org> | 2016-10-08 07:51:07 +0000 |
---|---|---|
committer | Lin Jen-Shin <godfat@godfat.org> | 2016-10-08 07:51:07 +0000 |
commit | 94611607e56f0e0f0b05682481db79ff99e9e34e (patch) | |
tree | d2efea4ac3a6ec9aa3feabeb5c2187cfdd91d8f6 /app | |
parent | 720968cc8799f665f4f4392e80bf8dfe88fdd69b (diff) | |
parent | 28ca8502c254d5c3edfb7ece36fc365e7a715df0 (diff) | |
download | gitlab-ce-94611607e56f0e0f0b05682481db79ff99e9e34e.tar.gz |
Merge remote-tracking branch 'upstream/master' into pipeline-emails
* upstream/master: (292 commits)
Deletes extra empty line breaking the build
Optimize the `award_user_list` helper spec
Fix typo and add he MWBS accronym for "Merge When Build Succeeds"
Added missing content and improved layout
ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
Improve the contribution and MR review guide
Updates test in order to look for link
Make projects API docs match parameter style
Fix Event#reset_project_activity updates
Update user whitelist reject message
Call ensure_secret_token! in secret token test's before block since it would be called in an initializer.
Add a CHANGELOG for CacheMarkdownField
Enable CacheMarkdownField for the remaining models
Make search results use the markdown cache columns, treating them consistently
Use CacheMarkdownField for notes
Add markdown cache columns to the database, but don't use them yet
Update issue board spec
Link to Registry docs from project settings
Truncate long labels with ellipsis in labels page
Improve issue load time performance by avoiding ORDER BY in find_by call
...
Diffstat (limited to 'app')
224 files changed, 2526 insertions, 1616 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/LabelManager.js.es6 b/app/assets/javascripts/LabelManager.js.es6 new file mode 100644 index 00000000000..bc68e53504f --- /dev/null +++ b/app/assets/javascripts/LabelManager.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/activities.js b/app/assets/javascripts/activities.js index d5e11e22be5..f4f8cf04184 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -21,16 +21,14 @@ }; Activities.prototype.toggleFilter = function(sender) { - var event_filters, filter; + var filter = sender.attr("id").split("_")[0]; + $('.event-filter .active').removeClass("active"); - event_filters = $.cookie("event_filter"); - filter = sender.attr("id").split("_")[0]; - $.cookie("event_filter", (event_filters !== filter ? filter : ""), { + $.cookie("event_filter", filter, { path: gon.relative_url_root || '/' }); - if (event_filters !== filter) { - return sender.closest('li').toggleClass("active"); - } + + sender.closest('li').toggleClass("active"); }; return Activities; 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/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/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..8d99b12102d 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(); @@ -165,7 +169,7 @@ break; case 'projects:labels:index': if ($('.prioritized-labels').length) { - new LabelManager(); + new gl.LabelManager(); } break; case 'projects:network:show': @@ -279,7 +283,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/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 1b6db641200..d4403375643 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(); 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/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/pipeline.js.es6 b/app/assets/javascripts/pipeline.js.es6 index bf33eb10100..8813bb5dfef 100644 --- a/app/assets/javascripts/pipeline.js.es6 +++ b/app/assets/javascripts/pipeline.js.es6 @@ -3,12 +3,21 @@ 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 $icon = $(this).find('.fa'); $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed'); const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed'); + const expandIcon = 'fa-caret-down'; + const hideIcon = 'fa-caret-up'; - graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide') + if(graphCollapsed) { + $btnText.text('Expand'); + $icon.removeClass(hideIcon).addClass(expandIcon); + } else { + $btnText.text('Hide'); + $icon.removeClass(expandIcon).addClass(hideIcon); + } } $(document).on('click', '.toggle-pipeline-btn', toggleGraph); 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/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index ce489f7c3de..d11b2fe7ec2 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -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; 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..a67d31de2f7 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -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/selects.scss b/app/assets/stylesheets/framework/selects.scss index c75dacf95d9..bcd60391543 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; } } 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/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/labels.scss b/app/assets/stylesheets/pages/labels.scss index 38c7cd98e41..822830706a5 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -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/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 926247e5e87..bc8693ae467 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -70,7 +70,8 @@ &.ci-success { color: $gl-success; - a.environment { + a.environment, + a.pipeline { color: inherit; } } @@ -349,6 +350,10 @@ .issuable-form-select-holder { display: inline-block; width: 250px; + + .dropdown-menu-toggle { + width: 100%; + } } .table-holder { diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index b035bfc9f3c..a2779704eff 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; - } } } @@ -385,6 +383,8 @@ left: auto; right: -214px; top: -9px; + max-height: 245px; + overflow-y: scroll; a:hover { .ci-status-text { 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/projects.scss b/app/assets/stylesheets/pages/projects.scss index 78bc4b79e86..87548dcb590 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; } 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 bd4ba384b29..b3455e04c29 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -173,7 +173,8 @@ class ApplicationController < ActionController::Base end def event_filter - filters = cookies['event_filter'].split(',') if cookies['event_filter'].present? + # 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/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/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 88a0c18180b..38e5943eb76 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController end def trending - @projects = TrendingProjectsFinder.new.execute(current_user) + @projects = TrendingProjectsFinder.new.execute @projects = filter_projects(@projects) @projects = @projects.page(params[:page]) 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/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..ffd9833e3b1 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -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! @@ -210,29 +212,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 @@ -490,6 +489,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 +541,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/finders/trending_projects_finder.rb b/app/finders/trending_projects_finder.rb index 81a12403801..c1e434d9926 100644 --- a/app/finders/trending_projects_finder.rb +++ b/app/finders/trending_projects_finder.rb @@ -1,11 +1,16 @@ +# Finder for retrieving public trending projects in a given time range. class TrendingProjectsFinder - def execute(current_user, start_date = 1.month.ago) - projects_for(current_user).trending(start_date) + # current_user - The currently logged in User, if any. + # last_months - The number of months to limit the trending data to. + def execute(months_limit = 1) + Rails.cache.fetch(cache_key_for(months_limit), expires_in: 1.day) do + Project.public_only.trending(months_limit.months.ago) + end end private - def projects_for(current_user) - ProjectsFinder.new.execute(current_user) + def cache_key_for(months) + "trending_projects/#{months}" 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/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/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/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/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_status.rb b/app/models/commit_status.rb index 736db1ab0f6..9fa8d17e74e 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -24,7 +24,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,13 +84,18 @@ 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? + commit_status.pipeline.try do |pipeline| + break if transition.loopback? + + if commit_status.complete? + ProcessPipelineWorker.perform_async(pipeline.id) + end + + UpdatePipelineWorker.perform_async(pipeline.id) + end + + true end after_transition [:created, :pending, :running] => :success do |commit_status| @@ -111,7 +131,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..314d5ba438f 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -328,13 +328,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 +344,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/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..b7f2b2bbe61 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" 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 b34e320cd34..a89ad9bca1d 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 @@ -381,6 +384,7 @@ class Project < ActiveRecord::Base SELECT project_id, COUNT(*) AS amount FROM notes WHERE created_at >= #{sanitize(since)} + AND system IS FALSE GROUP BY project_id ) join_note_counts ON projects.id = join_note_counts.project_id" 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/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..bf59b74495b 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -838,6 +838,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 +1043,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 2259e05a4e5..625fbc48302 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/user.rb b/app/models/user.rb index 6996740eebd..892ac28d5b3 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 @@ -827,6 +832,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 +902,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/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/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/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/system_note_service.rb b/app/services/system_note_service.rb index bf251816e7e..1ce66d50368 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -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/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/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/_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/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..da5b9832ba5 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -3,7 +3,7 @@ .btn.btn-grouped.btn-white.toggle-pipeline-btn %span.toggle-btn-text Hide %span pipeline graph - %span.caret + = icon('caret-up') - 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 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/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/edit.html.haml b/app/views/projects/edit.html.haml index a04d53e02bf..d19422c8657 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -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/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/show.html.haml b/app/views/projects/issues/show.html.haml index 3fb4191c60e..b94d6f8633c 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -23,7 +23,7 @@ .issuable-actions .clearfix.issue-btn-group.dropdown %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } - %span.caret + = icon('caret-down') Options .dropdown-menu.dropdown-menu-align-right.hidden-lg %ul @@ -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_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..88d8013a0d1 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -19,34 +19,32 @@ .mr-compare.merge-request %ul.merge-request-tabs.nav-links.no-top.no-bottom - %li.commits-tab + %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.active + %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.active - = link_to url_for(params), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do + %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= @diffs.real_size + %span.badge= @merge_request.diff_size .tab-content - #commits.commits.tab-pane + #commits.commits.tab-pane.active = 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 + #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){ $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change"); @@ -54,6 +52,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..46a2d862c91 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -4,9 +4,6 @@ - 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/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..9f2a0f5d99a 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -19,7 +19,7 @@ .issuable-actions .clearfix.issue-btn-group.dropdown %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } - %span.caret + = icon('caret-down') Options .dropdown-menu.dropdown-menu-align-right.hidden-lg %ul 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/_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/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/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index 8824bcc158e..3480800369a 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -1,4 +1,5 @@ %ul.nav-links.event-filter.scrolling-tabs + = event_filter_link EventFilter.all, 'All' = event_filter_link EventFilter.push, 'Push events' = event_filter_link EventFilter.merged, 'Merge events' = event_filter_link EventFilter.comments, 'Comments' 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/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/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/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 |