summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/dispatcher.js.es68
-rw-r--r--app/assets/javascripts/extensions/object.js.es626
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es65
-rw-r--r--app/assets/javascripts/gl_dropdown.js26
-rw-r--r--app/assets/javascripts/issuable.js.es64
-rw-r--r--app/assets/javascripts/issues_bulk_assignment.js.es694
-rw-r--r--app/assets/javascripts/labels_select.js146
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss1
-rw-r--r--app/models/ci/pipeline.rb18
-rw-r--r--app/models/commit.rb12
-rw-r--r--app/services/ci/image_for_build_service.rb8
-rw-r--r--app/views/dashboard/todos/index.html.haml6
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_commits.html.haml4
-rw-r--r--changelogs/unreleased/24824-dropdown-items-focus.yml4
-rw-r--r--changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml4
-rw-r--r--changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml4
-rw-r--r--changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml4
-rw-r--r--changelogs/unreleased/25617-todos-filter-placeholder.yml4
-rw-r--r--changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml4
-rw-r--r--changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml4
-rw-r--r--changelogs/unreleased/show-commit-status-from-latest-pipeline.yml4
-rw-r--r--doc/project_services/mattermost_slash_commands.md18
-rw-r--r--features/admin/active_tab.feature54
-rw-r--r--features/steps/admin/active_tab.rb41
-rw-r--r--lib/gitlab/badge/build/status.rb4
-rw-r--r--lib/gitlab/chat_commands/issue_create.rb2
-rw-r--r--public/404.html9
-rw-r--r--public/422.html9
-rw-r--r--public/500.html9
-rw-r--r--public/502.html9
-rw-r--r--public/503.html9
-rw-r--r--spec/features/admin/admin_active_tab_spec.rb90
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb42
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb8
-rw-r--r--spec/javascripts/extensions/object_spec.js.es625
-rw-r--r--spec/lib/gitlab/badge/build/status_spec.rb4
-rw-r--r--spec/models/ci/pipeline_spec.rb59
-rw-r--r--spec/models/commit_spec.rb29
-rw-r--r--spec/models/group_spec.rb2
40 files changed, 537 insertions, 278 deletions
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 7d588e8eee6..1ec950494ff 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -74,7 +74,9 @@
case 'projects:merge_requests:index':
case 'projects:issues:index':
Issuable.init();
- new gl.IssuableBulkActions();
+ new gl.IssuableBulkActions({
+ prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_'
+ });
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:issues:show':
@@ -144,10 +146,6 @@
new ZenMode();
new MergedButtons();
break;
- case 'projects:merge_requests:index':
- shortcut_handler = new ShortcutsNavigation();
- Issuable.init();
- break;
case 'dashboard:activity':
new gl.Activities();
break;
diff --git a/app/assets/javascripts/extensions/object.js.es6 b/app/assets/javascripts/extensions/object.js.es6
new file mode 100644
index 00000000000..70a2d765abd
--- /dev/null
+++ b/app/assets/javascripts/extensions/object.js.es6
@@ -0,0 +1,26 @@
+/* eslint-disable no-restricted-syntax */
+
+// Adapted from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
+if (typeof Object.assign !== 'function') {
+ Object.assign = function assign(target, ...args) {
+ if (target == null) { // TypeError if undefined or null
+ throw new TypeError('Cannot convert undefined or null to object');
+ }
+
+ const to = Object(target);
+
+ for (let index = 0; index < args.length; index += 1) {
+ const nextSource = args[index];
+
+ if (nextSource != null) { // Skip over if undefined or null
+ for (const nextKey in nextSource) {
+ // Avoid bugs when hasOwnProperty is shadowed
+ if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
+ to[nextKey] = nextSource[nextKey];
+ }
+ }
+ }
+ }
+ return to;
+ };
+}
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 245383438d1..90010b9fd54 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -67,14 +67,15 @@
// The below is taken from At.js source
// Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js
- var _a, _y, regexp, match;
+ var _a, _y, regexp, match, atSymbols;
+ atSymbols = Object.keys(this.app.controllers).join('|');
subtext = subtext.split(' ').pop();
flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
_a = decodeURI("%C3%80");
_y = decodeURI("%C3%BF");
- regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?!\\W)([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)|([^\\x00-\\xff]*)$", 'gi');
+ regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?![" + atSymbols + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi');
match = regexp.exec(subtext);
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 68a345d83f9..57dabfe05e4 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -23,7 +23,6 @@
this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
$inputContainer = this.input.parent();
$clearButton = $inputContainer.find('.js-dropdown-input-clear');
- this.indeterminateIds = [];
$clearButton.on('click', (function(_this) {
// Clear click
return function(e) {
@@ -348,12 +347,12 @@
$el = $(this);
selected = self.rowClicked($el);
if (self.options.clicked) {
- self.options.clicked(selected, $el, e);
+ self.options.clicked(selected[0], $el, e, selected[1]);
}
// Update label right after all modifications in dropdown has been done
if (self.options.toggleLabel) {
- self.updateLabel(selected, $el, self);
+ self.updateLabel(selected[0], $el, self);
}
$el.trigger('blur');
@@ -444,12 +443,6 @@
this.resetRows();
this.addArrowKeyEvent();
- if (this.options.setIndeterminateIds) {
- this.options.setIndeterminateIds.call(this);
- }
- if (this.options.setActiveIds) {
- this.options.setActiveIds.call(this);
- }
// Makes indeterminate items effective
if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
this.parseData(this.fullData);
@@ -483,11 +476,6 @@
if (this.options.filterable) {
$input.blur().val("");
}
- // Triggering 'keyup' will re-render the dropdown which is not always required
- // specially if we want to keep the state of the dropdown needed for bulk-assignment
- if (!this.options.persistWhenHide) {
- $input.trigger("input");
- }
if (this.dropdown.find(".dropdown-toggle-page").length) {
$('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
}
@@ -620,7 +608,8 @@
};
GitLabDropdown.prototype.rowClicked = function(el) {
- var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value;
+ var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
+
fieldName = this.options.fieldName;
isInput = $(this.el).is('input');
if (this.renderedData) {
@@ -641,7 +630,7 @@
el.addClass(ACTIVE_CLASS);
}
- return selectedObject;
+ return [selectedObject];
}
field = [];
@@ -659,6 +648,7 @@
}
if (el.hasClass(ACTIVE_CLASS)) {
+ isMarking = false;
el.removeClass(ACTIVE_CLASS);
if (field && field.length) {
if (isInput) {
@@ -668,6 +658,7 @@
}
}
} else if (el.hasClass(INDETERMINATE_CLASS)) {
+ isMarking = true;
el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS);
if (field && field.length && value == null) {
@@ -677,6 +668,7 @@
this.addInput(fieldName, value, selectedObject);
}
} else {
+ isMarking = true;
if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
if (!isInput) {
@@ -697,7 +689,7 @@
}
}
- return selectedObject;
+ return [selectedObject, isMarking];
};
GitLabDropdown.prototype.focusTextInput = function() {
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index b174eb2ff96..1c10a7445bb 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -144,6 +144,9 @@
const $issuesOtherFilters = $('.issues-other-filters');
const $issuesBulkUpdate = $('.issues_bulk_update');
+ this.issuableBulkActions.willUpdateLabels = false;
+ this.issuableBulkActions.setOriginalDropdownData();
+
if ($checkedIssues.length > 0) {
let ids = $.map($checkedIssues, function(value) {
return $(value).data('id');
@@ -155,7 +158,6 @@
$updateIssuesIds.val([]);
$issuesBulkUpdate.hide();
$issuesOtherFilters.show();
- this.issuableBulkActions.willUpdateLabels = false;
}
return true;
},
diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6
index ad25104152c..1c8e5dede6f 100644
--- a/app/assets/javascripts/issues_bulk_assignment.js.es6
+++ b/app/assets/javascripts/issues_bulk_assignment.js.es6
@@ -5,9 +5,10 @@
((global) => {
class IssuableBulkActions {
- constructor({ container, form, issues } = {}) {
- this.container = container || $('.content'),
+ constructor({ container, form, issues, prefixId } = {}) {
+ this.prefixId = prefixId || 'issue_';
this.form = form || this.getElement('.bulk-update');
+ this.$labelDropdown = this.form.find('.js-label-select');
this.issues = issues || this.getElement('.issues-list .issue');
this.form.data('bulkActions', this);
this.willUpdateLabels = false;
@@ -16,10 +17,6 @@
Issuable.initChecks();
}
- getElement(selector) {
- return this.container.find(selector);
- }
-
bindEvents() {
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
}
@@ -73,10 +70,7 @@
getUnmarkedIndeterminedLabels() {
const result = [];
- const labelsToKeep = [];
-
- this.getElement('.labels-filter .is-indeterminate')
- .each((i, el) => labelsToKeep.push($(el).data('labelId')));
+ const labelsToKeep = this.$labelDropdown.data('indeterminate');
this.getLabelsFromSelection().forEach((id) => {
if (labelsToKeep.indexOf(id) === -1) {
@@ -106,45 +100,65 @@
}
};
if (this.willUpdateLabels) {
- this.getLabelsToApply().map(function(id) {
- return formData.update.add_label_ids.push(id);
- });
- this.getLabelsToRemove().map(function(id) {
- return formData.update.remove_label_ids.push(id);
- });
+ formData.update.add_label_ids = this.$labelDropdown.data('marked');
+ formData.update.remove_label_ids = this.$labelDropdown.data('unmarked');
}
return formData;
}
- getLabelsToApply() {
- 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;
+ setOriginalDropdownData() {
+ let $labelSelect = $('.bulk-update .js-label-select');
+ $labelSelect.data('common', this.getOriginalCommonIds());
+ $labelSelect.data('marked', this.getOriginalMarkedIds());
+ $labelSelect.data('indeterminate', this.getOriginalIndeterminateIds());
}
+ // From issuable's initial bulk selection
+ getOriginalCommonIds() {
+ let labelIds = [];
- /**
- * Returns Label IDs that will be removed from issue selection
- * @return {Array} Array of labels IDs
- */
+ this.getElement('.selected_issue:checked').each((i, el) => {
+ labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
+ });
+ return _.intersection.apply(this, labelIds);
+ }
- getLabelsToRemove() {
- const result = [];
- const indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
- const labelsToApply = this.getLabelsToApply();
- indeterminatedLabels.map(function(id) {
- // We need to exclude label IDs that will be applied
- // By not doing this will cause issues from selection to not add labels at all
- if (labelsToApply.indexOf(id) === -1) {
- return result.push(id);
- }
+ // From issuable's initial bulk selection
+ getOriginalMarkedIds() {
+ var labelIds = [];
+ this.getElement('.selected_issue:checked').each((i, el) => {
+ labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
- return result;
+ return _.intersection.apply(_, labelIds);
+ }
+
+ // From issuable's initial bulk selection
+ getOriginalIndeterminateIds() {
+ let uniqueIds = [];
+ let labelIds = [];
+ let issuableLabels = [];
+
+ // Collect unique label IDs for all checked issues
+ this.getElement('.selected_issue:checked').each((i, el) => {
+ issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
+ issuableLabels.forEach((labelId) => {
+ // Store unique IDs
+ if (uniqueIds.indexOf(labelId) === -1) {
+ uniqueIds.push(labelId);
+ }
+ });
+ // Store array of IDs per issuable
+ labelIds.push(issuableLabels);
+ });
+ // Add uniqueIds to add it as argument for _.intersection
+ labelIds.unshift(uniqueIds);
+ // Return IDs that are present but not in all selected issueables
+ return _.difference(uniqueIds, _.intersection.apply(this, labelIds));
+ }
+
+ getElement(selector) {
+ this.scopeEl = this.scopeEl || $('.content');
+ return this.scopeEl.find(selector);
}
}
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index d29a5edb9ad..6853d6b9db2 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -8,8 +8,9 @@
var _this;
_this = this;
$('.js-label-select').each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove;
+ var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container;
$dropdown = $(dropdown);
+ $dropdownContainer = $dropdown.closest('.labels-filter');
$toggleText = $dropdown.find('.dropdown-toggle-text');
namespacePath = $dropdown.data('namespace-path');
projectPath = $dropdown.data('project-path');
@@ -125,7 +126,7 @@
});
});
};
- return $dropdown.glDropdown({
+ $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
return $.ajax({
@@ -172,33 +173,40 @@
});
},
renderRow: function(label, instance) {
- var $a, $li, active, color, colorEl, indeterminate, removesAll, selectedClass, spacing;
+ var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
$li = $('<li>');
$a = $('<a href="#">');
selectedClass = [];
removesAll = label.id <= 0 || (label.id == null);
if ($dropdown.hasClass('js-filter-bulk-update')) {
- indeterminate = instance.indeterminateIds;
- active = instance.activeIds;
+ indeterminate = $dropdown.data('indeterminate') || [];
+ marked = $dropdown.data('marked') || [];
+
if (indeterminate.indexOf(label.id) !== -1) {
selectedClass.push('is-indeterminate');
}
- if (active.indexOf(label.id) !== -1) {
+
+ if (marked.indexOf(label.id) !== -1) {
// Remove is-indeterminate class if the item will be marked as active
i = selectedClass.indexOf('is-indeterminate');
if (i !== -1) {
selectedClass.splice(i, 1);
}
selectedClass.push('is-active');
- // Add input manually
- instance.addInput(this.fieldName, label.id);
}
- }
- if (this.id(label) && $form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + this.id(label).toString().replace(/'/g, '\\\'') + "']").length) {
- selectedClass.push('is-active');
- }
- if ($dropdown.hasClass('js-multiselect') && removesAll) {
- selectedClass.push('dropdown-clear-active');
+ } else {
+ if (this.id(label)) {
+ dropdownName = $dropdown.data('fieldName');
+ dropdownValue = this.id(label).toString().replace(/'/g, '\\\'');
+
+ if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) {
+ selectedClass.push('is-active');
+ }
+ }
+
+ if ($dropdown.hasClass('js-multiselect') && removesAll) {
+ selectedClass.push('dropdown-clear-active');
+ }
}
if (label.duplicate) {
spacing = 100 / label.color.length;
@@ -234,7 +242,6 @@
// Return generated html
return $li.html($a).prop('outerHTML');
},
- persistWhenHide: $dropdown.data('persistWhenHide'),
search: {
fields: ['title']
},
@@ -313,18 +320,15 @@
}
}
}
- if ($dropdown.hasClass('js-filter-bulk-update')) {
- // If we are persisting state we need the classes
- if (!this.options.persistWhenHide) {
- return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass();
- }
- }
},
multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
- clicked: function(label, $el, e) {
+ clicked: function(label, $el, e, isMarking) {
var isIssueIndex, isMRIndex, page;
- _this.enableBulkLabelDropdown();
+
+ page = $('body').data('page');
+ isIssueIndex = page === 'projects:issues:index';
+ isMRIndex = page === 'projects:merge_requests:index';
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
$dropdown.parent()
@@ -333,12 +337,11 @@
}
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+ _this.enableBulkLabelDropdown();
+ _this.setDropdownData($dropdown, isMarking, this.id(label));
return;
}
- page = $('body').data('page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = page === 'projects:merge_requests:index';
if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
@@ -400,17 +403,10 @@
}
}
},
- setIndeterminateIds: function() {
- if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
- return this.indeterminateIds = _this.getIndeterminateIds();
- }
- },
- setActiveIds: function() {
- if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
- return this.activeIds = _this.getActiveIds();
- }
- }
});
+
+ // Set dropdown data
+ _this.setOriginalDropdownData($dropdownContainer, $dropdown);
});
this.bindEvents();
}
@@ -423,34 +419,9 @@
if ($('.selected_issue:checked').length) {
return;
}
- // Remove inputs
- $('.issues_bulk_update .labels-filter input[type="hidden"]').remove();
- // Also restore button text
return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label');
};
- LabelsSelect.prototype.getIndeterminateIds = function() {
- var label_ids;
- label_ids = [];
- $('.selected_issue:checked').each(function(i, el) {
- var issue_id;
- issue_id = $(el).data('id');
- return label_ids.push($("#issue_" + issue_id).data('labels'));
- });
- return _.flatten(label_ids);
- };
-
- LabelsSelect.prototype.getActiveIds = function() {
- var label_ids;
- label_ids = [];
- $('.selected_issue:checked').each(function(i, el) {
- var issue_id;
- issue_id = $(el).data('id');
- return label_ids.push($("#issue_" + issue_id).data('labels'));
- });
- return _.intersection.apply(_, label_ids);
- };
-
LabelsSelect.prototype.enableBulkLabelDropdown = function() {
var issuableBulkActions;
if ($('.selected_issue:checked').length) {
@@ -459,8 +430,59 @@
}
};
- return LabelsSelect;
+ LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) {
+ var i, markedIds, unmarkedIds, indeterminateIds;
+ var issuableBulkActions = $('.bulk-update').data('bulkActions');
+
+ markedIds = $dropdown.data('marked') || [];
+ unmarkedIds = $dropdown.data('unmarked') || [];
+ indeterminateIds = $dropdown.data('indeterminate') || [];
+
+ if (isMarking) {
+ markedIds.push(value);
+
+ i = indeterminateIds.indexOf(value);
+ if (i > -1) {
+ indeterminateIds.splice(i, 1);
+ }
+
+ i = unmarkedIds.indexOf(value);
+ if (i > -1) {
+ unmarkedIds.splice(i, 1);
+ }
+ } else {
+ // If marked item (not common) is unmarked
+ i = markedIds.indexOf(value);
+ if (i > -1) {
+ markedIds.splice(i, 1);
+ }
+
+ // If an indeterminate item is being unmarked
+ if (issuableBulkActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
+ unmarkedIds.push(value);
+ }
+
+ // If a marked item is being unmarked
+ // (a marked item could also be a label that is present in all selection)
+ if (issuableBulkActions.getOriginalCommonIds().indexOf(value) > -1) {
+ unmarkedIds.push(value);
+ }
+ }
+ $dropdown.data('marked', markedIds);
+ $dropdown.data('unmarked', unmarkedIds);
+ $dropdown.data('indeterminate', indeterminateIds);
+ };
+
+ LabelsSelect.prototype.setOriginalDropdownData = function($container, $dropdown) {
+ var labels = [];
+ $container.find('[name="label_name[]"]').map(function() {
+ return labels.push(this.value);
+ });
+ $dropdown.data('marked', labels);
+ };
+
+ return LabelsSelect;
})();
}).call(this);
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index ecd540793d0..11adf2568a1 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -188,7 +188,6 @@
&.is-focused {
background-color: $dropdown-link-hover-bg;
text-decoration: none;
- outline: 0;
}
&.dropdown-menu-empty-link {
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 54f73171fd4..48354cdbefb 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -88,8 +88,24 @@ module Ci
end
# ref can't be HEAD or SHA, can only be branch/tag name
+ scope :latest, ->(ref = nil) do
+ max_id = unscope(:select)
+ .select("max(#{quoted_table_name}.id)")
+ .group(:ref, :sha)
+
+ if ref
+ where(id: max_id, ref: ref)
+ else
+ where(id: max_id)
+ end
+ end
+
+ def self.latest_status(ref = nil)
+ latest(ref).status
+ end
+
def self.latest_successful_for(ref)
- where(ref: ref).order(id: :desc).success.first
+ success.latest(ref).first
end
def self.truncate_sha(sha)
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 1831cc7e175..69cfc47f5bf 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -228,13 +228,9 @@ class Commit
def status(ref = nil)
@statuses ||= {}
- if @statuses.key?(ref)
- @statuses[ref]
- elsif ref
- @statuses[ref] = pipelines.where(ref: ref).status
- else
- @statuses[ref] = pipelines.status
- end
+ return @statuses[ref] if @statuses.key?(ref)
+
+ @statuses[ref] = pipelines.latest_status(ref)
end
def revert_branch_name
@@ -270,7 +266,7 @@ class Commit
@merged_merge_request_hash ||= Hash.new do |hash, user|
hash[user] = merged_merge_request_no_cache(user)
end
-
+
@merged_merge_request_hash[current_user]
end
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
index 75d847d5bee..240ddabec36 100644
--- a/app/services/ci/image_for_build_service.rb
+++ b/app/services/ci/image_for_build_service.rb
@@ -1,13 +1,13 @@
module Ci
class ImageForBuildService
def execute(project, opts)
- sha = opts[:sha] || ref_sha(project, opts[:ref])
-
+ ref = opts[:ref]
+ sha = opts[:sha] || ref_sha(project, ref)
pipelines = project.pipelines.where(sha: sha)
- pipelines = pipelines.where(ref: opts[:ref]) if opts[:ref]
- image_name = image_for_status(pipelines.status)
+ image_name = image_for_status(pipelines.latest_status(ref))
image_path = Rails.root.join('public/ci', image_name)
+
OpenStruct.new(path: image_path, name: image_name)
end
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index ea95e91eada..e13f404fee2 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -32,7 +32,7 @@
- if params[:project_id].present?
= hidden_field_tag(:project_id, params[:project_id])
= dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
- placeholder: 'Search projects', data: { data: todo_projects_options } })
+ placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project' } })
.filter-item.inline
- if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
@@ -42,12 +42,12 @@
- if params[:type].present?
= hidden_field_tag(:type, params[:type])
= dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
- data: { data: todo_types_options } })
+ data: { data: todo_types_options, default_label: 'Type' } })
.filter-item.inline.actions-filter
- if params[:action_id].present?
= hidden_field_tag(:action_id, params[:action_id])
= dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
- data: { data: todo_actions_options }})
+ data: { data: todo_actions_options, default_label: 'Action' } })
.pull-right
.dropdown.inline.prepend-left-10
%button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'}
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 959c796ecec..b3c43286a50 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,4 +1,4 @@
-%li{ class: mr_css_classes(merge_request) }
+%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } }
- if @bulk_edit
.issue-check
= check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml
index a0e12fb3f38..baa1ade5eee 100644
--- a/app/views/projects/merge_requests/show/_commits.html.haml
+++ b/app/views/projects/merge_requests/show/_commits.html.haml
@@ -1,6 +1,2 @@
-.content-block.oneline-block
- = icon("sort-amount-desc")
- Most recent commits displayed first
-
%ol#commits-list.list-unstyled
= render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch
diff --git a/changelogs/unreleased/24824-dropdown-items-focus.yml b/changelogs/unreleased/24824-dropdown-items-focus.yml
new file mode 100644
index 00000000000..66970c2a9a5
--- /dev/null
+++ b/changelogs/unreleased/24824-dropdown-items-focus.yml
@@ -0,0 +1,4 @@
+---
+title: Add focus state to dropdown items
+merge_request:
+author:
diff --git a/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml b/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml
new file mode 100644
index 00000000000..cc7c2604824
--- /dev/null
+++ b/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml
@@ -0,0 +1,4 @@
+---
+title: Improve bulk assignment for issuables
+merge_request:
+author:
diff --git a/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml
new file mode 100644
index 00000000000..531b0f83099
--- /dev/null
+++ b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml
@@ -0,0 +1,4 @@
+---
+title: Improve help message for issue create slash command
+merge_request: 7850
+author:
diff --git a/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml b/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml
new file mode 100644
index 00000000000..c6a92547c5c
--- /dev/null
+++ b/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml
@@ -0,0 +1,4 @@
+---
+title: Added go back anchor on error pages.
+merge_request: 8087
+author:
diff --git a/changelogs/unreleased/25617-todos-filter-placeholder.yml b/changelogs/unreleased/25617-todos-filter-placeholder.yml
new file mode 100644
index 00000000000..5d0adb04ef3
--- /dev/null
+++ b/changelogs/unreleased/25617-todos-filter-placeholder.yml
@@ -0,0 +1,4 @@
+---
+title: 25617 Fix placeholder color of todo filters
+merge_request:
+author:
diff --git a/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml b/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml
new file mode 100644
index 00000000000..11250643a23
--- /dev/null
+++ b/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml
@@ -0,0 +1,4 @@
+---
+title: Move admin active tab spinach tests to rspec
+merge_request: 8037
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml b/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml
new file mode 100644
index 00000000000..754af641add
--- /dev/null
+++ b/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml
@@ -0,0 +1,4 @@
+---
+title: Remove unnecessary commits order message
+merge_request: 8004
+author:
diff --git a/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml b/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml
new file mode 100644
index 00000000000..bbd7a217493
--- /dev/null
+++ b/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml
@@ -0,0 +1,4 @@
+---
+title: Show commit status from latest pipeline
+merge_request: 7333
+author:
diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md
index 6fcbf6f1642..1a7c13a29b4 100644
--- a/doc/project_services/mattermost_slash_commands.md
+++ b/doc/project_services/mattermost_slash_commands.md
@@ -65,7 +65,7 @@ the administrator console.
### Step 3. Create a new custom slash command in Mattermost
-Now that you have enabled the custom slash commands in Mattermost and opened
+Now that you have enabled custom slash commands in Mattermost and opened
the Mattermost slash commands service in GitLab, it's time to copy these values
in a new slash command.
@@ -128,20 +128,16 @@ GitLab using the Mattermost commands.
## Available slash commands
-The available slash commands so far are:
+The available slash commands are:
| Command | Description | Example |
| ------- | ----------- | ------- |
-| `/<trigger> issue create <title>\n<description>` | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | `/trigger issue create We need to change the homepage` |
-| `/<trigger> issue show <issue-number>` | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | `/trigger issue show 42` |
-| `/<trigger> deploy <environment> to <environment>` | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | `/trigger deploy staging to production` |
+| <kbd>/&lt;trigger&gt; issue new &lt;title&gt; <kbd>⇧ Shift</kbd>+<kbd>↵ Enter</kbd> &lt;description&gt;</kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue new We need to change the homepage</samp> |
+| <kbd>/&lt;trigger&gt; issue show &lt;issue-number&gt;</kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> |
+| <kbd>/&lt;trigger&gt; deploy &lt;environment&gt; to &lt;environment&gt;</kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> |
-To see a list of available commands that can interact with GitLab, type the
-trigger word followed by `help`:
-
-```
-/my-project help
-```
+To see a list of available commands to interact with GitLab, type the
+trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp>
![Mattermost bot available commands](img/mattermost_bot_available_commands.png)
diff --git a/features/admin/active_tab.feature b/features/admin/active_tab.feature
deleted file mode 100644
index f5bb06dea7d..00000000000
--- a/features/admin/active_tab.feature
+++ /dev/null
@@ -1,54 +0,0 @@
-@admin
-Feature: Admin Active Tab
- Background:
- Given I sign in as an admin
-
- Scenario: On Admin Home
- Given I visit admin page
- Then the active main tab should be Overview
- And no other main tabs should be active
-
- Scenario: On Admin Projects
- Given I visit admin projects page
- Then the active main tab should be Overview
- And the active sub tab should be Projects
- And no other main tabs should be active
- And no other sub tabs should be active
-
- Scenario: On Admin Groups
- Given I visit admin groups page
- Then the active main tab should be Overview
- And the active sub tab should be Groups
- And no other main tabs should be active
- And no other sub tabs should be active
-
- Scenario: On Admin Users
- Given I visit admin users page
- Then the active main tab should be Overview
- And the active sub tab should be Users
- And no other main tabs should be active
- And no other sub tabs should be active
-
- Scenario: On Admin Logs
- Given I visit admin logs page
- Then the active main tab should be Monitoring
- And the active sub tab should be Logs
- And no other main tabs should be active
- And no other sub tabs should be active
-
- Scenario: On Admin Messages
- Given I visit admin messages page
- Then the active main tab should be Messages
- And no other main tabs should be active
-
- Scenario: On Admin Hooks
- Given I visit admin hooks page
- Then the active main tab should be Hooks
- And no other main tabs should be active
-
- Scenario: On Admin Resque
- Given I visit admin Resque page
- Then the active main tab should be Monitoring
- And the active sub tab should be Resque
- And no other main tabs should be active
- And no other sub tabs should be active
diff --git a/features/steps/admin/active_tab.rb b/features/steps/admin/active_tab.rb
deleted file mode 100644
index 9b1689a8198..00000000000
--- a/features/steps/admin/active_tab.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-class Spinach::Features::AdminActiveTab < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedActiveTab
-
- step 'the active main tab should be Overview' do
- ensure_active_main_tab('Overview')
- end
-
- step 'the active sub tab should be Projects' do
- ensure_active_sub_tab('Projects')
- end
-
- step 'the active sub tab should be Groups' do
- ensure_active_sub_tab('Groups')
- end
-
- step 'the active sub tab should be Users' do
- ensure_active_sub_tab('Users')
- end
-
- step 'the active main tab should be Hooks' do
- ensure_active_main_tab('Hooks')
- end
-
- step 'the active main tab should be Monitoring' do
- ensure_active_main_tab('Monitoring')
- end
-
- step 'the active sub tab should be Resque' do
- ensure_active_sub_tab('Background Jobs')
- end
-
- step 'the active sub tab should be Logs' do
- ensure_active_sub_tab('Logs')
- end
-
- step 'the active main tab should be Messages' do
- ensure_active_main_tab('Messages')
- end
-end
diff --git a/lib/gitlab/badge/build/status.rb b/lib/gitlab/badge/build/status.rb
index 50aa45e5406..b762d85b6e5 100644
--- a/lib/gitlab/badge/build/status.rb
+++ b/lib/gitlab/badge/build/status.rb
@@ -20,8 +20,8 @@ module Gitlab
def status
@project.pipelines
- .where(sha: @sha, ref: @ref)
- .status || 'unknown'
+ .where(sha: @sha)
+ .latest_status(@ref) || 'unknown'
end
def metadata
diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb
index 1dba85c1b51..cefb6775db8 100644
--- a/lib/gitlab/chat_commands/issue_create.rb
+++ b/lib/gitlab/chat_commands/issue_create.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def self.help_message
- 'issue new <title>\n<description>'
+ 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>'
end
def self.allowed?(project, user)
diff --git a/public/404.html b/public/404.html
index 11b29d09a82..b3b3a0fa3f3 100644
--- a/public/404.html
+++ b/public/404.html
@@ -46,6 +46,14 @@
margin: 40px auto;
}
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
+ }
+
.container {
margin: auto 20px;
}
@@ -63,6 +71,7 @@
<hr />
<p>Make sure the address is correct and that the page hasn't moved.</p>
<p>Please contact your GitLab administrator if you think this is a mistake.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/public/422.html b/public/422.html
index 9bd7cb4b7c8..119e54ad8bd 100644
--- a/public/422.html
+++ b/public/422.html
@@ -46,6 +46,14 @@
margin: 40px auto;
}
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
+ }
+
.container {
margin: auto 20px;
}
@@ -63,6 +71,7 @@
<hr />
<p>Make sure you have access to the thing you tried to change.</p>
<p>Please contact your GitLab administrator if you think this is a mistake.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/public/500.html b/public/500.html
index f92e8839f8d..226ef3c40ea 100644
--- a/public/500.html
+++ b/public/500.html
@@ -46,6 +46,14 @@
margin: 40px auto;
}
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
+ }
+
.container {
margin: auto 20px;
}
@@ -63,6 +71,7 @@
<hr />
<p>Try refreshing the page, or going back and attempting the action again.</p>
<p>Please contact your GitLab administrator if this problem persists.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/public/502.html b/public/502.html
index c2be4f130a9..f037b81bace 100644
--- a/public/502.html
+++ b/public/502.html
@@ -46,6 +46,14 @@
margin: 40px auto;
}
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
+ }
+
.container {
margin: auto 20px;
}
@@ -63,6 +71,7 @@
<hr />
<p>Try refreshing the page, or going back and attempting the action again.</p>
<p>Please contact your GitLab administrator if this problem persists.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/public/503.html b/public/503.html
index 8850ffce362..f946a087871 100644
--- a/public/503.html
+++ b/public/503.html
@@ -46,6 +46,14 @@
margin: 40px auto;
}
+ a {
+ line-height: 100px;
+ font-weight: normal;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
+ }
+
.container {
margin: auto 20px;
}
@@ -63,6 +71,7 @@
<hr />
<p>Try refreshing the page, or going back and attempting the action again.</p>
<p>Please contact your GitLab administrator if this problem persists.</p>
+ <a href="javascript:history.back()">Go back</a>
</div>
</body>
</html>
diff --git a/spec/features/admin/admin_active_tab_spec.rb b/spec/features/admin/admin_active_tab_spec.rb
new file mode 100644
index 00000000000..f2eecc5b552
--- /dev/null
+++ b/spec/features/admin/admin_active_tab_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+RSpec.describe 'admin active tab' do
+ before do
+ login_as :admin
+ end
+
+ shared_examples 'page has active tab' do |title|
+ it "activates #{title} tab" do
+ expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 1)
+ expect(page.find('.layout-nav li.active')).to have_content(title)
+ end
+ end
+
+ shared_examples 'page has active sub tab' do |title|
+ it "activates #{title} sub tab" do
+ expect(page).to have_selector('.sub-nav li.active', count: 1)
+ expect(page.find('.sub-nav li.active')).to have_content(title)
+ end
+ end
+
+ context 'on home page' do
+ before do
+ visit admin_root_path
+ end
+
+ it_behaves_like 'page has active tab', 'Overview'
+ end
+
+ context 'on projects' do
+ before do
+ visit admin_namespaces_projects_path
+ end
+
+ it_behaves_like 'page has active tab', 'Overview'
+ it_behaves_like 'page has active sub tab', 'Projects'
+ end
+
+ context 'on groups' do
+ before do
+ visit admin_groups_path
+ end
+
+ it_behaves_like 'page has active tab', 'Overview'
+ it_behaves_like 'page has active sub tab', 'Groups'
+ end
+
+ context 'on users' do
+ before do
+ visit admin_users_path
+ end
+
+ it_behaves_like 'page has active tab', 'Overview'
+ it_behaves_like 'page has active sub tab', 'Users'
+ end
+
+ context 'on logs' do
+ before do
+ visit admin_logs_path
+ end
+
+ it_behaves_like 'page has active tab', 'Monitoring'
+ it_behaves_like 'page has active sub tab', 'Logs'
+ end
+
+ context 'on messages' do
+ before do
+ visit admin_broadcast_messages_path
+ end
+
+ it_behaves_like 'page has active tab', 'Messages'
+ end
+
+ context 'on hooks' do
+ before do
+ visit admin_hooks_path
+ end
+
+ it_behaves_like 'page has active tab', 'Hooks'
+ end
+
+ context 'on background jobs' do
+ before do
+ visit admin_background_jobs_path
+ end
+
+ it_behaves_like 'page has active tab', 'Monitoring'
+ it_behaves_like 'page has active sub tab', 'Background Jobs'
+ end
+end
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index bc2c087c9b9..832757b24d4 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -9,6 +9,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
let!(:issue2) { create(:issue, project: project, title: "Issue 2") }
let!(:bug) { create(:label, project: project, title: 'bug') }
let!(:feature) { create(:label, project: project, title: 'feature') }
+ let!(:wontfix) { create(:label, project: project, title: 'wontfix') }
context 'as an allowed user', js: true do
before do
@@ -291,6 +292,45 @@ feature 'Issues > Labels bulk assignment', feature: true do
expect(find("#issue_#{issue1.id}")).not_to have_content 'feature'
end
end
+
+ # Special case https://gitlab.com/gitlab-org/gitlab-ce/issues/24877
+ context 'unmarking common label' do
+ before do
+ issue1.labels << bug
+ issue1.labels << feature
+ issue2.labels << bug
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ it 'applies label from filtered results' do
+ check 'check_all_issues'
+
+ page.within('.issues_bulk_update') do
+ click_button 'Labels'
+ wait_for_ajax
+
+ expect(find('.dropdown-menu-labels li', text: 'bug')).to have_css('.is-active')
+ expect(find('.dropdown-menu-labels li', text: 'feature')).to have_css('.is-indeterminate')
+
+ click_link 'bug'
+ find('.dropdown-input-field', visible: true).set('wontfix')
+ click_link 'wontfix'
+ end
+
+ update_issues
+
+ page.within '.issues-holder' do
+ expect(find("#issue_#{issue1.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).to have_content 'feature'
+ expect(find("#issue_#{issue1.id}")).to have_content 'wontfix'
+
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'feature'
+ expect(find("#issue_#{issue2.id}")).to have_content 'wontfix'
+ end
+ end
+ end
end
context 'as a guest' do
@@ -320,7 +360,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
def open_labels_dropdown(items = [], unmark = false)
page.within('.issues_bulk_update') do
- click_button 'Label'
+ click_button 'Labels'
wait_for_ajax
items.map do |item|
click_link item
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index cd0512a37e6..da64827b377 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -89,4 +89,12 @@ feature 'GFM autocomplete', feature: true, js: true do
end
end
end
+
+ it 'doesnt open autocomplete after non-word character' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys("@#{user.username[0..2]}!")
+ end
+
+ expect(page).not_to have_selector('.atwho-view')
+ end
end
diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6
new file mode 100644
index 00000000000..3b71c255b30
--- /dev/null
+++ b/spec/javascripts/extensions/object_spec.js.es6
@@ -0,0 +1,25 @@
+/*= require extensions/object */
+
+describe('Object extensions', () => {
+ describe('assign', () => {
+ it('merges source object into target object', () => {
+ const targetObj = {};
+ const sourceObj = {
+ foo: 'bar',
+ };
+ Object.assign(targetObj, sourceObj);
+ expect(targetObj.foo).toBe('bar');
+ });
+
+ it('merges object with the same properties', () => {
+ const targetObj = {
+ foo: 'bar',
+ };
+ const sourceObj = {
+ foo: 'baz',
+ };
+ Object.assign(targetObj, sourceObj);
+ expect(targetObj.foo).toBe('baz');
+ });
+ });
+});
diff --git a/spec/lib/gitlab/badge/build/status_spec.rb b/spec/lib/gitlab/badge/build/status_spec.rb
index 38eebb2a176..70f03021d36 100644
--- a/spec/lib/gitlab/badge/build/status_spec.rb
+++ b/spec/lib/gitlab/badge/build/status_spec.rb
@@ -69,8 +69,8 @@ describe Gitlab::Badge::Build::Status do
new_build.success!
end
- it 'reports the compound status' do
- expect(badge.status).to eq 'failed'
+ it 'does not take outdated pipeline into account' do
+ expect(badge.status).to eq 'success'
end
end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index e78ae14b737..52dd41065e9 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -381,6 +381,65 @@ describe Ci::Pipeline, models: true do
end
end
+ shared_context 'with some outdated pipelines' do
+ before do
+ create_pipeline(:canceled, 'ref', 'A')
+ create_pipeline(:success, 'ref', 'A')
+ create_pipeline(:failed, 'ref', 'B')
+ create_pipeline(:skipped, 'feature', 'C')
+ end
+
+ def create_pipeline(status, ref, sha)
+ create(:ci_empty_pipeline, status: status, ref: ref, sha: sha)
+ end
+ end
+
+ describe '.latest' do
+ include_context 'with some outdated pipelines'
+
+ context 'when no ref is specified' do
+ let(:pipelines) { described_class.latest.all }
+
+ it 'returns the latest pipeline for the same ref and different sha' do
+ expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C')
+ expect(pipelines.map(&:status)).
+ to contain_exactly('success', 'failed', 'skipped')
+ end
+ end
+
+ context 'when ref is specified' do
+ let(:pipelines) { described_class.latest('ref').all }
+
+ it 'returns the latest pipeline for ref and different sha' do
+ expect(pipelines.map(&:sha)).to contain_exactly('A', 'B')
+ expect(pipelines.map(&:status)).
+ to contain_exactly('success', 'failed')
+ end
+ end
+ end
+
+ describe '.latest_status' do
+ include_context 'with some outdated pipelines'
+
+ context 'when no ref is specified' do
+ let(:latest_status) { described_class.latest_status }
+
+ it 'returns the latest status for the same ref and different sha' do
+ expect(latest_status).to eq(described_class.latest.status)
+ expect(latest_status).to eq('failed')
+ end
+ end
+
+ context 'when ref is specified' do
+ let(:latest_status) { described_class.latest_status('ref') }
+
+ it 'returns the latest status for ref and different sha' do
+ expect(latest_status).to eq(described_class.latest_status('ref'))
+ expect(latest_status).to eq('failed')
+ end
+ end
+ end
+
describe '#status' do
let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') }
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 0935fc0561c..74b50d2908d 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -213,23 +213,19 @@ eos
end
describe '#status' do
- context 'without arguments for compound status' do
- shared_examples 'giving the status from pipeline' do
- it do
- expect(commit.status).to eq(Ci::Pipeline.status)
- end
- end
-
- context 'with pipelines' do
- let!(:pipeline) do
- create(:ci_empty_pipeline, project: project, sha: commit.sha)
+ context 'without ref argument' do
+ before do
+ %w[success failed created pending].each do |status|
+ create(:ci_empty_pipeline,
+ project: project,
+ sha: commit.sha,
+ status: status)
end
-
- it_behaves_like 'giving the status from pipeline'
end
- context 'without pipelines' do
- it_behaves_like 'giving the status from pipeline'
+ it 'gives compound status from latest pipelines' do
+ expect(commit.status).to eq(Ci::Pipeline.latest_status)
+ expect(commit.status).to eq('pending')
end
end
@@ -255,8 +251,9 @@ eos
expect(commit.status('fix')).to eq(pipeline_from_fix.status)
end
- it 'gives compound status if ref is nil' do
- expect(commit.status(nil)).to eq(commit.status)
+ it 'gives compound status from latest pipelines if ref is nil' do
+ expect(commit.status(nil)).to eq(Ci::Pipeline.latest_status)
+ expect(commit.status(nil)).to eq('failed')
end
end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 893c6827a91..7d5ecfbaa64 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -272,7 +272,7 @@ describe Group, models: true do
end
describe 'nested group' do
- subject { create(:group, :nested) }
+ subject { build(:group, :nested) }
it { is_expected.to be_valid }
it { expect(subject.parent).to be_kind_of(Group) }