Commit e289ce65 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 3995-improve-sast

* master: (31 commits)
  Fix prop type
  Include epics from subgroups
  Port changes from gitlab-org/gitlab-ee!4064 to CE
  Replace $.get in single file diff with axios
  Add Geo attachment replication QA spec
  Update icon size and alignment Update neutral icon color
  cache jQuery selector
  Add createNewItemFromValue option and clearDropdown method
  Add createNewItemFromValue option and clearDropdown method
  Don't run scripts/lint-changelog-yaml in scripts/static-analysis but only in the 'docs lint' job
  Update CHANGELOG.md for 10.4.2
  Check MR state before submitting queries for discussion state
  Fix grape-route-helper route shadowing
  fixed issuable_spec.js
  Converted integration_settings_form to axios
  Converted issuable_bulk_update_actions to axios
  Converted issuable_index to axios
  Converted group_label_subscription to axios
  Converted graphs_show to axios
  Converted gl_dropdown to axios
  ...
parents 7f818790 cdefac3a
...@@ -12,6 +12,7 @@ export default class CreateItemDropdown { ...@@ -12,6 +12,7 @@ export default class CreateItemDropdown {
this.fieldName = options.fieldName; this.fieldName = options.fieldName;
this.onSelect = options.onSelect || (() => {}); this.onSelect = options.onSelect || (() => {});
this.getDataOption = options.getData; this.getDataOption = options.getData;
this.createNewItemFromValueOption = options.createNewItemFromValue;
this.$dropdown = options.$dropdown; this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent(); this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
...@@ -30,15 +31,15 @@ export default class CreateItemDropdown { ...@@ -30,15 +31,15 @@ export default class CreateItemDropdown {
filterable: true, filterable: true,
remote: false, remote: false,
search: { search: {
fields: ['title'], fields: ['text'],
}, },
selectable: true, selectable: true,
toggleLabel(selected) { toggleLabel(selected) {
return (selected && 'id' in selected) ? selected.title : this.defaultToggleLabel; return (selected && 'id' in selected) ? _.escape(selected.title) : this.defaultToggleLabel;
}, },
fieldName: this.fieldName, fieldName: this.fieldName,
text(item) { text(item) {
return _.escape(item.title); return _.escape(item.text);
}, },
id(item) { id(item) {
return _.escape(item.id); return _.escape(item.id);
...@@ -51,6 +52,11 @@ export default class CreateItemDropdown { ...@@ -51,6 +52,11 @@ export default class CreateItemDropdown {
}); });
} }
clearDropdown() {
this.$dropdownContainer.find('.dropdown-content').html('');
this.$dropdownContainer.find('.dropdown-input-field').val('');
}
bindEvents() { bindEvents() {
this.$createButton.on('click', this.onClickCreateWildcard.bind(this)); this.$createButton.on('click', this.onClickCreateWildcard.bind(this));
} }
...@@ -58,9 +64,13 @@ export default class CreateItemDropdown { ...@@ -58,9 +64,13 @@ export default class CreateItemDropdown {
onClickCreateWildcard(e) { onClickCreateWildcard(e) {
e.preventDefault(); e.preventDefault();
this.refreshData();
this.$dropdown.data('glDropdown').selectRowAtIndex();
}
refreshData() {
// Refresh the dropdown's data, which ends up calling `getData` // Refresh the dropdown's data, which ends up calling `getData`
this.$dropdown.data('glDropdown').remote.execute(); this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex();
} }
getData(term, callback) { getData(term, callback) {
...@@ -79,20 +89,28 @@ export default class CreateItemDropdown { ...@@ -79,20 +89,28 @@ export default class CreateItemDropdown {
}); });
} }
toggleCreateNewButton(item) { createNewItemFromValue(newValue) {
if (item) { if (this.createNewItemFromValueOption) {
this.selectedItem = { return this.createNewItemFromValueOption(newValue);
title: item, }
id: item,
text: item, return {
}; title: newValue,
id: newValue,
text: newValue,
};
}
toggleCreateNewButton(newValue) {
if (newValue) {
this.selectedItem = this.createNewItemFromValue(newValue);
this.$dropdownContainer this.$dropdownContainer
.find('.js-dropdown-create-new-item code') .find('.js-dropdown-create-new-item code')
.text(item); .text(newValue);
} }
this.toggleFooter(!item); this.toggleFooter(!newValue);
} }
toggleFooter(toggleState) { toggleFooter(toggleState) {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
/* global fuzzaldrinPlus */ /* global fuzzaldrinPlus */
import _ from 'underscore'; import _ from 'underscore';
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import axios from './lib/utils/axios_utils';
import { visitUrl } from './lib/utils/url_utility'; import { visitUrl } from './lib/utils/url_utility';
import { isObject } from './lib/utils/type_utility'; import { isObject } from './lib/utils/type_utility';
...@@ -212,25 +213,17 @@ GitLabDropdownRemote = (function() { ...@@ -212,25 +213,17 @@ GitLabDropdownRemote = (function() {
}; };
GitLabDropdownRemote.prototype.fetchData = function() { GitLabDropdownRemote.prototype.fetchData = function() {
return $.ajax({ if (this.options.beforeSend) {
url: this.dataEndpoint, this.options.beforeSend();
dataType: this.options.dataType, }
beforeSend: (function(_this) {
return function() { // Fetch the data through ajax if the data is a string
if (_this.options.beforeSend) { return axios.get(this.dataEndpoint)
return _this.options.beforeSend(); .then(({ data }) => {
} if (this.options.success) {
}; return this.options.success(data);
})(this), }
success: (function(_this) { });
return function(data) {
if (_this.options.success) {
return _this.options.success(data);
}
};
})(this)
});
// Fetch the data through ajax if the data is a string
}; };
return GitLabDropdownRemote; return GitLabDropdownRemote;
......
import flash from '../flash';
import { __ } from '../locale';
import axios from '../lib/utils/axios_utils';
import ContributorsStatGraph from './stat_graph_contributors'; import ContributorsStatGraph from './stat_graph_contributors';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
$.ajax({ const url = document.querySelector('.js-graphs-show').dataset.projectGraphPath;
type: 'GET',
url: document.querySelector('.js-graphs-show').dataset.projectGraphPath, axios.get(url)
dataType: 'json', .then(({ data }) => {
success(data) {
const graph = new ContributorsStatGraph(); const graph = new ContributorsStatGraph();
graph.init(data); graph.init(data);
...@@ -16,6 +18,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -16,6 +18,6 @@ document.addEventListener('DOMContentLoaded', () => {
$('.stat-graph').fadeIn(); $('.stat-graph').fadeIn();
$('.loading-graph').hide(); $('.loading-graph').hide();
}, })
}); .catch(() => flash(__('Error fetching contributors data.')));
}); });
import axios from './lib/utils/axios_utils';
import flash from './flash';
import { __ } from './locale';
export default class GroupLabelSubscription { export default class GroupLabelSubscription {
constructor(container) { constructor(container) {
const $container = $(container); const $container = $(container);
...@@ -13,14 +17,12 @@ export default class GroupLabelSubscription { ...@@ -13,14 +17,12 @@ export default class GroupLabelSubscription {
event.preventDefault(); event.preventDefault();
const url = this.$unsubscribeButtons.attr('data-url'); const url = this.$unsubscribeButtons.attr('data-url');
axios.post(url)
$.ajax({ .then(() => {
type: 'POST', this.toggleSubscriptionButtons();
url, this.$unsubscribeButtons.removeAttr('data-url');
}).done(() => { })
this.toggleSubscriptionButtons(); .catch(() => flash(__('There was an error when unsubscribing from this label.')));
this.$unsubscribeButtons.removeAttr('data-url');
});
} }
subscribe(event) { subscribe(event) {
...@@ -31,12 +33,9 @@ export default class GroupLabelSubscription { ...@@ -31,12 +33,9 @@ export default class GroupLabelSubscription {
this.$unsubscribeButtons.attr('data-url', url); this.$unsubscribeButtons.attr('data-url', url);
$.ajax({ axios.post(url)
type: 'POST', .then(() => this.toggleSubscriptionButtons())
url, .catch(() => flash(__('There was an error when subscribing to this label.')));
}).done(() => {
this.toggleSubscriptionButtons();
});
} }
toggleSubscriptionButtons() { toggleSubscriptionButtons() {
......
import Flash from '../flash'; import axios from '../lib/utils/axios_utils';
import flash from '../flash';
export default class IntegrationSettingsForm { export default class IntegrationSettingsForm {
constructor(formSelector) { constructor(formSelector) {
...@@ -95,29 +96,26 @@ export default class IntegrationSettingsForm { ...@@ -95,29 +96,26 @@ export default class IntegrationSettingsForm {
*/ */
testSettings(formData) { testSettings(formData) {
this.toggleSubmitBtnState(true); this.toggleSubmitBtnState(true);
$.ajax({
type: 'PUT', return axios.put(this.testEndPoint, formData)
url: this.testEndPoint, .then(({ data }) => {
data: formData, if (data.error) {
}) flash(`${data.message} ${data.service_response}`, 'alert', document, {
.done((res) => { title: 'Save anyway',
if (res.error) { clickHandler: (e) => {
new Flash(`${res.message} ${res.service_response}`, 'alert', document, { e.preventDefault();
title: 'Save anyway', this.$form.submit();
clickHandler: (e) => { },
e.preventDefault(); });
this.$form.submit(); } else {
}, this.$form.submit();
}); }
} else {
this.$form.submit(); this.toggleSubmitBtnState(false);
} })
}) .catch(() => {
.fail(() => { flash('Something went wrong on our end.');
new Flash('Something went wrong on our end.'); this.toggleSubmitBtnState(false);
}) });
.always(() => {
this.toggleSubmitBtnState(false);
});
} }
} }
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */ /* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
import _ from 'underscore'; import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import Flash from './flash'; import Flash from './flash';
export default { export default {
...@@ -22,15 +23,9 @@ export default { ...@@ -22,15 +23,9 @@ export default {
}, },
submit() { submit() {
const _this = this; axios[this.form.attr('method')](this.form.attr('action'), this.getFormDataAsObject())
const xhr = $.ajax({ .then(() => window.location.reload())
url: this.form.attr('action'), .catch(() => this.onFormSubmitFailure());
method: this.form.attr('method'),
dataType: 'JSON',
data: this.getFormDataAsObject()
});
xhr.done(() => window.location.reload());
xhr.fail(() => this.onFormSubmitFailure());
}, },
onFormSubmitFailure() { onFormSubmitFailure() {
......
import axios from './lib/utils/axios_utils';
import flash from './flash';
import { __ } from './locale';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar'; import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
...@@ -20,23 +23,24 @@ export default class IssuableIndex { ...@@ -20,23 +23,24 @@ export default class IssuableIndex {
} }
static resetIncomingEmailToken() { static resetIncomingEmailToken() {
$('.incoming-email-token-reset').on('click', (e) => { const $resetToken = $('.incoming-email-token-reset');
$resetToken.on('click', (e) => {
e.preventDefault(); e.preventDefault();
$.ajax({ $resetToken.text('resetting...');
type: 'PUT',
url: $('.incoming-email-token-reset').attr('href'), axios.put($resetToken.attr('href'))
dataType: 'json', .then(({ data }) => {
success(response) { $('#issuable_email').val(data.new_address).focus();
$('#issuable_email').val(response.new_address).focus();
}, $resetToken.text('reset it');
beforeSend() { })
$('.incoming-email-token-reset').text('resetting...'); .catch(() => {
}, flash(__('There was an error when reseting email token.'));
complete() {
$('.incoming-email-token-reset').text('reset it'); $resetToken.text('reset it');
}, });
});
}); });
} }
} }
/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */ /* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */
import Sortable from 'vendor/Sortable'; import Sortable from 'vendor/Sortable';
import Flash from './flash'; import flash from './flash';
import axios from './lib/utils/axios_utils';
export default class LabelManager { export default class LabelManager {
constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) { constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
...@@ -50,11 +51,12 @@ export default class LabelManager { ...@@ -50,11 +51,12 @@ export default class LabelManager {
if (persistState == null) { if (persistState == null) {
persistState = true; persistState = true;
} }
let xhr;
const _this = this; const _this = this;
const url = $label.find('.js-toggle-priority').data('url'); const url = $label.find('.js-toggle-priority').data('url');
let $target = this.prioritizedLabels; let $target = this.prioritizedLabels;
let $from = this.otherLabels; let $from = this.otherLabels;
const rollbackLabelPosition = this.rollbackLabelPosition.bind(this, $label, action);
if (action === 'remove') { if (action === 'remove') {
$target = this.otherLabels; $target = this.otherLabels;
$from = this.prioritizedLabels; $from = this.prioritizedLabels;
...@@ -71,40 +73,34 @@ export default class LabelManager { ...@@ -71,40 +73,34 @@ export default class LabelManager {
return; return;
} }
if (action === 'remove') { if (action === 'remove') {
xhr = $.ajax({ axios.delete(url)
url, .catch(rollbackLabelPosition);
type: 'DELETE'
});
// Restore empty message // Restore empty message
if (!$from.find('li').length) { if (!$from.find('li').length) {
$from.find('.empty-message').removeClass('hidden'); $from.find('.empty-message').removeClass('hidden');
} }
} else { } else {
xhr = this.savePrioritySort($label, action); this.savePrioritySort($label, action)
.catch(rollbackLabelPosition);
} }
return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
} }
onPrioritySortUpdate() { onPrioritySortUpdate() {
const xhr = this.savePrioritySort(); this.savePrioritySort()
return xhr.fail(function() { .catch(() => flash(this.errorMessage));
return new Flash(this.errorMessage, 'alert');
});
} }
savePrioritySort() { savePrioritySort() {
return $.post({ return axios.post(this.prioritizedLabels.data('url'), {
url: this.prioritizedLabels.data('url'), label_ids: this.getSortedLabelsIds(),
data: {
label_ids: this.getSortedLabelsIds()
}
}); });
} }
rollbackLabelPosition($label, originalAction) { rollbackLabelPosition($label, originalAction) {
const action = originalAction === 'remove' ? 'add' : 'remove'; const action = originalAction === 'remove' ? 'add' : 'remove';
this.toggleLabelPriority($label, action, false); this.toggleLabelPriority($label, action, false);
return new Flash(this.errorMessage, 'alert'); flash(this.errorMessage);
} }
getSortedLabelsIds() { getSortedLabelsIds() {
......
/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */ /* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import createFlash from './flash';
import FilesCommentButton from './files_comment_button'; import FilesCommentButton from './files_comment_button';
import imageDiffHelper from './image_diff/helpers/index'; import imageDiffHelper from './image_diff/helpers/index';
import syntaxHighlight from './syntax_highlight'; import syntaxHighlight from './syntax_highlight';
...@@ -60,30 +63,31 @@ export default class SingleFileDiff { ...@@ -60,30 +63,31 @@ export default class SingleFileDiff {
getContentHTML(cb) { getContentHTML(cb) {
this.collapsedContent.hide(); this.collapsedContent.hide();
this.loadingContent.show(); this.loadingContent.show();
$.get(this.diffForPath, (function(_this) {
return function(data) { axios.get(this.diffForPath)
_this.loadingContent.hide(); .then(({ data }) => {
this.loadingContent.hide();
if (data.html) { if (data.html) {
_this.content = $(data.html); this.content = $(data.html);
syntaxHighlight(_this.content); syntaxHighlight(this.content);
} else { } else {
_this.hasError = true; this.hasError = true;
_this.content = $(ERROR_HTML); this.content = $(ERROR_HTML);
} }
_this.collapsedContent.after(_this.content); this.collapsedContent.after(this.content);
if (typeof gl.diffNotesCompileComponents !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
} }
const $file = $(_this.file); const $file = $(this.file);
FilesCommentButton.init($file); FilesCommentButton.init($file);
const canCreateNote = $file.closest('.files').is('[data-can-create-note]'); const canCreateNote = $file.closest('.files').is('[data-can-create-note]');
imageDiffHelper.initImageDiff($file[0], canCreateNote); imageDiffHelper.initImageDiff($file[0], canCreateNote);
if (cb) cb(); if (cb) cb();
}; })
})(this)); .catch(createFlash(__('An error occurred while retrieving diff')));
} }
} }
...@@ -785,10 +785,6 @@ ...@@ -785,10 +785,6 @@
} }
.mr-widget-code-quality { .mr-widget-code-quality {
.ci-status-icon-warning svg {
fill: $theme-gray-600;
}
.code-quality-container { .code-quality-container {
border-top: 1px solid $gray-darker; border-top: 1px solid $gray-darker;
padding: $gl-padding-top; padding: $gl-padding-top;
...@@ -796,7 +792,7 @@ ...@@ -796,7 +792,7 @@
margin: $gl-padding -16px -16px; margin: $gl-padding -16px -16px;
.mr-widget-code-quality-info { .mr-widget-code-quality-info {
padding-left: 12px; padding-left: 10px;
} }
.mr-widget-dast-code { .mr-widget-dast-code {
...@@ -805,34 +801,29 @@ ...@@ -805,34 +801,29 @@
.mr-widget-code-quality-list { .mr-widget-code-quality-list {
list-style: none; list-style: none;
padding: 0 12px; padding: 0 2px 0 0;
margin: 0; margin: 0;
line-height: $code_line_height; line-height: $code_line_height;
.mr-widget-code-quality-icon { .mr-widget-code-quality-list-item {
margin-right: 12px; display: flex;
fill: currentColor;
svg {
width: 10px;
height: 10px;
}
} }
.success { .failed .mr-widget-code-quality-icon {
color: $green-500; color: $red-500;
} }
.failed { .success .mr-widget-code-quality-icon {
color: $red-500; color: $green-500;
} }
.neutral { .neutral .mr-widget-code-quality-icon {
color: $gl-gray-light; color: $theme-gray-700;
} }
.modal-body { .mr-widget-code-quality-icon {
color: $gl-text-color; margin: -5px 4px 0 0;
fill: currentColor;
} }
} }
} }
......
...@@ -645,12 +645,12 @@ class MergeRequest < ActiveRecord::Base ...@@ -645,12 +645,12 @@ class MergeRequest < ActiveRecord::Base
can_be_merged? && !should_be_rebased? can_be_merged? && !should_be_rebased?
end end
def mergeable_state?(skip_ci_check: false) def mergeable_state?(skip_ci_check: false, skip_discussions_check: false)
return false unless open? return false unless open?
return false if work_in_progress? return false if work_in_progress?
return false if broken? return false if broken?
return false unless skip_ci_check || mergeable_ci_state? return false unless skip_ci_check || mergeable_ci_state?
return false unless mergeable_discussions_state? return false unless skip_discussions_check || mergeable_discussions_state?
true true
end end
......
...@@ -67,7 +67,18 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -67,7 +67,18 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :merge_ongoing?, as: :merge_ongoing expose :merge_ongoing?, as: :merge_ongoing
expose :work_in_progress?, as: :work_in_progress expose :work_in_progress?, as: :work_in_progress
expose :source_branch_exists?, as: :source_branch_exists expose :source_branch_exists?, as: :source_branch_exists
expose :mergeable_discussions_state?, as: :mergeable_discussions_state
expose :mergeable_discussions_state?, as: :mergeable_discussions_state do |merge_request|
# This avoids calling MergeRequest#mergeable_discussions_state without
# considering the state of the MR first. If a MR isn't mergeable, we can
# safely short-circuit it.
if merge_request.mergeable_state?(skip_ci_check: true, skip_discussions_check: true)
merge_request.mergeable_discussions_state?
else
false
end
end
expose :branch_missing?, as: :branch_missing expose :branch_missing?, as: :branch_missing
expose :commits_count expose :commits_count
expose :cannot_be_merged?, as: :has_conflicts expose :cannot_be_merged?, as: :has_conflicts
......
%li.header-new.dropdown %li.header-new.dropdown
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do
= sprite_icon('plus-square', size: 16) = sprite_icon('plus-square', size: 16)
= sprite_icon('angle-down', css_class: 'caret-down') = sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu-nav.dropdown-menu-align-right .dropdown-menu-nav.dropdown-menu-align-right
......
...@@ -32,5 +32,5 @@ ...@@ -32,5 +32,5 @@
= icon("pencil") = icon("pencil")
- if can?(current_user, :admin_project, @project) - if can?(current_user, :admin_project, @project)
= link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do = link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip prepend-left-10 #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do
= icon("trash-o") = icon("trash-o")
---
title: Update UI for merge widget reports
merge_request:
author:
type: changed
---
title: Include epics from subgroups on Epic index page
merge_request:
author:
type: fixed
---
title: Adds spacing between edit and delete tag btn in tag list
merge_request: 16757
author: Jacopo Beschi @jacopo-beschi
type: fixed
---
title: Stop checking if discussions are in a mergeable state if the MR isn't
merge_request:
author:
type: performance
if defined?(GrapeRouteHelpers) if defined?(GrapeRouteHelpers)
module GrapeRouteHelpers module GrapeRouteHelpers
module AllRoutes
# Bringing in PR https://github.com/reprah/grape-route-helpers/pull/21 due to abandonment.
#
# Without the following fix, when two helper methods are the same, but have different arguments
# (for example: api_v1_cats_owners_path(id: 1) vs api_v1_cats_owners_path(id: 1, owner_id: 2))
# if the helper method with the least number of arguments is defined first (because the route was defined first)
# then it will shadow the longer route.
#
# The fix is to sort descending by amount of arguments
def decorated_routes
@decorated_routes ||= all_routes
.map { |r| DecoratedRoute.new(r) }
.sort_by { |r| -r.dynamic_path_segments.count }
end
end
class DecoratedRoute class DecoratedRoute
# GrapeRouteHelpers gem tries to parse the versions # GrapeRouteHelpers gem tries to parse the versions
# from a string, not supporting Grape `version` array definition. # from a string, not supporting Grape `version` array definition.
......
...@@ -12,7 +12,7 @@ The [epic issues API](epic_issues.md) allows you to interact with issues associa ...@@ -12,7 +12,7 @@ The [epic issues API](epic_issues.md) allows you to interact with issues associa
## List epics for a group ## List epics for a group
Gets all epics of the requested group. Gets all epics of the requested group and its subgroups.
``` ```
GET /groups/:id/-/epics GET /groups/:id/-/epics
......
...@@ -68,7 +68,7 @@ Example response: ...@@ -68,7 +68,7 @@ Example response:
```json ```json
{ {
"file_name": "app/project.rb", "file_path": "app/project.rb",
"branch": "master" "branch": "master"
} }
``` ```
...@@ -98,7 +98,7 @@ Example response: ...@@ -98,7 +98,7 @@ Example response:
```json ```json
{ {
"file_name": "app/project.rb", "file_path": "app/project.rb",
"branch": "master" "branch": "master"
} }
``` ```
...@@ -134,15 +134,6 @@ DELETE /projects/:id/repository/files/:file_path ...@@ -134,15 +134,6 @@ DELETE /projects/:id/repository/files/:file_path
curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file' curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
``` ```
Example response:
```json
{
"file_name": "app/project.rb",
"branch": "master"
}
```
Parameters: Parameters:
- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb - `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
......
...@@ -9,7 +9,8 @@ milestones. ...@@ -9,7 +9,8 @@ milestones.
## Creating an epic ## Creating an epic
A paginated list of epics is available in each group from where you can create A paginated list of epics is available in each group from where you can create
a new epic. From your group page: a new epic. The list of epics includes also epics from all subgroups of the
selected group. From your group page:
1. Go to **Epics** 1. Go to **Epics**
1. Click the **New epic** button at the top right 1. Click the **New epic** button at the top right
......
...@@ -18,7 +18,7 @@ When you create a new [project](../../index.md), GitLab sets `master` as the def ...@@ -18,7 +18,7 @@ When you create a new [project](../../index.md), GitLab sets `master` as the def
branch for your project. You can choose another branch to be your project's branch for your project. You can choose another branch to be your project's
default under your project's **Settings > General**. default under your project's **Settings > General**.
The default branch is the branched affected by the The default branch is the branch affected by the
[issue closing pattern](../../issues/automatic_issue_closing.md), [issue closing pattern](../../issues/automatic_issue_closing.md),
which means that _an issue will be closed when a merge request is merged to which means that _an issue will be closed when a merge request is merged to
the **default branch**_. the **default branch**_.
......
<script> <script>
<<<<<<< HEAD
=======
import { __ } from '~/locale';
>>>>>>> master
import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import issuesBlock from './mr_widget_report_issues.vue'; import issuesBlock from './mr_widget_report_issues.vue';
...@@ -67,7 +71,7 @@ ...@@ -67,7 +71,7 @@
data() { data() {
return { return {
collapseText: 'Expand', collapseText: __('Expand'),
isCollapsed: true, isCollapsed: true,
isFullReportVisible: false, isFullReportVisible: false,
}; };
...@@ -100,7 +104,7 @@ ...@@ -100,7 +104,7 @@
toggleCollapsed() { toggleCollapsed() {
this.isCollapsed = !this.isCollapsed; this.isCollapsed = !this.isCollapsed;
const text = this.isCollapsed ? 'Expand' : 'Collapse'; const text = this.isCollapsed ? __('Expand') : __('Collapse');
this.collapseText = text; this.collapseText = text;
}, },
openFullReport() { openFullReport() {
...@@ -114,28 +118,40 @@ ...@@ -114,28 +118,40 @@
<div <div
v-if="isLoading" v-if="isLoading"
class="media"> class="media"
<div class="mr-widget-icon"> >
<div
class="mr-widget-icon"
>
<loading-icon /> <loading-icon />
</div> </div>
<div class="media-body"> <div
class="media-body"
>
{{ loadingText }} {{ loadingText }}
</div> </div>
</div> </div>
<div <div
v-else-if="isSuccess" v-else-if="isSuccess"
class="media"> class="media"
<status-icon :status="statusIconName" /> >
<status-icon
:status="statusIconName"
/>
<div class="media-body space-children"> <div
<span class="js-code-text"> class="media-body space-children"
>
<span
class="js-code-text"
>
{{ successText }} {{ successText }}
</span> </span>
<button <button
type="button" type="button"
class="btn-link btn-blank" class="btn pull-right btn-sm"
v-if="hasIssues" v-if="hasIssues"
@click="toggleCollapsed" @click="toggleCollapsed"
> >
......
<script> <script>
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { spriteIcon } from '~/lib/utils/common_utils'; import icon from '~/vue_shared/components/icon.vue';
import modal from './mr_widget_dast_modal.vue'; import modal from './mr_widget_dast_modal.vue';
const modalDefaultData = { const modalDefaultData = {
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
name: 'MrWidgetReportIssues', name: 'MrWidgetReportIssues',
components: { components: {
modal, modal,
icon,
}, },
props: { props: {
issues: { issues: {
...@@ -41,15 +42,18 @@ ...@@ -41,15 +42,18 @@
return modalDefaultData; return modalDefaultData;
}, },
computed: { computed: {
icon() {
return this.isStatusSuccess ? spriteIcon('plus') : this.cutIcon;
},
cutIcon() {
return spriteIcon('cut');
},
fixedLabel() { fixedLabel() {
return s__('ciReport|Fixed:'); return s__('ciReport|Fixed:');
}, },
iconName() {
if (this.isStatusFailed) {
return 'status_failed_borderless';
} else if (this.isStatusSuccess) {
return 'status_success_borderless';
}
return 'status_created_borderless';
},
isStatusFailed() { isStatusFailed() {
return this.status === 'failed'; return this.status === 'failed';
}, },
...@@ -114,15 +118,15 @@ ...@@ -114,15 +118,15 @@
success: isStatusSuccess, success: isStatusSuccess,
neutral: isStatusNeutral neutral: isStatusNeutral
}" }"
class="mr-widget-code-quality-list-item"
v-for="(issue, index) in issues" v-for="(issue, index) in issues"
:key="index" :key="index"
> >
<icon
<span
class="mr-widget-code-quality-icon" class="mr-widget-code-quality-icon"
v-html="icon" :name="iconName"
> :size="32"
</span> />
<template v-if="isStatusSuccess && isTypeQuality">{{ fixedLabel }}</template> <template v-if="isStatusSuccess && isTypeQuality">{{ fixedLabel }}</template>
<template v-if="shouldRenderPriority(issue)">{{ issue.priority }}:</template> <template v-if="shouldRenderPriority(issue)">{{ issue.priority }}:</template>
......
...@@ -10,7 +10,6 @@ class EpicsFinder < IssuableFinder ...@@ -10,7 +10,6 @@ class EpicsFinder < IssuableFinder
items = by_created_at(items) items = by_created_at(items)
items = by_search(items) items = by_search(items)
items = by_author(items) items = by_author(items)
items = by_iids(items)
sort(items) sort(items)
end end
...@@ -37,6 +36,16 @@ class EpicsFinder < IssuableFinder ...@@ -37,6 +36,16 @@ class EpicsFinder < IssuableFinder
end end
def init_collection def init_collection
group.epics groups = groups_user_can_read_epics(group.self_and_descendants)
Epic.where(group: groups)
end
private
def groups_user_can_read_epics(groups)
DeclarativePolicy.user_scope do
groups.select { |g| Ability.allowed?(current_user, :read_epic, g) }
end
end end
end end
...@@ -7,8 +7,7 @@ ...@@ -7,8 +7,7 @@
= link_to epic.title, epic_path(epic) = link_to epic.title, epic_path(epic)
.issuable-info .issuable-info
%span.issuable-reference %span.issuable-reference
-# TODO: Use to_reference = epic.to_reference(@group)
= "&#{epic.iid}"
%span.issuable-authored.hidden-xs %span.issuable-authored.hidden-xs
&middot; &middot;
opened #{time_ago_with_tooltip(epic.created_at, placement: 'bottom')} opened #{time_ago_with_tooltip(epic.created_at, placement: 'bottom')}
......
...@@ -40,7 +40,7 @@ module API ...@@ -40,7 +40,7 @@ module API
success Entities::Epic success Entities::Epic
end end
get ':id/-/epics' do get ':id/-/epics' do
present user_group.epics, with: Entities::Epic present EpicsFinder.new(current_user, group_id: user_group.id).execute, with: Entities::Epic
end end
desc 'Get details of an epic' do desc 'Get details of an epic' do
......
...@@ -27,6 +27,7 @@ module QA ...@@ -27,6 +27,7 @@ module QA
module Resource module Resource
autoload :Sandbox, 'qa/factory/resource/sandbox' autoload :Sandbox, 'qa/factory/resource/sandbox'
autoload :Group, 'qa/factory/resource/group' autoload :Group, 'qa/factory/resource/group'
autoload :Issue, 'qa/factory/resource/issue'
autoload :Project, 'qa/factory/resource/project' autoload :Project, 'qa/factory/resource/project'
autoload :MergeRequest, 'qa/factory/resource/merge_request' autoload :MergeRequest, 'qa/factory/resource/merge_request'
autoload :DeployKey, 'qa/factory/resource/deploy_key' autoload :DeployKey, 'qa/factory/resource/deploy_key'
...@@ -130,6 +131,12 @@ module QA ...@@ -130,6 +131,12 @@ module QA
autoload :SecretVariables, 'qa/page/project/settings/secret_variables' autoload :SecretVariables, 'qa/page/project/settings/secret_variables'
autoload :Runners, 'qa/page/project/settings/runners' autoload :Runners, 'qa/page/project/settings/runners'
end end
module Issue
autoload :New, 'qa/page/project/issue/new'
autoload :Show, 'qa/page/project/issue/show'
autoload :Index, 'qa/page/project/issue/index'
end
end end
module Profile module Profile
......
require 'securerandom'
module QA
module Factory
module Resource
class Issue < Factory::Base
attr_writer :title, :description, :project
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-for-issues'
project.description = 'project for adding issues'
end
product :title do
Page::Project::Issue::Show.act { issue_title }
end
def fabricate!
project.visit!
Page::Project::Show.act do
go_to_new_issue
end
Page::Project::Issue::New.perform do |page|
page.add_title(@title)
page.add_description(@description)
page.create_new_issue
end
end
end
end
end
end
...@@ -42,6 +42,23 @@ module QA ...@@ -42,6 +42,23 @@ module QA
page.within(selector) { yield } if block_given? page.within(selector) { yield } if block_given?
end end
# Returns true if successfully GETs the given URL
# Useful because `page.status_code` is unsupported by our driver, and
# we don't have access to the `response` to use `have_http_status`.
def asset_exists?(url)
page.execute_script <<~JS
xhr = new XMLHttpRequest();
xhr.open('GET', '#{url}', true);
xhr.send();
JS
return false unless wait(time: 0.5, max: 60, reload: false) do
page.evaluate_script('xhr.readyState == XMLHttpRequest.DONE')
end
page.evaluate_script('xhr.status') == 200
end
def find_element(name) def find_element(name)
find(element_selector_css(name)) find(element_selector_css(name))
end end
...@@ -80,6 +97,21 @@ module QA ...@@ -80,6 +97,21 @@ module QA
views.map(&:errors).flatten views.map(&:errors).flatten
end end
# Not tested and not expected to work with multiple dropzones
# instantiated on one page because there is no distinguishing
# attribute per dropzone file field.
def attach_file_to_dropzone(attachment, dropzone_form_container)
filename = File.basename(attachment)
field_style = { visibility: 'visible', height: '', width: '' }
attach_file(attachment, class: 'dz-hidden-input', make_visible: field_style)
# Wait for link to be appended to dropzone text
wait(reload: false) do
find("#{dropzone_form_container} textarea").value.match(filename)
end
end
class DSL class DSL
attr_reader :views attr_reader :views
......
...@@ -2,12 +2,16 @@ module QA ...@@ -2,12 +2,16 @@ module QA
module Page module Page
module Group module Group
class Show < Page::Base class Show < Page::Base
## view 'app/views/groups/show.html.haml' do
# TODO, define all selectors required by this page object element :new_project_or_subgroup_dropdown, '.new-project-subgroup'
# element :new_project_or_subgroup_dropdown_toggle, '.dropdown-toggle'
# See gitlab-org/gitlab-qa#154 element :new_project_option, /%li.*data:.*value: "new-project"/
# element :new_project_button, /%input.*data:.*action: "new-project"/
view 'app/views/groups/show.html.haml' element :new_subgroup_option, /%li.*data:.*value: "new-subgroup"/
# data-value and data-action get modified by JS for subgroup
element :new_subgroup_button, /%input.*\.js-new-group-child/
end
def go_to_subgroup(name) def go_to_subgroup(name)
click_link name click_link name
......
...@@ -7,6 +7,8 @@ module QA ...@@ -7,6 +7,8 @@ module QA
element :settings_link, 'link_to edit_project_path' element :settings_link, 'link_to edit_project_path'
element :repository_link, "title: 'Repository'" element :repository_link, "title: 'Repository'"
element :pipelines_settings_link, "title: 'CI / CD'" element :pipelines_settings_link, "title: 'CI / CD'"
element :issues_link, %r{link_to.*shortcuts-issues}
element :issues_link_text, "Issues"
element :top_level_items, '.sidebar-top-level-items' element :top_level_items, '.sidebar-top-level-items'
element :activity_link, "title: 'Activity'" element :activity_link, "title: 'Activity'"
end end
...@@ -43,6 +45,12 @@ module QA ...@@ -43,6 +45,12 @@ module QA
end end
end end
def click_issues
within_sidebar do
click_link('Issues')
end
end
private private
def hover_settings def hover_settings
......
module QA
module Page
module Project
module Issue
class Index < Page::Base
view 'app/views/projects/issues/_issue.html.haml' do
element :issue_link, 'link_to issue.title'
end
def go_to_issue(title)
click_link(title)
end
end
end
end
end
end
module QA
module Page
module Project
module Issue
class New < Page::Base
view 'app/views/shared/issuable/_form.html.haml' do
element :submit_issue_button, 'form.submit "Submit'
end
view 'app/views/shared/issuable/form/_title.html.haml' do
element :issue_title_textbox, 'form.text_field :title'
end
view 'app/views/shared/form_elements/_description.html.haml' do
element :issue_description_textarea, "render 'projects/zen', f: form, attr: :description"
end
def add_title(title)
fill_in 'issue_title', with: title
end
def add_description(description)
fill_in 'issue_description', with: description
end
def create_new_issue
click_on 'Submit issue'
end
end
end
end
end
end
module QA
module Page
module Project
module Issue
class Show < Page::Base
view 'app/views/projects/issues/show.html.haml' do
element :issue_details, '.issue-details'
element :title, '.title'
end
view 'app/views/shared/notes/_form.html.haml' do
element :new_note_form, 'new-note'
element :new_note_form, 'attr: :note'
end
view 'app/views/shared/notes/_comment_button.html.haml' do
element :comment_button, '%strong Comment'
end
def issue_title
find('.issue-details .title').text
end
# Adds a comment to an issue
# attachment option should be an absolute path
def comment(text, attachment:)
fill_in(with: text, name: 'note[note]')
attach_file_to_dropzone(attachment, '.new-note') if attachment
click_on 'Comment'
end
end
end
end
end
end
...@@ -17,6 +17,11 @@ module QA ...@@ -17,6 +17,11 @@ module QA
element :project_name element :project_name
end end
view 'app/views/layouts/header/_new_dropdown.haml' do
element :new_menu_toggle
element :new_issue_link, "link_to 'New issue', new_project_issue_path(@project)"
end
def choose_repository_clone_http def choose_repository_clone_http
wait(reload: false) do wait(reload: false) do
click_element :clone_dropdown click_element :clone_dropdown
...@@ -46,6 +51,12 @@ module QA ...@@ -46,6 +51,12 @@ module QA
sleep 5 sleep 5
refresh refresh
end end
def go_to_new_issue
click_element :new_menu_toggle
click_link 'New issue'
end
end end
end end
end end
......
module QA
feature 'GitLab Geo attachment replication', :geo do
let(:file_to_attach) { File.absolute_path(File.join('spec', 'fixtures', 'banana_sample.gif')) }
scenario 'user uploads attachment to the primary node' do
Runtime::Browser.visit(:geo_primary, QA::Page::Main::Login) do
Page::Main::Login.act { sign_in_using_credentials }
project = Factory::Resource::Project.fabricate! do |project|
project.name = 'project-for-issues'
project.description = 'project for adding issues'
end
issue = Factory::Resource::Issue.fabricate! do |issue|
issue.title = 'My geo issue'
issue.project = project
end
Page::Project::Issue::Show.perform do |show|
show.comment('See attached banana for scale', attachment: file_to_attach)
end
Runtime::Browser.visit(:geo_secondary, QA::Page::Main::Login) do |session|
Page::Main::OAuth.act do
authorize! if needs_authorization?
end
expect(page).to have_content 'You are on a secondary (read-only) Geo node'
Page::Menu::Main.perform do |menu|
menu.go_to_projects
end
Page::Dashboard::Projects.perform do |dashboard|
dashboard.go_to_project(project.name)
end
Page::Menu::Side.act { click_issues }
Page::Project::Issue::Index.perform do |index|
index.go_to_issue(issue.title)
end
image_url = find('a[href$="banana_sample.gif"]')[:href]
Page::Project::Issue::Show.perform do |show|
# Wait for attachment replication
found = show.wait(reload: false) do
show.asset_exists?(image_url)
end
expect(found).to be_truthy
end
end
end
end
end
end
module QA module QA
feature 'GitLab Geo replication', :geo do feature 'GitLab Geo repository replication', :geo do
scenario 'users pushes code to the primary node' do scenario 'users pushes code to the primary node' do
Runtime::Browser.visit(:geo_primary, QA::Page::Main::Login) do Runtime::Browser.visit(:geo_primary, QA::Page::Main::Login) do
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.act { sign_in_using_credentials }
......
...@@ -35,7 +35,6 @@ tasks = [ ...@@ -35,7 +35,6 @@ tasks = [
%w[bundle exec rubocop --parallel], %w[bundle exec rubocop --parallel],
%w[bundle exec rake gettext:lint], %w[bundle exec rake gettext:lint],
%w[bundle exec rake lint:static_verification], %w[bundle exec rake lint:static_verification],
%w[scripts/lint-changelog-yaml],
%w[scripts/lint-conflicts.sh], %w[scripts/lint-conflicts.sh],
%w[scripts/lint-rugged] %w[scripts/lint-rugged]
] ]
......
...@@ -44,7 +44,7 @@ describe EpicsFinder do ...@@ -44,7 +44,7 @@ describe EpicsFinder do
end end
end end
context 'wtih correct params' do context 'with correct params' do
before do before do
group.add_developer(search_user) group.add_developer(search_user)
end end
...@@ -79,9 +79,14 @@ describe EpicsFinder do ...@@ -79,9 +79,14 @@ describe EpicsFinder do
end end
end end
context 'by iids' do context 'when subgroups are supported', :nested_groups do
it 'returns all epics by the given iids' do let(:subgroup) { create(:group, :private, parent: group) }
expect(epics(iids: [epic1.iid, epic3.iid])).to contain_exactly(epic1, epic3) let(:subgroup2) { create(:group, :private, parent: subgroup) }
let!(:subepic1) { create(:epic, group: subgroup) }
let!(:subepic2) { create(:epic, group: subgroup2) }
it 'returns all epics that belong to the given group and its subgroups' do
expect(epics).to contain_exactly(epic1, epic2, epic3, subepic1, subepic2)
end end
end end
end end
......
...@@ -112,13 +112,6 @@ feature 'Expand and collapse diffs', :js do ...@@ -112,13 +112,6 @@ feature 'Expand and collapse diffs', :js do
wait_for_requests wait_for_requests
end end
it 'makes a request to get the content' do
ajax_uris = evaluate_script('ajaxUris')
expect(ajax_uris).not_to be_empty
expect(ajax_uris.first).to include('large_diff.md')
end
it 'shows the diff content' do it 'shows the diff content' do
expect(large_diff).to have_selector('.code') expect(large_diff).to have_selector('.code')
expect(large_diff).not_to have_selector('.nothing-here-block') expect(large_diff).not_to have_selector('.nothing-here-block')
......
require 'spec_helper'
require_relative '../../config/initializers/grape_route_helpers_fix'
describe 'route shadowing' do
include GrapeRouteHelpers::NamedRouteMatcher
it 'does not occur' do
path = api_v4_projects_merge_requests_path(id: 1)
expect(path).to eq('/api/v4/projects/1/merge_requests')
path = api_v4_projects_merge_requests_path(id: 1, merge_request_iid: 3)
expect(path).to eq('/api/v4/projects/1/merge_requests/3')
end
end
...@@ -18,54 +18,67 @@ describe('CreateItemDropdown', () => { ...@@ -18,54 +18,67 @@ describe('CreateItemDropdown', () => {
preloadFixtures('static/create_item_dropdown.html.raw'); preloadFixtures('static/create_item_dropdown.html.raw');
let $wrapperEl; let $wrapperEl;
let createItemDropdown;
function createItemAndClearInput(text) {
// Filter for the new item
$wrapperEl.find('.dropdown-input-field')
.val(text)
.trigger('input');
// Create the new item
const $createButton = $wrapperEl.find('.js-dropdown-create-new-item');
$createButton.click();
// Clear out the filter
$wrapperEl.find('.dropdown-input-field')
.val('')
.trigger('input');
}
beforeEach(() => { beforeEach(() => {
loadFixtures('static/create_item_dropdown.html.raw'); loadFixtures('static/create_item_dropdown.html.raw');
$wrapperEl = $('.js-create-item-dropdown-fixture-root'); $wrapperEl = $('.js-create-item-dropdown-fixture-root');
// eslint-disable-next-line no-new
new CreateItemDropdown({
$dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
defaultToggleLabel: 'All variables',
fieldName: 'variable[environment]',
getData: (term, callback) => {
callback(DROPDOWN_ITEM_DATA);
},
});
}); });
afterEach(() => { afterEach(() => {
$wrapperEl.remove(); $wrapperEl.remove();
}); });
it('should have a dropdown item for each piece of data', () => { describe('items', () => {
// Get the data in the dropdown beforeEach(() => {
$('.js-dropdown-menu-toggle').click(); createItemDropdown = new CreateItemDropdown({
$dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
defaultToggleLabel: 'All variables',
fieldName: 'variable[environment]',
getData: (term, callback) => {
callback(DROPDOWN_ITEM_DATA);
},
});
});
it('should have a dropdown item for each piece of data', () => {
// Get the data in the dropdown
$('.js-dropdown-menu-toggle').click();
const $itemEls = $wrapperEl.find('.js-dropdown-content a'); const $itemEls = $wrapperEl.find('.js-dropdown-content a');
expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length); expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length);
});
}); });
describe('created items', () => { describe('created items', () => {
const NEW_ITEM_TEXT = 'foobarbaz'; const NEW_ITEM_TEXT = 'foobarbaz';
function createItemAndClearInput(text) {
// Filter for the new item
$wrapperEl.find('.dropdown-input-field')
.val(text)
.trigger('input');
// Create the new item
const $createButton = $wrapperEl.find('.js-dropdown-create-new-item');
$createButton.click();
// Clear out the filter
$wrapperEl.find('.dropdown-input-field')
.val('')
.trigger('input');
}
beforeEach(() => { beforeEach(() => {
createItemDropdown = new CreateItemDropdown({
$dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
defaultToggleLabel: 'All variables',
fieldName: 'variable[environment]',
getData: (term, callback) => {
callback(DROPDOWN_ITEM_DATA);
},
});
// Open the dropdown // Open the dropdown
$('.js-dropdown-menu-toggle').click(); $('.js-dropdown-menu-toggle').click();
...@@ -103,4 +116,68 @@ describe('CreateItemDropdown', () => { ...@@ -103,4 +116,68 @@ describe('CreateItemDropdown', () => {
expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length); expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length);
}); });
}); });
describe('clearDropdown()', () => {
beforeEach(() => {
createItemDropdown = new CreateItemDropdown({
$dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
defaultToggleLabel: 'All variables',
fieldName: 'variable[environment]',
getData: (term, callback) => {
callback(DROPDOWN_ITEM_DATA);
},
});
});
it('should clear all data and filter input', () => {
const filterInput = $wrapperEl.find('.dropdown-input-field');
// Get the data in the dropdown
$('.js-dropdown-menu-toggle').click();
// Filter for an item
filterInput
.val('one')
.trigger('input');
const $itemElsAfterFilter = $wrapperEl.find('.js-dropdown-content a');
expect($itemElsAfterFilter.length).toEqual(1);
createItemDropdown.clearDropdown();
const $itemElsAfterClear = $wrapperEl.find('.js-dropdown-content a');
expect($itemElsAfterClear.length).toEqual(0);
expect(filterInput.val()).toEqual('');
});
});
describe('createNewItemFromValue option', () => {
beforeEach(() => {
createItemDropdown = new CreateItemDropdown({
$dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
defaultToggleLabel: 'All variables',
fieldName: 'variable[environment]',
getData: (term, callback) => {
callback(DROPDOWN_ITEM_DATA);
},
createNewItemFromValue: newValue => ({
title: `${newValue}-title`,
id: `${newValue}-id`,
text: `${newValue}-text`,
}),
});
});
it('all items go through createNewItemFromValue', () => {
// Get the data in the dropdown
$('.js-dropdown-menu-toggle').click();
createItemAndClearInput('new-item');
const $itemEls = $wrapperEl.find('.js-dropdown-content a');
expect($itemEls.length).toEqual(1 + DROPDOWN_ITEM_DATA.length);
expect($($itemEls[3]).text()).toEqual('new-item-text');
expect($wrapperEl.find('.dropdown-toggle-text').text()).toEqual('new-item-title');
});
});
}); });
import MockAdaptor from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import IntegrationSettingsForm from '~/integrations/integration_settings_form'; import IntegrationSettingsForm from '~/integrations/integration_settings_form';
describe('IntegrationSettingsForm', () => { describe('IntegrationSettingsForm', () => {
...@@ -109,91 +111,117 @@ describe('IntegrationSettingsForm', () => { ...@@ -109,91 +111,117 @@ describe('IntegrationSettingsForm', () => {
describe('testSettings', () => { describe('testSettings', () => {
let integrationSettingsForm; let integrationSettingsForm;
let formData; let formData;
let mock;
beforeEach(() => { beforeEach(() => {
mock = new MockAdaptor(axios);
spyOn(axios, 'put').and.callThrough();
integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
formData = integrationSettingsForm.$form.serialize(); formData = integrationSettingsForm.$form.serialize();
}); });
it('should make an ajax request with provided `formData`', () => { afterEach(() => {
const deferred = $.Deferred(); mock.restore();
spyOn($, 'ajax').and.returnValue(deferred.promise()); });
integrationSettingsForm.testSettings(formData); it('should make an ajax request with provided `formData`', (done) => {
integrationSettingsForm.testSettings(formData)
.then(() => {
expect(axios.put).toHaveBeenCalledWith(integrationSettingsForm.testEndPoint, formData);
expect($.ajax).toHaveBeenCalledWith({ done();
type: 'PUT', })
url: integrationSettingsForm.testEndPoint, .catch(done.fail);
data: formData,
});
}); });
it('should show error Flash with `Save anyway` action if ajax request responds with error in test', () => { it('should show error Flash with `Save anyway` action if ajax request responds with error in test', (done) => {
const errorMessage = 'Test failed.'; const errorMessage = 'Test failed.';
const deferred = $.Deferred(); mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
spyOn($, 'ajax').and.returnValue(deferred.promise()); error: true,
message: errorMessage,
integrationSettingsForm.testSettings(formData); service_response: 'some error',
});
deferred.resolve({ error: true, message: errorMessage, service_response: 'some error' }); integrationSettingsForm.testSettings(formData)
.then(() => {
const $flashContainer = $('.flash-container');
expect($flashContainer.find('.flash-text').text().trim()).toEqual('Test failed. some error');
expect($flashContainer.find('.flash-action')).toBeDefined();
expect($flashContainer.find('.flash-action').text().trim()).toEqual('Save anyway');
const $flashContainer = $('.flash-container'); done();
expect($flashContainer.find('.flash-text').text().trim()).toEqual('Test failed. some error'); })
expect($flashContainer.find('.flash-action')).toBeDefined(); .catch(done.fail);
expect($flashContainer.find('.flash-action').text().trim()).toEqual('Save anyway');
}); });
it('should submit form if ajax request responds without any error in test', () => { it('should submit form if ajax request responds without any error in test', (done) => {
const deferred = $.Deferred(); spyOn(integrationSettingsForm.$form, 'submit');
spyOn($, 'ajax').and.returnValue(deferred.promise());
integrationSettingsForm.testSettings(formData); mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
error: false,
});
spyOn(integrationSettingsForm.$form, 'submit'); integrationSettingsForm.testSettings(formData)
deferred.resolve({ error: false }); .then(() => {
expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
expect(integrationSettingsForm.$form.submit).toHaveBeenCalled(); done();
})
.catch(done.fail);
}); });
it('should submit form when clicked on `Save anyway` action of error Flash', () => { it('should submit form when clicked on `Save anyway` action of error Flash', (done) => {
const errorMessage = 'Test failed.'; spyOn(integrationSettingsForm.$form, 'submit');
const deferred = $.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.promise());
integrationSettingsForm.testSettings(formData); const errorMessage = 'Test failed.';
mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
error: true,
message: errorMessage,
});
deferred.resolve({ error: true, message: errorMessage }); integrationSettingsForm.testSettings(formData)
.then(() => {
const $flashAction = $('.flash-container .flash-action');
expect($flashAction).toBeDefined();
const $flashAction = $('.flash-container .flash-action'); $flashAction.get(0).click();
expect($flashAction).toBeDefined(); })
.then(() => {
expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
spyOn(integrationSettingsForm.$form, 'submit'); done();
$flashAction.get(0).click(); })
expect(integrationSettingsForm.$form.submit).toHaveBeenCalled(); .catch(done.fail);
}); });
it('should show error Flash if ajax request failed', () => { it('should show error Flash if ajax request failed', (done) => {
const errorMessage = 'Something went wrong on our end.'; const errorMessage = 'Something went wrong on our end.';
const deferred = $.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.promise());
integrationSettingsForm.testSettings(formData); mock.onPut(integrationSettingsForm.testEndPoint).networkError();
deferred.reject(); integrationSettingsForm.testSettings(formData)
.then(() => {
expect($('.flash-container .flash-text').text().trim()).toEqual(errorMessage);
expect($('.flash-container .flash-text').text().trim()).toEqual(errorMessage); done();
})
.catch(done.fail);
}); });
it('should always call `toggleSubmitBtnState` with `false` once request is completed', () => { it('should always call `toggleSubmitBtnState` with `false` once request is completed', (done) => {
const deferred = $.Deferred(); mock.onPut(integrationSettingsForm.testEndPoint).networkError();
spyOn($, 'ajax').and.returnValue(deferred.promise());
integrationSettingsForm.testSettings(formData);
spyOn(integrationSettingsForm, 'toggleSubmitBtnState'); spyOn(integrationSettingsForm, 'toggleSubmitBtnState');
deferred.reject();
expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false); integrationSettingsForm.testSettings(formData)
.then(() => {
expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false);
done();
})
.catch(done.fail);
}); });
}); });
}); });
import MockAdaptor from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import IssuableIndex from '~/issuable_index'; import IssuableIndex from '~/issuable_index';
describe('Issuable', () => { describe('Issuable', () => {
...@@ -19,6 +21,8 @@ describe('Issuable', () => { ...@@ -19,6 +21,8 @@ describe('Issuable', () => {
}); });
describe('resetIncomingEmailToken', () => { describe('resetIncomingEmailToken', () => {
let mock;
beforeEach(() => { beforeEach(() => {
const element = document.createElement('a'); const element = document.createElement('a');
element.classList.add('incoming-email-token-reset'); element.classList.add('incoming-email-token-reset');
...@@ -30,14 +34,28 @@ describe('Issuable', () => { ...@@ -30,14 +34,28 @@ describe('Issuable', () => {
document.body.appendChild(input); document.body.appendChild(input);
Issuable = new IssuableIndex('issue_'); Issuable = new IssuableIndex('issue_');
mock = new MockAdaptor(axios);
mock.onPut('foo').reply(200, {
new_address: 'testing123',
});
}); });
it('should send request to reset email token', () => { afterEach(() => {
spyOn(jQuery, 'ajax').and.callThrough(); mock.restore();
});
it('should send request to reset email token', (done) => {
spyOn(axios, 'put').and.callThrough();
document.querySelector('.incoming-email-token-reset').click(); document.querySelector('.incoming-email-token-reset').click();
expect(jQuery.ajax).toHaveBeenCalled(); setTimeout(() => {
expect(jQuery.ajax.calls.argsFor(0)[0].url).toEqual('foo'); expect(axios.put).toHaveBeenCalledWith('foo');
expect($('#issuable_email').val()).toBe('testing123');
done();
});
}); });
}); });
}); });
......
...@@ -1568,6 +1568,10 @@ describe MergeRequest do ...@@ -1568,6 +1568,10 @@ describe MergeRequest do
it 'returns false' do it 'returns false' do
expect(subject.mergeable_state?).to be_falsey expect(subject.mergeable_state?).to be_falsey
end end
it 'returns true when skipping discussions check' do
expect(subject.mergeable_state?(skip_discussions_check: true)).to be(true)
end
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment