Commit f315216d authored by Florie Guibert's avatar Florie Guibert Committed by Phil Hughes

WIP: Close blocked issue warning

- Close issue warning, top of issue page
parent 45183ce3
...@@ -12,6 +12,8 @@ export default class Issue { ...@@ -12,6 +12,8 @@ export default class Issue {
constructor() { constructor() {
if ($('a.btn-close').length) this.initIssueBtnEventListeners(); if ($('a.btn-close').length) this.initIssueBtnEventListeners();
if ($('.js-close-blocked-issue-warning').length) this.initIssueWarningBtnEventListener();
Issue.$btnNewBranch = $('#new-branch'); Issue.$btnNewBranch = $('#new-branch');
Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap'); Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap');
...@@ -89,7 +91,7 @@ export default class Issue { ...@@ -89,7 +91,7 @@ export default class Issue {
return $(document).on( return $(document).on(
'click', 'click',
'.js-issuable-actions a.btn-close, .js-issuable-actions a.btn-reopen', '.js-issuable-actions a.btn-close, .js-issuable-actions a.btn-reopen, a.btn-close-anyway',
e => { e => {
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
...@@ -99,19 +101,30 @@ export default class Issue { ...@@ -99,19 +101,30 @@ export default class Issue {
Issue.submitNoteForm($button.closest('form')); Issue.submitNoteForm($button.closest('form'));
} }
this.disableCloseReopenButton($button); const shouldDisplayBlockedWarning = $button.hasClass('btn-issue-blocked');
const warningBanner = $('.js-close-blocked-issue-warning');
const url = $button.attr('href'); if (shouldDisplayBlockedWarning) {
return axios this.toggleWarningAndCloseButton();
.put(url) } else {
.then(({ data }) => { this.disableCloseReopenButton($button);
const isClosed = $button.hasClass('btn-close');
this.updateTopState(isClosed, data); const url = $button.attr('href');
}) return axios
.catch(() => flash(issueFailMessage)) .put(url)
.then(() => { .then(({ data }) => {
this.disableCloseReopenButton($button, false); const isClosed = $button.is('.btn-close, .btn-close-anyway');
}); this.updateTopState(isClosed, data);
if ($button.hasClass('btn-close-anyway')) {
warningBanner.addClass('hidden');
if (this.closeReopenReportToggle)
$('.js-issuable-close-dropdown').removeClass('hidden');
}
})
.catch(() => flash(issueFailMessage))
.then(() => {
this.disableCloseReopenButton($button, false);
});
}
}, },
); );
} }
...@@ -137,6 +150,23 @@ export default class Issue { ...@@ -137,6 +150,23 @@ export default class Issue {
this.reopenButtons.toggleClass('hidden', !isClosed); this.reopenButtons.toggleClass('hidden', !isClosed);
} }
toggleWarningAndCloseButton() {
const warningBanner = $('.js-close-blocked-issue-warning');
warningBanner.toggleClass('hidden');
$('.btn-close').toggleClass('hidden');
if (this.closeReopenReportToggle) {
$('.js-issuable-close-dropdown').toggleClass('hidden');
}
}
initIssueWarningBtnEventListener() {
return $(document).on('click', '.js-close-blocked-issue-warning button.btn-secondary', e => {
e.preventDefault();
e.stopImmediatePropagation();
this.toggleWarningAndCloseButton();
});
}
static submitNoteForm(form) { static submitNoteForm(form) {
const noteText = form.find('textarea.js-note-text').val(); const noteText = form.find('textarea.js-note-text').val();
if (noteText && noteText.trim().length > 0) { if (noteText && noteText.trim().length > 0) {
......
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
- can_report_spam = @issue.submittable_as_spam_by?(current_user) - can_report_spam = @issue.submittable_as_spam_by?(current_user)
- can_create_issue = show_new_issue_link?(@project) - can_create_issue = show_new_issue_link?(@project)
= render_if_exists "projects/issues/alert_blocked", issue: @issue, current_user: current_user
.detail-page-header .detail-page-header
.detail-page-header-body .detail-page-header-body
.issuable-status-box.status-box.status-box-issue-closed{ class: issue_status_visibility(@issue, status_box: :closed) } .issuable-status-box.status-box.status-box-issue-closed{ class: issue_status_visibility(@issue, status_box: :closed) }
...@@ -50,7 +52,7 @@ ...@@ -50,7 +52,7 @@
%li.divider %li.divider
%li= link_to 'New issue', new_project_issue_path(@project), id: 'new_issue_link' %li= link_to 'New issue', new_project_issue_path(@project), id: 'new_issue_link'
= render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue, can_reopen: can_reopen_issue = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue, can_reopen: can_reopen_issue, warn_before_close: defined?(@issue.blocked?) && @issue.blocked?
- if can_report_spam - if can_report_spam
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-sm-none d-md-block btn btn-grouped btn-spam', title: 'Submit as spam' = link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-sm-none d-md-block btn btn-grouped btn-spam', title: 'Submit as spam'
......
...@@ -2,17 +2,20 @@ ...@@ -2,17 +2,20 @@
- display_issuable_type = issuable_display_type(issuable) - display_issuable_type = issuable_display_type(issuable)
- button_method = issuable_close_reopen_button_method(issuable) - button_method = issuable_close_reopen_button_method(issuable)
- are_close_and_open_buttons_hidden = issuable_button_hidden?(issuable, true) && issuable_button_hidden?(issuable, false) - are_close_and_open_buttons_hidden = issuable_button_hidden?(issuable, true) && issuable_button_hidden?(issuable, false)
- add_blocked_class = false
- if defined? warn_before_close
- add_blocked_class = warn_before_close
- if is_current_user - if is_current_user
- if can_update - if can_update
= link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method, = link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method,
class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}", data: { qa_selector: 'close_issue_button' } class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)} #{(add_blocked_class ? 'btn-issue-blocked' : '')}", title: "Close #{display_issuable_type}", data: { qa_selector: 'close_issue_button' }
- if can_reopen - if can_reopen
= link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method, = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method,
class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}", data: { qa_selector: 'reopen_issue_button' } class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}", data: { qa_selector: 'reopen_issue_button' }
- else - else
- if can_update && !are_close_and_open_buttons_hidden - if can_update && !are_close_and_open_buttons_hidden
= render 'shared/issuable/close_reopen_report_toggle', issuable: issuable = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable, warn_before_close: add_blocked_class
- else - else
= link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), = link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: 'Report abuse' class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: 'Report abuse'
...@@ -5,10 +5,13 @@ ...@@ -5,10 +5,13 @@
- button_class = "#{button_responsive_class} btn btn-grouped js-issuable-close-button js-btn-issue-action issuable-close-button" - button_class = "#{button_responsive_class} btn btn-grouped js-issuable-close-button js-btn-issue-action issuable-close-button"
- toggle_class = "#{button_responsive_class} btn btn-nr dropdown-toggle js-issuable-close-toggle" - toggle_class = "#{button_responsive_class} btn btn-nr dropdown-toggle js-issuable-close-toggle"
- button_method = issuable_close_reopen_button_method(issuable) - button_method = issuable_close_reopen_button_method(issuable)
- add_blocked_class = false
- if defined? warn_before_close
- add_blocked_class = !issuable.closed? && warn_before_close
.float-left.btn-group.prepend-left-10.issuable-close-dropdown.droplab-dropdown.js-issuable-close-dropdown .float-left.btn-group.prepend-left-10.issuable-close-dropdown.droplab-dropdown.js-issuable-close-dropdown
= link_to "#{display_button_action} #{display_issuable_type}", close_reopen_issuable_path(issuable), = link_to "#{display_button_action} #{display_issuable_type}", close_reopen_issuable_path(issuable),
method: button_method, class: "#{button_class} btn-#{button_action}", title: "#{display_button_action} #{display_issuable_type}" method: button_method, class: "#{button_class} btn-#{button_action} #{(add_blocked_class ? 'btn-issue-blocked' : '')}", title: "#{display_button_action} #{display_issuable_type}"
= button_tag type: 'button', class: "#{toggle_class} btn-#{button_action}-color", = button_tag type: 'button', class: "#{toggle_class} btn-#{button_action}-color",
data: { 'dropdown-trigger' => '#issuable-close-menu' }, 'aria-label' => 'Toggle dropdown' do data: { 'dropdown-trigger' => '#issuable-close-menu' }, 'aria-label' => 'Toggle dropdown' do
......
...@@ -11,7 +11,7 @@ module EE ...@@ -11,7 +11,7 @@ module EE
expose :blocked_by_issues do |issue| expose :blocked_by_issues do |issue|
issues = issue.blocked_by_issues(request.current_user) issues = issue.blocked_by_issues(request.current_user)
serializer_options = options.merge(only: [:id, :web_url]) serializer_options = options.merge(only: [:iid, :web_url])
::IssueEntity.represent(issues, serializer_options) ::IssueEntity.represent(issues, serializer_options)
end end
......
- blocked_by_issues = @issue.blocked_by_issues(current_user)
- blocked_by_issues_links = blocked_by_issues.map { |blocking_issue| link_to "\##{blocking_issue.iid}", project_issue_path(blocking_issue.project, blocking_issue), class: 'gl-link' }.join(', ').html_safe
- if @issue.blocked? && @issue.blocked_by_issues(current_user).length > 0
.hidden.js-close-blocked-issue-warning.gl-alert.gl-alert-warning.prepend-top-16{ role: 'alert' }
= sprite_icon('warning', size: 16, css_class: 'gl-icon gl-alert-icon')
%h4.gl-alert-title
= _('Are you sure you want to close this blocked issue?')
.gl-alert-body
= _('This issue is currently blocked by the following issues: %{issues}.').html_safe % { issues: blocked_by_issues_links }
.gl-alert-actions
= link_to _("Yes, close issue"), close_issuable_path(issue), rel: 'nofollow', method: '',
class: "btn btn-close-anyway gl-alert-action btn-warning btn-md gl-button", title: _("Yes, close issue")
%button.btn.gl-alert-action.btn-warning.btn-md.gl-button.btn-secondary
= s_('Cancel')
# frozen_string_literal: true
require 'spec_helper'
describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
let(:user) { create(:user, feed_token: 'feedtoken:coldfeed') }
let(:group) { create(:group) }
let(:project) { create(:project_empty_repo, namespace: group, path: 'issues-project') }
render_views
before(:all) do
clean_frontend_fixtures('ee/issues/')
end
before do
project.add_developer(user)
sign_in(user)
end
after do
remove_repository(project)
end
it 'ee/issues/blocked-issue.html' do
issue = create(:issue, project: project)
related_issue = create(:issue, project: project)
create(:issue_link, source: related_issue, target: issue, link_type: IssueLink::TYPE_BLOCKS)
render_issue(issue)
end
private
def render_issue(issue)
get :show, params: {
namespace_id: project.namespace.to_param,
project_id: project,
id: issue.to_param
}
expect(response).to be_successful
end
end
/* eslint-disable one-var */
import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Issue from '~/issue';
import '~/lib/utils/text_utility';
describe('Issue', () => {
let testContext;
beforeEach(() => {
testContext = {};
});
let $btn, $dropdown, $alert, $boxOpen, $boxClosed;
preloadFixtures('ee/issues/blocked-issue.html');
describe('with blocked issue', () => {
let mock;
function setup() {
testContext.issue = new Issue();
testContext.$projectIssuesCounter = $('.issue_counter').first();
testContext.$projectIssuesCounter.text('1,001');
}
function mockCloseButtonResponseSuccess(url, response) {
mock.onPut(url).reply(() => [200, response]);
}
beforeEach(() => {
loadFixtures('ee/issues/blocked-issue.html');
mock = new MockAdapter(axios);
mock.onGet(/(.*)\/related_branches$/).reply(200, {});
jest.spyOn(axios, 'get');
});
afterEach(() => {
mock.restore();
});
it(`displays warning when attempting to close the issue`, done => {
setup();
$btn = $('.js-issuable-close-button');
$dropdown = $('.js-issuable-close-dropdown ');
$alert = $('.js-close-blocked-issue-warning');
expect($btn).toExist();
expect($btn).toHaveClass('btn-issue-blocked');
expect($dropdown).not.toHaveClass('hidden');
expect($alert).toHaveClass('hidden');
testContext.$triggeredButton = $btn;
testContext.$triggeredButton.trigger('click');
setImmediate(() => {
expect($alert).not.toHaveClass('hidden');
expect($dropdown).toHaveClass('hidden');
done();
});
});
it(`hides warning when cancelling closing the issue`, done => {
setup();
$btn = $('.js-issuable-close-button');
$alert = $('.js-close-blocked-issue-warning');
testContext.$triggeredButton = $btn;
testContext.$triggeredButton.trigger('click');
setImmediate(() => {
expect($alert).not.toHaveClass('hidden');
const $cancelbtn = $('.js-close-blocked-issue-warning .btn-secondary');
$cancelbtn.trigger('click');
expect($alert).toHaveClass('hidden');
done();
});
});
it('closes the issue when clicking alert close button', done => {
$btn = $('.js-issuable-close-button');
$boxOpen = $('div.status-box-open');
$boxClosed = $('div.status-box-issue-closed');
expect($boxOpen).not.toHaveClass('hidden');
expect($boxOpen).toHaveText('Open');
expect($boxClosed).toHaveClass('hidden');
testContext.$triggeredButton = $btn;
mockCloseButtonResponseSuccess(testContext.$triggeredButton.attr('href'), {
id: 34,
});
setup();
testContext.$triggeredButton.trigger('click');
const $btnCloseAnyway = $('.js-close-blocked-issue-warning .btn-close-anyway');
$btnCloseAnyway.trigger('click');
setImmediate(() => {
expect($btn).toHaveText('Reopen');
expect($boxOpen).toHaveClass('hidden');
expect($boxClosed).not.toHaveClass('hidden');
expect($boxClosed).toHaveText('Closed');
done();
});
});
});
});
...@@ -32,10 +32,10 @@ describe IssueEntity do ...@@ -32,10 +32,10 @@ describe IssueEntity do
expect(subject).to include(:blocked_by_issues) expect(subject).to include(:blocked_by_issues)
end end
it 'exposes only id and web_path' do it 'exposes only iid and web_url' do
response = described_class.new(blocked_issue, request: request, with_blocking_issues: true).as_json response = described_class.new(blocked_issue, request: request, with_blocking_issues: true).as_json
expect(response[:blocked_by_issues].first.keys).to match_array([:id, :web_url]) expect(response[:blocked_by_issues].first.keys).to match_array([:iid, :web_url])
end end
end end
end end
...@@ -2423,6 +2423,9 @@ msgstr "" ...@@ -2423,6 +2423,9 @@ msgstr ""
msgid "Are you sure you want to cancel editing this comment?" msgid "Are you sure you want to cancel editing this comment?"
msgstr "" msgstr ""
msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?" msgid "Are you sure you want to delete %{name}?"
msgstr "" msgstr ""
...@@ -21074,6 +21077,9 @@ msgstr "" ...@@ -21074,6 +21077,9 @@ msgstr ""
msgid "This issue is confidential" msgid "This issue is confidential"
msgstr "" msgstr ""
msgid "This issue is currently blocked by the following issues: %{issues}."
msgstr ""
msgid "This issue is locked." msgid "This issue is locked."
msgstr "" msgstr ""
...@@ -23640,6 +23646,9 @@ msgstr "" ...@@ -23640,6 +23646,9 @@ msgstr ""
msgid "Yes, add it" msgid "Yes, add it"
msgstr "" msgstr ""
msgid "Yes, close issue"
msgstr ""
msgid "Yes, let me map Google Code users to full names or GitLab users." msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr "" msgstr ""
......
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