Commit 5f8f555a authored by Jacob Schatz's avatar Jacob Schatz

Merge branch '30281-report-abusive-content-from-an-issue-or-merge-request' into 'master'

Resolve "Report abusive content from an issue or merge request"

Closes #30281

See merge request !11786
parents 5b257778 63fc3935
import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter);
class CloseReopenReportToggle {
constructor(opts = {}) {
this.dropdownTrigger = opts.dropdownTrigger;
this.dropdownList = opts.dropdownList;
this.button = opts.button;
}
initDroplab() {
this.reopenItem = this.dropdownList.querySelector('.reopen-item');
this.closeItem = this.dropdownList.querySelector('.close-item');
this.droplab = new DropLab();
const config = this.setConfig();
this.droplab.init(this.dropdownTrigger, this.dropdownList, [InputSetter], config);
}
updateButton(isClosed) {
this.toggleButtonType(isClosed);
this.button.blur();
}
toggleButtonType(isClosed) {
const [showItem, hideItem] = this.getButtonTypes(isClosed);
showItem.classList.remove('hidden');
showItem.classList.add('droplab-item-selected');
hideItem.classList.add('hidden');
hideItem.classList.remove('droplab-item-selected');
showItem.click();
}
getButtonTypes(isClosed) {
return isClosed ? [this.reopenItem, this.closeItem] : [this.closeItem, this.reopenItem];
}
setDisable(shouldDisable = true) {
if (shouldDisable) {
this.button.setAttribute('disabled', 'true');
this.dropdownTrigger.setAttribute('disabled', 'true');
} else {
this.button.removeAttribute('disabled');
this.dropdownTrigger.removeAttribute('disabled');
}
}
setConfig() {
const config = {
InputSetter: [
{
input: this.button,
valueAttribute: 'data-text',
inputAttribute: 'data-value',
},
{
input: this.button,
valueAttribute: 'data-text',
inputAttribute: 'title',
},
{
input: this.button,
valueAttribute: 'data-button-class',
inputAttribute: 'class',
},
{
input: this.dropdownTrigger,
valueAttribute: 'data-toggle-class',
inputAttribute: 'class',
},
{
input: this.button,
valueAttribute: 'data-url',
inputAttribute: 'href',
},
{
input: this.button,
valueAttribute: 'data-method',
inputAttribute: 'data-method',
},
],
};
return config;
}
}
export default CloseReopenReportToggle;
import DropLab from './droplab/drop_lab';
import InputSetter from './droplab/plugins/input_setter';
import ISetter from './droplab/plugins/input_setter';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter);
class CommentTypeToggle {
constructor(opts = {}) {
......
import CloseReopenReportToggle from '../close_reopen_report_toggle';
function initCloseReopenReport() {
const container = document.querySelector('.js-issuable-close-dropdown');
if (!container) return undefined;
const dropdownTrigger = container.querySelector('.js-issuable-close-toggle');
const dropdownList = container.querySelector('.js-issuable-close-menu');
const button = container.querySelector('.js-issuable-close-button');
const closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
closeReopenReportToggle.initDroplab();
return closeReopenReportToggle;
}
const IssuablesHelper = {
initCloseReopenReport,
};
export default IssuablesHelper;
......@@ -6,6 +6,7 @@ import '~/lib/utils/text_utility';
import './flash';
import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
class Issue {
constructor() {
......@@ -28,6 +29,11 @@ class Issue {
Issue.initMergeRequests();
Issue.initRelatedBranches();
this.closeButtons = $('a.btn-close');
this.reopenButtons = $('a.btn-reopen');
this.initCloseReopenReport();
if (Issue.createMrDropdownWrap) {
this.createMergeRequestDropdown = new CreateMergeRequestDropdown(Issue.createMrDropdownWrap);
}
......@@ -35,13 +41,8 @@ class Issue {
initIssueBtnEventListeners() {
const issueFailMessage = 'Unable to update this issue at this time.';
const closeButtons = $('a.btn-close');
const isClosedBadge = $('div.status-box-closed');
const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter');
const reopenButtons = $('a.btn-reopen');
return closeButtons.add(reopenButtons).on('click', (e) => {
return $(document).on('click', 'a.btn-close, a.btn-reopen', (e) => {
var $button, shouldSubmit, url;
e.preventDefault();
e.stopImmediatePropagation();
......@@ -50,7 +51,9 @@ class Issue {
if (shouldSubmit) {
Issue.submitNoteForm($button.closest('form'));
}
$button.prop('disabled', true);
this.disableCloseReopenButton($button);
url = $button.attr('href');
return $.ajax({
type: 'PUT',
......@@ -58,15 +61,19 @@ class Issue {
})
.fail(() => new Flash(issueFailMessage))
.done((data) => {
const isClosedBadge = $('div.status-box-closed');
const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter');
if ('id' in data) {
$(document).trigger('issuable:change');
const isClosed = $button.hasClass('btn-close');
closeButtons.toggleClass('hidden', isClosed);
reopenButtons.toggleClass('hidden', !isClosed);
isClosedBadge.toggleClass('hidden', !isClosed);
isOpenBadge.toggleClass('hidden', isClosed);
this.toggleCloseReopenButton(isClosed);
let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues));
......@@ -83,12 +90,34 @@ class Issue {
} else {
new Flash(issueFailMessage);
}
$button.prop('disabled', false);
})
.then(() => {
this.disableCloseReopenButton($button, false);
});
});
}
initCloseReopenReport() {
this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport();
if (this.closeButtons) this.closeButtons = this.closeButtons.not('.issuable-close-button');
if (this.reopenButtons) this.reopenButtons = this.reopenButtons.not('.issuable-close-button');
}
disableCloseReopenButton($button, shouldDisable) {
if (this.closeReopenReportToggle) {
this.closeReopenReportToggle.setDisable(shouldDisable);
} else {
$button.prop('disabled', shouldDisable);
}
}
toggleCloseReopenButton(isClosed) {
if (this.closeReopenReportToggle) this.closeReopenReportToggle.updateButton(isClosed);
this.closeButtons.toggleClass('hidden', isClosed);
this.reopenButtons.toggleClass('hidden', !isClosed);
}
static submitNoteForm(form) {
var noteText;
noteText = form.find("textarea.js-note-text").val();
......
......@@ -4,6 +4,7 @@
import 'vendor/jquery.waitforimages';
import TaskList from './task_list';
import './merge_request_tabs';
import IssuablesHelper from './helpers/issuables_helper';
(function() {
this.MergeRequest = (function() {
......@@ -21,9 +22,12 @@ import './merge_request_tabs';
return _this.showAllCommits();
};
})(this));
this.initTabs();
this.initMRBtnListeners();
this.initCommitMessageListeners();
this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport();
if ($("a.btn-close").length) {
this.taskList = new TaskList({
dataType: 'merge_request',
......@@ -64,11 +68,15 @@ import './merge_request_tabs';
if (shouldSubmit && $this.data('submitted')) {
return;
}
if (this.closeReopenReportToggle) this.closeReopenReportToggle.setDisable();
if (shouldSubmit) {
if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) {
e.preventDefault();
e.stopImmediatePropagation();
return _this.submitNoteForm($this.closest('form'), $this);
_this.submitNoteForm($this.closest('form'), $this);
}
}
});
......
......@@ -20,17 +20,29 @@
color: $text;
border-color: $border;
> .icon {
color: $text;
}
&:hover,
&:focus {
background-color: $hover-background;
border-color: $hover-border;
color: $hover-text;
> .icon {
color: $hover-text;
}
}
&:active {
background-color: $active-background;
border-color: $active-border;
color: $hover-text;
> .icon {
color: $hover-text;
}
}
}
......@@ -163,7 +175,8 @@
@include btn-orange;
}
&.btn-close {
&.btn-close,
&.btn-close-color {
@include btn-outline($white-light, $orange-600, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700);
}
......@@ -181,7 +194,8 @@
float: right;
}
&.btn-reopen {
&.btn-reopen,
.btn-reopen-color {
/* should be same as parent class for now */
}
......
......@@ -295,9 +295,74 @@
}
}
.filtered-search-box-input-container .dropdown-menu,
.filtered-search-box-input-container .dropdown-menu-nav,
.comment-type-dropdown .dropdown-menu {
.droplab-dropdown {
.description {
display: inline-block;
white-space: normal;
margin-left: 5px;
}
.dropdown-toggle > i {
pointer-events: none;
}
li {
padding: $gl-btn-padding $gl-btn-padding 2px;
cursor: pointer;
> a,
> button {
display: flex;
margin: 0;
padding: 0;
border-radius: 0;
text-overflow: inherit;
background-color: inherit;
color: inherit;
border: inherit;
text-align: left;
&:hover,
&:focus {
background-color: inherit;
color: inherit;
}
&.btn .fa:not(:last-child) {
margin-left: 5px;
}
}
&:hover,
&:focus {
background-color: $dropdown-hover-color;
color: $white-light;
}
&.droplab-item-selected i {
visibility: visible;
}
.icon {
visibility: hidden;
}
}
.icon {
display: inline-block;
vertical-align: top;
padding-top: 2px;
}
.divider {
margin: 0 8px;
padding: 0;
border-top: $gray-darkest;
}
}
.droplab-dropdown .dropdown-menu,
.droplab-dropdown .dropdown-menu-nav {
display: none;
opacity: 1;
visibility: visible;
......
......@@ -70,6 +70,13 @@
.input-token {
max-width: 200px;
padding: 0;
&:hover,
&:focus {
background-color: inherit;
color: inherit;
}
}
.input-token:only-child,
......@@ -156,6 +163,16 @@
}
}
.droplab-dropdown li.filtered-search-token {
padding: 0;
&:hover,
&:focus {
background-color: inherit;
color: inherit;
}
}
.filtered-search-term {
.name {
background-color: inherit;
......
......@@ -799,3 +799,28 @@
}
}
}
.issuable-close-button,
.issuable-close-toggle {
@include transition(border-color, color);
}
.issuable-close-dropdown {
.dropdown-menu {
min-width: 270px;
left: auto;
right: 0;
}
.description {
margin-bottom: 10px;
.text {
margin: 0;
}
}
.dropdown-toggle > .icon {
margin: 0 3px;
}
}
......@@ -356,7 +356,6 @@
color: $white-light;
padding-right: 2px;
margin-top: 2px;
pointer-events: none;
}
}
......@@ -366,56 +365,6 @@
width: 298px;
}
.description {
display: inline-block;
white-space: normal;
margin-left: 8px;
padding-right: 33px;
}
li {
padding-top: 6px;
& > a {
margin: 0;
padding: 0;
color: inherit;
border-radius: 0;
text-overflow: inherit;
&:hover,
&:focus {
background-color: inherit;
color: inherit;
}
}
&:hover,
&:focus {
background-color: $dropdown-hover-color;
color: $white-light;
}
&.droplab-item-selected i {
visibility: visible;
}
i {
visibility: hidden;
}
}
i {
display: inline-block;
vertical-align: top;
padding-top: 2px;
}
.divider {
margin: 0 8px;
padding: 0;
border-top: $gray-darkest;
}
@media (max-width: $screen-xs-max) {
display: flex;
......
......@@ -17,8 +17,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
end
def merge_request_params
params.require(:merge_request)
.permit(merge_request_params_attributes)
params.require(:merge_request).permit(merge_request_params_attributes)
end
def merge_request_params_attributes
......
......@@ -245,6 +245,53 @@ module IssuablesHelper
@counts[cache_key][state]
end
def close_issuable_url(issuable)
issuable_url(issuable, close_reopen_params(issuable, :close))
end
def reopen_issuable_url(issuable)
issuable_url(issuable, close_reopen_params(issuable, :reopen))
end
def close_reopen_issuable_url(issuable, should_inverse = false)
issuable.closed? ^ should_inverse ? reopen_issuable_url(issuable) : close_issuable_url(issuable)
end
def issuable_url(issuable, *options)
case issuable
when Issue
issue_url(issuable, *options)
when MergeRequest
merge_request_url(issuable, *options)
end
end
def issuable_button_visibility(issuable, closed)
case issuable
when Issue
issue_button_visibility(issuable, closed)
when MergeRequest
merge_request_button_visibility(issuable, closed)
end
end
def issuable_close_reopen_button_method(issuable)
case issuable
when Issue
''
when MergeRequest
'put'
end
end
def issuable_author_is_current_user(issuable)
issuable.author == current_user
end
def issuable_display_type(issuable)
issuable.model_name.human.downcase
end
private
def sidebar_gutter_collapsed?
......@@ -270,8 +317,6 @@ module IssuablesHelper
issue_template_names
when MergeRequest
merge_request_template_names
else
raise 'Unknown issuable type!'
end
end
......@@ -301,4 +346,12 @@ module IssuablesHelper
container: (is_collapsed ? 'body' : nil)
}
end
def close_reopen_params(issuable, action)
{
issuable.model_name.to_s.underscore => { state_event: action }
}.tap do |params|
params[:format] = :json if issuable.is_a?(Issue)
end
end
end
......@@ -30,24 +30,23 @@
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
- if can_update_issue
%li
= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'issuable-edit'
%li
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%li
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
%li= link_to 'Edit', edit_project_issue_path(@project, @issue)
- unless current_user == @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue
%li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- if can_report_spam
%li
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
%li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
- if can_update_issue || can_report_spam
%li.divider
%li
= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
%li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
- if can_update_issue
= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue
- if can_report_spam
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
= link_to new_project_issue_path(@project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
......
- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.closed_without_fork?
.alert.alert-danger
%p The source project of this merge request has been removed.
......@@ -15,21 +17,24 @@
.issuable-meta
= issuable_meta(@merge_request, @project, "Merge request")
- if can?(current_user, :update_merge_request, @merge_request)
.issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
Options
= icon('caret-down')
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
.issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
Options
= icon('caret-down')
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
- if can_update_merge_request
%li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit'
- unless current_user == @merge_request.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request))
- if can_update_merge_request
%li{ class: merge_request_button_visibility(@merge_request, true) }
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
%li{ class: merge_request_button_visibility(@merge_request, false) }
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
%li
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: 'issuable-edit'
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{merge_request_button_visibility(@merge_request, true)}", title: 'Close merge request'
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen reopen-mr-link #{merge_request_button_visibility(@merge_request, false)}", title: 'Reopen merge request'
= link_to edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit" do
Edit
- if can_update_merge_request
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit"
= render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request
- is_current_user = issuable_author_is_current_user(issuable)
- display_issuable_type = issuable_display_type(issuable)
- button_method = issuable_close_reopen_button_method(issuable)
- if can_update && is_current_user
= link_to "Close #{display_issuable_type}", close_issuable_url(issuable), method: button_method,
class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
= link_to "Reopen #{display_issuable_type}", reopen_issuable_url(issuable), method: button_method,
class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
- elsif can_update && !is_current_user
= render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
- else
= link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
class: 'hidden-xs hidden-sm btn btn-grouped btn-close-color', title: 'Report abuse'
- display_issuable_type = issuable_display_type(issuable)
- button_action = issuable.closed? ? 'reopen' : 'close'
- display_button_action = button_action.capitalize
- button_responsive_class = 'hidden-xs hidden-sm'
- button_class = "#{button_responsive_class} btn btn-grouped js-issuable-close-button issuable-close-button"
- toggle_class = "#{button_responsive_class} btn btn-nr dropdown-toggle js-issuable-close-toggle"
- button_method = issuable_close_reopen_button_method(issuable)
.pull-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_url(issuable),
method: button_method, class: "#{button_class} btn-#{button_action}", title: "#{display_button_action} #{display_issuable_type}"
= button_tag type: 'button', class: "#{toggle_class} btn-#{button_action}-color",
data: { 'dropdown-trigger' => '#issuable-close-menu' }, 'aria-label' => 'Toggle dropdown' do
= icon('caret-down', class: 'toggle-icon icon')
%ul#issuable-close-menu.js-issuable-close-menu.dropdown-menu{ class: button_responsive_class, data: { dropdown: true } }
%li.close-item{ class: "#{issuable_button_visibility(issuable, true) || 'droplab-item-selected'}",
data: { text: "Close #{display_issuable_type}", url: close_issuable_url(issuable),
button_class: "#{button_class} btn-close", toggle_class: "#{toggle_class} btn-close-color", method: button_method } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
.description
%strong.title
Close
= display_issuable_type
%li.reopen-item{ class: "#{issuable_button_visibility(issuable, false) || 'droplab-item-selected'}",
data: { text: "Reopen #{display_issuable_type}", url: reopen_issuable_url(issuable),
button_class: "#{button_class} btn-reopen", toggle_class: "#{toggle_class} btn-reopen-color", method: button_method } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
.description
%strong.title
Reopen
= display_issuable_type
%li.divider.droplab-item-ignore
%li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
.description
%strong.title Report abuse
%p.text
Report
= display_issuable_type.pluralize
that are abusive, inappropriate or spam.
......@@ -19,7 +19,7 @@
content_class: "filtered-search-history-dropdown-content",
title: "Recent searches" }) do
.js-filtered-search-history-dropdown{ data: { project_full_path: @project.full_path } }
.filtered-search-box-input-container
.filtered-search-box-input-container.droplab-dropdown
.scroll-container
%ul.tokens-container.list-unstyled
%li.input-token
......
- noteable_name = @note.noteable.human_class_name
.pull-left.btn-group.append-right-10.comment-type-dropdown.js-comment-type-dropdown
.pull-left.btn-group.append-right-10.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown
%input.btn.btn-nr.btn-create.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: 'Comment' }
- if @note.can_be_discussion_note?
......@@ -9,8 +9,8 @@
%ul#resolvable-comment-menu.dropdown-menu{ data: { dropdown: true } }
%li#comment.droplab-item-selected{ data: { value: '', 'submit-text' => 'Comment', 'close-text' => "Comment & close #{noteable_name}", 'reopen-text' => "Comment & reopen #{noteable_name}" } }
%a{ href: '#' }
= icon('check')
%button.btn.btn-transparent
= icon('check', class: 'icon')
.description
%strong Comment
%p
......@@ -19,8 +19,8 @@
%li.divider.droplab-item-ignore
%li#discussion{ data: { value: 'DiscussionNote', 'submit-text' => 'Start discussion', 'close-text' => "Start discussion & close #{noteable_name}", 'reopen-text' => "Start discussion & reopen #{noteable_name}" } }
%a{ href: '#' }
= icon('check')
%button.btn.btn-transparent
= icon('check', class: 'icon')
.description
%strong Start discussion
%p
......
require 'spec_helper'
describe 'Issuables Close/Reopen/Report toggle', :feature do
let(:user) { create(:user) }
shared_examples 'an issuable close/reopen/report toggle' do
let(:container) { find('.issuable-close-dropdown') }
let(:human_model_name) { issuable.model_name.human.downcase }
it 'shows toggle' do
expect(page).to have_link("Close #{human_model_name}")
expect(page).to have_selector('.issuable-close-dropdown')
end
it 'opens a dropdown when toggle is clicked' do
container.find('.dropdown-toggle').click
expect(container).to have_selector('.dropdown-menu')
expect(container).to have_content("Close #{human_model_name}")
expect(container).to have_content('Report abuse')
expect(container).to have_content("Report #{human_model_name.pluralize} that are abusive, inappropriate or spam.")
expect(container).to have_selector('.close-item.droplab-item-selected')
expect(container).to have_selector('.report-item')
expect(container).not_to have_selector('.report-item.droplab-item-selected')
expect(container).not_to have_selector('.reopen-item')
end
it 'changes the button when an item is selected' do
button = container.find('.issuable-close-button')
container.find('.dropdown-toggle').click
container.find('.report-item').click
expect(container).not_to have_selector('.dropdown-menu')
expect(button).to have_content('Report abuse')
container.find('.dropdown-toggle').click
container.find('.close-item').click
expect(button).to have_content("Close #{human_model_name}")
end
end
context 'on an issue' do
let(:project) { create(:empty_project) }
let(:issuable) { create(:issue, project: project) }
before do
project.add_master(user)
login_as user
end
context 'when user has permission to update', :js do
before do
visit project_issue_path(project, issuable)
end
it_behaves_like 'an issuable close/reopen/report toggle'
end
context 'when user doesnt have permission to update' do
let(:cant_project) { create(:empty_project) }
let(:cant_issuable) { create(:issue, project: cant_project) }
before do
cant_project.add_guest(user)
visit project_issue_path(cant_project, cant_issuable)
end
it 'only shows the `Report abuse` and `New issue` buttons' do
expect(page).to have_link('Report abuse')
expect(page).to have_link('New issue')
expect(page).not_to have_link('Close issue')
expect(page).not_to have_link('Reopen issue')
expect(page).not_to have_link('Edit')
end
end
end
context 'on a merge request' do
let(:project) { create(:project) }
let(:issuable) { create(:merge_request, source_project: project) }
before do
project.add_master(user)
login_as user
end
context 'when user has permission to update', :js do
before do
visit project_merge_request_path(project, issuable)
end
it_behaves_like 'an issuable close/reopen/report toggle'
end
context 'when user doesnt have permission to update' do
let(:cant_project) { create(:project) }
let(:cant_issuable) { create(:merge_request, source_project: cant_project) }
before do
cant_project.add_reporter(user)
visit project_merge_request_path(cant_project, cant_issuable)
end
it 'only shows a `Report abuse` button' do
expect(page).to have_link('Report abuse')
expect(page).not_to have_link('Close merge request')
expect(page).not_to have_link('Reopen merge request')
expect(page).not_to have_link('Edit')
end
end
end
end
......@@ -133,7 +133,7 @@ describe 'Visual tokens', js: true, feature: true do
describe 'editing milestone token' do
before do
input_filtered_search('milestone:%10.0 author:none', submit: false)
first('.tokens-container .filtered-search-token').double_click
first('.tokens-container .filtered-search-token').click
first('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item')
end
......
import CloseReopenReportToggle from '~/close_reopen_report_toggle';
import DropLab from '~/droplab/drop_lab';
describe('CloseReopenReportToggle', () => {
describe('class constructor', () => {
const dropdownTrigger = {};
const dropdownList = {};
const button = {};
let commentTypeToggle;
beforeEach(function () {
commentTypeToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
});
it('sets .dropdownTrigger', function () {
expect(commentTypeToggle.dropdownTrigger).toBe(dropdownTrigger);
});
it('sets .dropdownList', function () {
expect(commentTypeToggle.dropdownList).toBe(dropdownList);
});
it('sets .button', function () {
expect(commentTypeToggle.button).toBe(button);
});
});
describe('initDroplab', () => {
let closeReopenReportToggle;
const dropdownList = jasmine.createSpyObj('dropdownList', ['querySelector']);
const dropdownTrigger = {};
const button = {};
const reopenItem = {};
const closeItem = {};
const config = {};
beforeEach(() => {
spyOn(DropLab.prototype, 'init');
dropdownList.querySelector.and.returnValues(reopenItem, closeItem);
closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
spyOn(closeReopenReportToggle, 'setConfig').and.returnValue(config);
closeReopenReportToggle.initDroplab();
});
it('sets .reopenItem and .closeItem', () => {
expect(dropdownList.querySelector).toHaveBeenCalledWith('.reopen-item');
expect(dropdownList.querySelector).toHaveBeenCalledWith('.close-item');
expect(closeReopenReportToggle.reopenItem).toBe(reopenItem);
expect(closeReopenReportToggle.closeItem).toBe(closeItem);
});
it('sets .droplab', () => {
expect(closeReopenReportToggle.droplab).toEqual(jasmine.any(Object));
});
it('calls .setConfig', () => {
expect(closeReopenReportToggle.setConfig).toHaveBeenCalled();
});
it('calls droplab.init', () => {
expect(DropLab.prototype.init).toHaveBeenCalledWith(
dropdownTrigger,
dropdownList,
jasmine.any(Array),
config,
);
});
});
describe('updateButton', () => {
let closeReopenReportToggle;
const dropdownList = {};
const dropdownTrigger = {};
const button = jasmine.createSpyObj('button', ['blur']);
const isClosed = true;
beforeEach(() => {
closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
spyOn(closeReopenReportToggle, 'toggleButtonType');
closeReopenReportToggle.updateButton(isClosed);
});
it('calls .toggleButtonType', () => {
expect(closeReopenReportToggle.toggleButtonType).toHaveBeenCalledWith(isClosed);
});
it('calls .button.blur', () => {
expect(closeReopenReportToggle.button.blur).toHaveBeenCalled();
});
});
describe('toggleButtonType', () => {
let closeReopenReportToggle;
const dropdownList = {};
const dropdownTrigger = {};
const button = {};
const isClosed = true;
const showItem = jasmine.createSpyObj('showItem', ['click']);
const hideItem = {};
showItem.classList = jasmine.createSpyObj('classList', ['add', 'remove']);
hideItem.classList = jasmine.createSpyObj('classList', ['add', 'remove']);
beforeEach(() => {
closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
spyOn(closeReopenReportToggle, 'getButtonTypes').and.returnValue([showItem, hideItem]);
closeReopenReportToggle.toggleButtonType(isClosed);
});
it('calls .getButtonTypes', () => {
expect(closeReopenReportToggle.getButtonTypes).toHaveBeenCalledWith(isClosed);
});
it('removes hide class and add selected class to showItem, opposite for hideItem', () => {
expect(showItem.classList.remove).toHaveBeenCalledWith('hidden');
expect(showItem.classList.add).toHaveBeenCalledWith('droplab-item-selected');
expect(hideItem.classList.add).toHaveBeenCalledWith('hidden');
expect(hideItem.classList.remove).toHaveBeenCalledWith('droplab-item-selected');
});
it('clicks the showItem', () => {
expect(showItem.click).toHaveBeenCalled();
});
});
describe('getButtonTypes', () => {
let closeReopenReportToggle;
const dropdownList = {};
const dropdownTrigger = {};
const button = {};
const reopenItem = {};
const closeItem = {};
beforeEach(() => {
closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
closeReopenReportToggle.reopenItem = reopenItem;
closeReopenReportToggle.closeItem = closeItem;
});
it('returns reopenItem, closeItem if isClosed is true', () => {
const buttonTypes = closeReopenReportToggle.getButtonTypes(true);
expect(buttonTypes).toEqual([reopenItem, closeItem]);
});
it('returns closeItem, reopenItem if isClosed is false', () => {
const buttonTypes = closeReopenReportToggle.getButtonTypes(false);
expect(buttonTypes).toEqual([closeItem, reopenItem]);
});
});
describe('setDisable', () => {
let closeReopenReportToggle;
const dropdownList = {};
const dropdownTrigger = jasmine.createSpyObj('button', ['setAttribute', 'removeAttribute']);
const button = jasmine.createSpyObj('button', ['setAttribute', 'removeAttribute']);
beforeEach(() => {
closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
});
it('disable .button and .dropdownTrigger if shouldDisable is true', () => {
closeReopenReportToggle.setDisable(true);
expect(button.setAttribute).toHaveBeenCalledWith('disabled', 'true');
expect(dropdownTrigger.setAttribute).toHaveBeenCalledWith('disabled', 'true');
});
it('disable .button and .dropdownTrigger if shouldDisable is undefined', () => {
closeReopenReportToggle.setDisable();
expect(button.setAttribute).toHaveBeenCalledWith('disabled', 'true');
expect(dropdownTrigger.setAttribute).toHaveBeenCalledWith('disabled', 'true');
});
it('enable .button and .dropdownTrigger if shouldDisable is false', () => {
closeReopenReportToggle.setDisable(false);
expect(button.removeAttribute).toHaveBeenCalledWith('disabled');
expect(dropdownTrigger.removeAttribute).toHaveBeenCalledWith('disabled');
});
});
describe('setConfig', () => {
let closeReopenReportToggle;
const dropdownList = {};
const dropdownTrigger = {};
const button = {};
let config;
beforeEach(() => {
closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
config = closeReopenReportToggle.setConfig();
});
it('returns a config object', () => {
expect(config).toEqual({
InputSetter: [
{
input: button,
valueAttribute: 'data-text',
inputAttribute: 'data-value',
},
{
input: button,
valueAttribute: 'data-text',
inputAttribute: 'title',
},
{
input: button,
valueAttribute: 'data-button-class',
inputAttribute: 'class',
},
{
input: dropdownTrigger,
valueAttribute: 'data-toggle-class',
inputAttribute: 'class',
},
{
input: button,
valueAttribute: 'data-url',
inputAttribute: 'href',
},
{
input: button,
valueAttribute: 'data-method',
inputAttribute: 'data-method',
},
],
});
});
});
});
/* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
import Issue from '~/issue';
import CloseReopenReportToggle from '~/close_reopen_report_toggle';
import '~/lib/utils/text_utility';
describe('Issue', function() {
let $boxClosed, $boxOpen, $btnClose, $btnReopen;
let $boxClosed, $boxOpen, $btn;
preloadFixtures('issues/closed-issue.html.raw');
preloadFixtures('issues/issue-with-task-list.html.raw');
......@@ -20,9 +20,7 @@ describe('Issue', function() {
function expectIssueState(isIssueOpen) {
expectVisibility($boxClosed, !isIssueOpen);
expectVisibility($boxOpen, isIssueOpen);
expectVisibility($btnClose, isIssueOpen);
expectVisibility($btnReopen, !isIssueOpen);
expect($btn).toHaveText(isIssueOpen ? 'Close issue' : 'Reopen issue');
}
function expectNewBranchButtonState(isPending, canCreate) {
......@@ -57,7 +55,7 @@ describe('Issue', function() {
}
}
function findElements() {
function findElements(isIssueInitiallyOpen) {
$boxClosed = $('div.status-box-closed');
expect($boxClosed).toExist();
expect($boxClosed).toHaveText('Closed');
......@@ -66,13 +64,9 @@ describe('Issue', function() {
expect($boxOpen).toExist();
expect($boxOpen).toHaveText('Open');
$btnClose = $('.btn-close.btn-grouped');
expect($btnClose).toExist();
expect($btnClose).toHaveText('Close issue');
$btnReopen = $('.btn-reopen.btn-grouped');
expect($btnReopen).toExist();
expect($btnReopen).toHaveText('Reopen issue');
$btn = $('.js-issuable-close-button');
expect($btn).toExist();
expect($btn).toHaveText(isIssueInitiallyOpen ? 'Close issue' : 'Reopen issue');
}
describe('task lists', function() {
......@@ -99,7 +93,6 @@ describe('Issue', function() {
function ajaxSpy(req) {
if (req.url === this.$triggeredButton.attr('href')) {
expect(req.type).toBe('PUT');
expect(this.$triggeredButton).toHaveProp('disabled', true);
expectNewBranchButtonState(true, false);
return this.issueStateDeferred;
} else if (req.url === Issue.createMrDropdownWrap.dataset.canCreatePath) {
......@@ -119,10 +112,11 @@ describe('Issue', function() {
loadFixtures('issues/closed-issue.html.raw');
}
findElements();
findElements(isIssueInitiallyOpen);
this.issue = new Issue();
expectIssueState(isIssueInitiallyOpen);
this.$triggeredButton = isIssueInitiallyOpen ? $btnClose : $btnReopen;
this.$triggeredButton = $btn;
this.$projectIssuesCounter = $('.issue_counter');
this.$projectIssuesCounter.text('1,001');
......@@ -143,7 +137,7 @@ describe('Issue', function() {
});
expectIssueState(!isIssueInitiallyOpen);
expect(this.$triggeredButton).toHaveProp('disabled', false);
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002');
expectNewBranchButtonState(false, !isIssueInitiallyOpen);
});
......@@ -158,7 +152,7 @@ describe('Issue', function() {
});
expectIssueState(isIssueInitiallyOpen);
expect(this.$triggeredButton).toHaveProp('disabled', false);
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expectErrorMessage();
expect(this.$projectIssuesCounter.text()).toBe('1,001');
expectNewBranchButtonState(false, isIssueInitiallyOpen);
......@@ -172,7 +166,7 @@ describe('Issue', function() {
});
expectIssueState(isIssueInitiallyOpen);
expect(this.$triggeredButton).toHaveProp('disabled', true);
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expectErrorMessage();
expect(this.$projectIssuesCounter.text()).toBe('1,001');
expectNewBranchButtonState(false, isIssueInitiallyOpen);
......@@ -195,4 +189,37 @@ describe('Issue', function() {
});
});
});
describe('units', () => {
describe('class constructor', () => {
it('calls .initCloseReopenReport', () => {
spyOn(Issue.prototype, 'initCloseReopenReport');
new Issue(); // eslint-disable-line no-new
expect(Issue.prototype.initCloseReopenReport).toHaveBeenCalled();
});
});
describe('initCloseReopenReport', () => {
it('calls .initDroplab', () => {
const container = jasmine.createSpyObj('container', ['querySelector']);
const dropdownTrigger = {};
const dropdownList = {};
const button = {};
spyOn(document, 'querySelector').and.returnValue(container);
spyOn(CloseReopenReportToggle.prototype, 'initDroplab');
container.querySelector.and.returnValues(dropdownTrigger, dropdownList, button);
Issue.prototype.initCloseReopenReport();
expect(document.querySelector).toHaveBeenCalledWith('.js-issuable-close-dropdown');
expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-toggle');
expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-menu');
expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-button');
expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled();
});
});
});
});
......@@ -2,10 +2,12 @@
/* global MergeRequest */
import '~/merge_request';
import CloseReopenReportToggle from '~/close_reopen_report_toggle';
import IssuablesHelper from '~/helpers/issuables_helper';
(function() {
describe('MergeRequest', function() {
return describe('task lists', function() {
describe('task lists', function() {
preloadFixtures('merge_requests/merge_request_with_task_list.html.raw');
beforeEach(function() {
loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
......@@ -27,5 +29,34 @@ import '~/merge_request';
return $('.js-task-list-field').trigger('tasklist:changed');
});
});
describe('class constructor', () => {
it('calls .initCloseReopenReport', () => {
spyOn(IssuablesHelper, 'initCloseReopenReport');
new MergeRequest(); // eslint-disable-line no-new
expect(IssuablesHelper.initCloseReopenReport).toHaveBeenCalled();
});
it('calls .initDroplab', () => {
const container = jasmine.createSpyObj('container', ['querySelector']);
const dropdownTrigger = {};
const dropdownList = {};
const button = {};
spyOn(CloseReopenReportToggle.prototype, 'initDroplab');
spyOn(document, 'querySelector').and.returnValue(container);
container.querySelector.and.returnValues(dropdownTrigger, dropdownList, button);
new MergeRequest(); // eslint-disable-line no-new
expect(document.querySelector).toHaveBeenCalledWith('.js-issuable-close-dropdown');
expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-toggle');
expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-menu');
expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-button');
expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled();
});
});
});
}).call(window);
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