Commit e9bc4c66 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'tc-namespace-license-checks--multiple-assignees' into 'master'

Namespace license checks for multiple assignees

Closes #2564

See merge request !1916
parents 251fa4c1 1786dd4c
......@@ -15,6 +15,10 @@ class FilteredSearchManager {
this.tokensContainer = this.container.querySelector('.tokens-container');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
gl.FilteredSearchTokenKeysIssuesEE.init({
multipleAssignees: this.filteredSearchInput.dataset.multipleAssignees,
});
if (this.page === 'issues' || this.page === 'boards') {
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeysIssuesEE;
}
......
......@@ -19,12 +19,18 @@ const weightConditions = [{
}];
class FilteredSearchTokenKeysIssuesEE extends gl.FilteredSearchTokenKeys {
static init(availableFeatures) {
this.availableFeatures = availableFeatures;
}
static get() {
const tokenKeys = Array.from(super.get());
// Enable multiple assignees
const assigneeTokenKey = tokenKeys.find(tk => tk.key === 'assignee');
assigneeTokenKey.type = 'array';
// Enable multiple assignees when available
if (this.availableFeatures && this.availableFeatures.multipleAssignees) {
const assigneeTokenKey = tokenKeys.find(tk => tk.key === 'assignee');
assigneeTokenKey.type = 'array';
}
tokenKeys.push(weightTokenKey);
return tokenKeys;
......
......@@ -33,12 +33,14 @@ export default {
saveAssignees() {
this.loading = true;
function setLoadingFalse() {
this.loading = false;
}
this.mediator.saveAssignees(this.field)
.then(() => {
this.loading = false;
})
.then(setLoadingFalse.bind(this))
.catch(() => {
this.loading = false;
setLoadingFalse();
return new Flash('Error occurred when saving assignees');
});
},
......
module EE
module FormHelper
def issue_assignees_dropdown_options
options = super
if @project.feature_available?(:multiple_issue_assignees)
options[:title] = 'Select assignee(s)'
options[:data][:'dropdown-header'] = 'Assignee(s)'
options[:data].delete(:'max-select')
end
options
end
end
end
module EE
module SearchHelper
def search_filter_input_options(type)
options = super
options[:data][:'multiple-assignees'] = 'true' if search_multiple_assignees?(type)
options
end
private
def search_multiple_assignees?(type)
type == :issues &&
@project.feature_available?(:multiple_issue_assignees)
end
end
end
module FormHelper
prepend ::EE::FormHelper
def form_errors(model)
return unless model.errors.any?
......@@ -16,8 +18,8 @@ module FormHelper
end
end
def issue_dropdown_options(issuable, has_multiple_assignees = true)
options = {
def issue_assignees_dropdown_options
{
toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data',
title: 'Select assignee',
filter: true,
......@@ -27,8 +29,8 @@ module FormHelper
first_user: current_user&.username,
null_user: true,
current_user: true,
project_id: issuable.project.try(:id),
field_name: "#{issuable.class.model_name.param_key}[assignee_ids][]",
project_id: @project.id,
field_name: "issue[assignee_ids][]",
default_label: 'Unassigned',
'max-select': 1,
'dropdown-header': 'Assignee',
......@@ -38,13 +40,5 @@ module FormHelper
current_user_info: current_user.to_json(only: [:id, :name])
}
}
if has_multiple_assignees
options[:title] = 'Select assignee(s)'
options[:data][:'dropdown-header'] = 'Assignee(s)'
options[:data].delete(:'max-select')
end
options
end
end
module SearchHelper
prepend EE::SearchHelper
def search_autocomplete_opts(term)
return unless current_user
......@@ -134,6 +136,18 @@ module SearchHelper
search_path(options)
end
def search_filter_input_options(type)
{
id: "filtered-search-#{type}",
placeholder: 'Search or filter results...',
data: {
'project-id' => @project.id,
'username-params' => @users.to_json(only: [:id, :username]),
'base-endpoint' => project_path(@project)
}
}
end
# Sanitize a HTML field for search display. Most tags are stripped out and the
# maximum length is set to 200 characters.
def search_md_sanitize(object, field)
......
......@@ -105,6 +105,10 @@ module Issuable
def locking_enabled?
title_changed? || description_changed?
end
def allows_multiple_assignees?
false
end
end
module ClassMethods
......
......@@ -5,6 +5,11 @@ module EE
author.support_bot? || super
end
# override
def allows_multiple_assignees?
project.feature_available?(:multiple_issue_assignees)
end
# override
def subscribed_without_subscriptions?(user, *)
# TODO: this really shouldn't be necessary, because the support
......
......@@ -18,6 +18,7 @@ class License < ActiveRecord::Base
MERGE_REQUEST_APPROVERS_FEATURE = 'GitLab_MergeRequestApprovers'.freeze
MERGE_REQUEST_REBASE_FEATURE = 'GitLab_MergeRequestRebase'.freeze
MERGE_REQUEST_SQUASH_FEATURE = 'GitLab_MergeRequestSquash'.freeze
MULTIPLE_ISSUE_ASSIGNEES_FEATURE = 'GitLab_MultipleIssueAssignees'.freeze
OBJECT_STORAGE_FEATURE = 'GitLab_ObjectStorage'.freeze
PUSH_RULES_FEATURE = 'GitLab_PushRules'.freeze
RELATED_ISSUES_FEATURE = 'RelatedIssues'.freeze
......@@ -48,6 +49,7 @@ class License < ActiveRecord::Base
merge_request_approvers: MERGE_REQUEST_APPROVERS_FEATURE,
merge_request_rebase: MERGE_REQUEST_REBASE_FEATURE,
merge_request_squash: MERGE_REQUEST_SQUASH_FEATURE,
multiple_issue_assignees: MULTIPLE_ISSUE_ASSIGNEES_FEATURE,
push_rules: PUSH_RULES_FEATURE
}.freeze
......@@ -70,6 +72,7 @@ class License < ActiveRecord::Base
{ MERGE_REQUEST_APPROVERS_FEATURE => 1 },
{ MERGE_REQUEST_REBASE_FEATURE => 1 },
{ MERGE_REQUEST_SQUASH_FEATURE => 1 },
{ MULTIPLE_ISSUE_ASSIGNEES_FEATURE => 1 },
{ PUSH_RULES_FEATURE => 1 },
{ RELATED_ISSUES_FEATURE => 1 }
].freeze
......@@ -113,6 +116,7 @@ class License < ActiveRecord::Base
{ MERGE_REQUEST_APPROVERS_FEATURE => 1 },
{ MERGE_REQUEST_REBASE_FEATURE => 1 },
{ MERGE_REQUEST_SQUASH_FEATURE => 1 },
{ MULTIPLE_ISSUE_ASSIGNEES_FEATURE => 1 },
{ OBJECT_STORAGE_FEATURE => 1 },
{ PUSH_RULES_FEATURE => 1 },
{ SERVICE_DESK_FEATURE => 1 }
......
......@@ -209,11 +209,19 @@ class MergeRequest < ActiveRecord::Base
}
end
# This method is needed for compatibility with issues to not mess view and other code
# These method are needed for compatibility with issues to not mess view and other code
def assignees
Array(assignee)
end
def assignee_ids
Array(assignee_id)
end
def assignee_ids=(ids)
write_attribute(:assignee_id, ids.last)
end
def assignee_or_author?(user)
author_id == user.id || assignee_id == user.id
end
......
module EE
module QuickActions
module InterpretService
include ::Gitlab::QuickActions::Dsl
desc 'Change assignee(s)'
explanation do
'Change assignee(s)'
end
params '@user1 @user2'
condition do
issuable.allows_multiple_assignees? &&
issuable.persisted? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :reassign do |unassign_param|
@updates[:assignee_ids] = extract_users(unassign_param).map(&:id)
end
desc 'Set weight'
explanation do |weight|
"Sets weight to #{weight}." if weight
end
params ::Issue::WEIGHT_RANGE.to_s.squeeze('.').tr('.', '-')
condition do
issuable.supports_weight? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
parse_params do |weight|
weight.to_i if ::Issue.weight_filter_options.include?(weight.to_i)
end
command :weight do |weight|
@updates[:weight] = weight if weight
end
desc 'Clear weight'
explanation 'Clears weight.'
condition do
issuable.persisted? &&
issuable.supports_weight? &&
issuable.weight? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
command :clear_weight do
@updates[:weight] = nil
end
end
end
end
......@@ -24,6 +24,10 @@ module Issues
def filter_assignee(issuable)
return if params[:assignee_ids].blank?
unless issuable.allows_multiple_assignees?
params[:assignee_ids] = params[:assignee_ids].take(1)
end
assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
if params[:assignee_ids].map(&:to_s) == [IssuableFinder::NONE]
......
module QuickActions
class InterpretService < BaseService
include Gitlab::QuickActions::Dsl
prepend EE::QuickActions::InterpretService
attr_reader :issuable
......@@ -92,13 +93,11 @@ module QuickActions
desc 'Assign'
explanation do |users|
## EE-specific
users = issuable.is_a?(Issue) ? users : users.take(1)
users = issuable.allows_multiple_assignees? ? users : users.take(1)
"Assigns #{users.map(&:to_reference).to_sentence}."
end
params do
## EE-specific
issuable.is_a?(Issue) ? '@user1 @user2' : '@user'
issuable.allows_multiple_assignees? ? '@user1 @user2' : '@user'
end
condition do
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
......@@ -109,60 +108,43 @@ module QuickActions
command :assign do |users|
next if users.empty?
if issuable.is_a?(Issue)
# EE specific. In CE we should replace one assignee with another
@updates[:assignee_ids] = issuable.assignees.pluck(:id) + users.map(&:id)
else
@updates[:assignee_id] = users.last.id
end
@updates[:assignee_ids] =
if issuable.allows_multiple_assignees?
issuable.assignees.pluck(:id) + users.map(&:id)
else
[users.last.id]
end
end
desc do
if issuable.is_a?(Issue)
if issuable.allows_multiple_assignees?
'Remove all or specific assignee(s)'
else
'Remove assignee'
end
end
explanation do
"Removes #{'assignee'.pluralize(issuable.assignees.size)} #{issuable.assignees.map(&:to_reference).to_sentence}"
"Removes #{'assignee'.pluralize(issuable.assignees.size)} #{issuable.assignees.map(&:to_reference).to_sentence}."
end
params do
issuable.is_a?(Issue) ? '@user1 @user2' : ''
issuable.allows_multiple_assignees? ? '@user1 @user2' : ''
end
condition do
issuable.persisted? &&
issuable.assignees.any? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :unassign do |unassign_param = nil|
users = extract_users(unassign_param)
if issuable.is_a?(Issue)
@updates[:assignee_ids] =
if users.any?
issuable.assignees.pluck(:id) - users.map(&:id)
else
[]
end
else
@updates[:assignee_id] = nil
end
parse_params do |unassign_param|
# When multiple users are assigned, all will be unassigned if multiple assignees are no longer allowed
extract_users(unassign_param) if issuable.allows_multiple_assignees?
end
desc 'Change assignee(s)'
explanation do
'Change assignee(s)'
end
params '@user1 @user2'
condition do
issuable.is_a?(Issue) &&
issuable.persisted? &&
issuable.assignees.any? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :reassign do |unassign_param|
@updates[:assignee_ids] = extract_users(unassign_param).map(&:id)
command :unassign do |users = nil|
@updates[:assignee_ids] =
if users&.any?
issuable.assignees.pluck(:id) - users.map(&:id)
else
[]
end
end
desc 'Set milestone'
......@@ -464,34 +446,6 @@ module QuickActions
@updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name)
end
desc 'Set weight'
explanation do |weight|
"Sets weight to #{weight}." if weight
end
params Issue::WEIGHT_RANGE.to_s.squeeze('.').tr('.', '-')
condition do
issuable.supports_weight? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
parse_params do |weight|
weight.to_i if Issue.weight_filter_options.include?(weight.to_i)
end
command :weight do |weight|
@updates[:weight] = weight if weight
end
desc 'Clear weight'
explanation 'Clears weight.'
condition do
issuable.persisted? &&
issuable.supports_weight? &&
issuable.weight? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
command :clear_weight do
@updates[:weight] = nil
end
desc 'Move issue from one column of the board to another'
explanation do |target_list_name|
label = find_label_references(target_list_name).first
......
......@@ -19,10 +19,11 @@
":data-name" => "assignee.name",
":data-username" => "assignee.username" }
.dropdown
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: "button", ref: "assigneeDropdown", data: { toggle: "dropdown", field_name: "issue[assignee_ids][]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", multi_select: "true", dropdown: { header: 'Assignee(s)'} },
- dropdown_options = issue_assignees_dropdown_options
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: { toggle: 'dropdown', field_name: 'issue[assignee_ids][]', first_user: current_user&.username, current_user: 'true', project_id: @project.id, null_user: 'true', multi_select: 'true', 'dropdown-header': dropdown_options[:data][:'dropdown-header'], 'max-select': dropdown_options[:data][:'max-select'] },
":data-issuable-id" => "issue.id",
":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" }
Select assignee(s)
= dropdown_options[:title]
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
= dropdown_title("Assign to")
......
......@@ -27,7 +27,7 @@
.scroll-container
%ul.tokens-container.list-unstyled
%li.input-token
%input.form-control.filtered-search{ id: "filtered-search-#{type.to_s}", placeholder: 'Search or filter results...', data: { 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => project_path(@project) } }
%input.form-control.filtered-search{ search_filter_input_options(type) }
= icon('filter')
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
......
......@@ -37,18 +37,20 @@
- issuable.assignees.each do |assignee|
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username }
- options = { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }
- options = { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: current_user&.username, current_user: true, project_id: @project&.id, author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }
- title = 'Select assignee'
- if issuable.is_a?(Issue)
- unless issuable.assignees.any?
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil
- dropdown_options = issue_assignees_dropdown_options
- title = dropdown_options[:title]
- options[:toggle_class] += ' js-multiselect js-save-user-data'
- data = { field_name: "#{issuable.to_ability_name}[assignee_ids][]" }
- data[:multi_select] = true
- data['dropdown-title'] = title
- data['dropdown-header'] = 'Assignee'
- data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
- data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
- options[:data].merge!(data)
= dropdown_tag(title, options: options)
......@@ -7,5 +7,5 @@
- if issuable.assignees.length === 0
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' }
= dropdown_tag(users_dropdown_label(issuable.assignees), options: issue_dropdown_options(issuable,true))
= dropdown_tag(users_dropdown_label(issuable.assignees), options: issue_assignees_dropdown_options)
= link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}"
......@@ -15,7 +15,7 @@ end
end
# EE-only
%w(license).each do |f|
%w(test_license).each do |f|
require Rails.root.join('spec', 'support', f)
end
......
require 'rails_helper'
describe 'Issue Boards', feature: true, js: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:project) { create(:empty_project, :public) }
let!(:milestone) { create(:milestone, project: project) }
let!(:development) { create(:label, project: project, name: 'Development') }
let!(:stretch) { create(:label, project: project, name: 'Stretch') }
let!(:issue1) { create(:labeled_issue, project: project, assignees: [user], milestone: milestone, labels: [development], relative_position: 2) }
let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, label: development, position: 0) }
let(:card) { find('.board:nth-child(2)').first('.card') }
before do
Timecop.freeze
stub_licensed_features(multiple_issue_assignees: true)
project.team << [user, :master]
project.team.add_developer(user2)
gitlab_sign_in(user)
visit project_board_path(project, board)
wait_for_requests
end
after do
Timecop.return
end
context 'assignee' do
it 'updates the issues assignee' do
click_card(card)
page.within('.assignee') do
click_link 'Edit'
wait_for_requests
page.within('.dropdown-menu-user') do
click_link user.name
wait_for_requests
end
expect(page).to have_content(user.name)
end
expect(card).to have_selector('.avatar')
end
it 'adds multiple assignees' do
click_card(card)
page.within('.assignee') do
click_link 'Edit'
wait_for_requests
page.within('.dropdown-menu-user') do
click_link user.name
click_link user2.name
end
expect(page).to have_content(user.name)
expect(page).to have_content(user2.name)
end
expect(card.all('.avatar').length).to eq(2)
end
it 'removes the assignee' do
card_two = find('.board:nth-child(2)').find('.card:nth-child(2)')
click_card(card_two)
page.within('.assignee') do
click_link 'Edit'
wait_for_requests
page.within('.dropdown-menu-user') do
click_link 'Unassigned'
end
find('.dropdown-menu-toggle').click
wait_for_requests
expect(page).to have_content('No assignee')
end
expect(card_two).not_to have_selector('.avatar')
end
it 'assignees to current user' do
click_card(card)
page.within(find('.assignee')) do
expect(page).to have_content('No assignee')
click_button 'assign yourself'
wait_for_requests
expect(page).to have_content(user.name)
end
expect(card).to have_selector('.avatar')
end
it 'updates assignee dropdown' do
click_card(card)
page.within('.assignee') do
click_link 'Edit'
wait_for_requests
page.within('.dropdown-menu-user') do
click_link user.name
wait_for_requests
end
expect(page).to have_content(user.name)
end
page.within(find('.board:nth-child(2)')) do
find('.card:nth-child(2)').trigger('click')
end
page.within('.assignee') do
click_link 'Edit'
expect(find('.dropdown-menu')).to have_selector('.is-active')
end
end
end
def click_card(card)
page.within(card) do
first('.card-number').click
end
wait_for_sidebar
end
def wait_for_sidebar
# loop until the CSS transition is complete
Timeout.timeout(0.5) do
loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290
end
end
end
......@@ -17,9 +17,9 @@ describe 'Issue Boards', feature: true, js: true do
before do
Timecop.freeze
stub_licensed_features(multiple_issue_assignees: false)
project.team << [user, :master]
project.team.add_developer(user2)
gitlab_sign_in(user)
......@@ -117,26 +117,6 @@ describe 'Issue Boards', feature: true, js: true do
expect(card).to have_selector('.avatar')
end
it 'adds multiple assignees' do
click_card(card)
page.within('.assignee') do
click_link 'Edit'
wait_for_requests
page.within('.dropdown-menu-user') do
click_link user.name
click_link user2.name
end
expect(page).to have_content(user.name)
expect(page).to have_content(user2.name)
end
expect(card.all('.avatar').length).to eq(2)
end
it 'removes the assignee' do
card_two = find('.board:nth-child(2)').find('.card:nth-child(2)')
click_card(card_two)
......@@ -150,8 +130,6 @@ describe 'Issue Boards', feature: true, js: true do
click_link 'Unassigned'
end
find('.dropdown-menu-toggle').click
wait_for_requests
expect(page).to have_content('No assignee')
......
require 'rails_helper'
describe 'New/edit issue (EE)', :feature, :js do
describe 'New/edit issue', :feature, :js do
include GitlabRoutingHelper
include ActionView::Helpers::JavaScriptHelper
include FormHelper
......@@ -16,6 +16,8 @@ describe 'New/edit issue (EE)', :feature, :js do
before do
project.team << [user, :master]
project.team << [user2, :master]
stub_licensed_features(multiple_issue_assignees: true)
gitlab_sign_in(user)
end
......@@ -24,15 +26,15 @@ describe 'New/edit issue (EE)', :feature, :js do
visit new_project_issue_path(project)
end
describe 'shorten users API pagination limit (CE)' do
describe 'shorten users API pagination limit' do
before do
# Using `allow_any_instance_of`/`and_wrap_original`, `original` would
# somehow refer to the very block we defined to _wrap_ that method, instead of
# the original method, resulting in infinite recurison when called.
# This is likely a bug with helper modules included into dynamically generated view classes.
# To work around this, we have to hold on to and call to the original implementation manually.
original_issue_dropdown_options = FormHelper.instance_method(:issue_dropdown_options)
allow_any_instance_of(FormHelper).to receive(:issue_dropdown_options).and_wrap_original do |original, *args|
original_issue_dropdown_options = EE::FormHelper.instance_method(:issue_assignees_dropdown_options)
allow_any_instance_of(EE::FormHelper).to receive(:issue_assignees_dropdown_options).and_wrap_original do |original, *args|
options = original_issue_dropdown_options.bind(original.receiver).call(*args)
options[:data][:per_page] = 2
......@@ -96,5 +98,174 @@ describe 'New/edit issue (EE)', :feature, :js do
expect(find('a', text: 'Assign to me')).to be_visible
end
end
it 'allows user to create new issue' do
fill_in 'issue_title', with: 'title'
fill_in 'issue_description', with: 'title'
expect(find('a', text: 'Assign to me')).to be_visible
click_button 'Unassigned'
wait_for_requests
page.within '.dropdown-menu-user' do
click_link user2.name
end
expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user2.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user2.name
end
expect(find('a', text: 'Assign to me')).to be_visible
click_link 'Assign to me'
assignee_ids = page.all('input[name="issue[assignee_ids][]"]', visible: false)
expect(assignee_ids[0].value).to match(user2.id.to_s)
expect(assignee_ids[1].value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content "#{user2.name} + 1 more"
end
expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
click_button 'Milestone'
page.within '.issue-milestone' do
click_link milestone.title
end
expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
page.within '.js-milestone-select' do
expect(page).to have_content milestone.title
end
click_button 'Labels'
page.within '.dropdown-menu-labels' do
click_link label.title
click_link label2.title
find('.dropdown-menu-close').click
end
page.within '.js-label-select' do
expect(page).to have_content label.title
end
expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
click_button 'Weight'
page.within '.dropdown-menu-weight' do
click_link '1'
end
click_button 'Submit issue'
page.within '.issuable-sidebar' do
page.within '.assignee' do
expect(page).to have_content "2 Assignees"
end
page.within '.milestone' do
expect(page).to have_content milestone.title
end
page.within '.labels' do
expect(page).to have_content label.title
expect(page).to have_content label2.title
end
page.within '.weight' do
expect(page).to have_content '1'
end
end
page.within '.issuable-meta' do
issue = Issue.find_by(title: 'title')
expect(page).to have_text("Issue #{issue.to_reference}")
# compare paths because the host differ in test
expect(find_link(issue.to_reference)[:href]).to end_with(issue_path(issue))
end
end
it 'correctly updates the selected user when changing assignee' do
click_button 'Unassigned'
wait_for_requests
page.within '.dropdown-menu-user' do
click_link user.name
end
expect(find('.js-assignee-search')).to have_content(user.name)
page.within '.dropdown-menu-user' do
click_link user2.name
end
expect(page.all('input[name="issue[assignee_ids][]"]', visible: false)[0].value).to match(user.id.to_s)
expect(page.all('input[name="issue[assignee_ids][]"]', visible: false)[1].value).to match(user2.id.to_s)
expect(page.all('.dropdown-menu-user a.is-active').length).to eq(2)
expect(page.all('.dropdown-menu-user a.is-active')[0].first(:xpath, '..')['data-user-id']).to eq(user.id.to_s)
expect(page.all('.dropdown-menu-user a.is-active')[1].first(:xpath, '..')['data-user-id']).to eq(user2.id.to_s)
end
end
context 'edit issue' do
before do
visit edit_project_issue_path(project, issue)
end
it 'allows user to update issue' do
expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
page.within '.js-user-search' do
expect(page).to have_content user.name
end
page.within '.js-milestone-select' do
expect(page).to have_content milestone.title
end
click_button 'Labels'
page.within '.dropdown-menu-labels' do
click_link label.title
click_link label2.title
end
page.within '.js-label-select' do
expect(page).to have_content label.title
end
expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
click_button 'Save changes'
page.within '.issuable-sidebar' do
page.within '.assignee' do
expect(page).to have_content user.name
end
page.within '.milestone' do
expect(page).to have_content milestone.title
end
page.within '.labels' do
expect(page).to have_content label.title
expect(page).to have_content label2.title
end
end
end
end
def before_for_selector(selector)
js = <<-JS.strip_heredoc
(function(selector) {
var el = document.querySelector(selector);
return window.getComputedStyle(el, '::before').getPropertyValue('content');
})("#{escape_javascript(selector)}")
JS
page.evaluate_script(js)
end
end
......@@ -13,6 +13,8 @@ describe 'New/edit issue', :feature, :js do
let!(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) }
before do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
project.team << [user, :master]
project.team << [user2, :master]
gitlab_sign_in(user)
......@@ -23,15 +25,15 @@ describe 'New/edit issue', :feature, :js do
visit new_project_issue_path(project)
end
xdescribe 'shorten users API pagination limit (CE)' do
describe 'shorten users API pagination limit' do
before do
# Using `allow_any_instance_of`/`and_wrap_original`, `original` would
# somehow refer to the very block we defined to _wrap_ that method, instead of
# the original method, resulting in infinite recurison when called.
# This is likely a bug with helper modules included into dynamically generated view classes.
# To work around this, we have to hold on to and call to the original implementation manually.
original_issue_dropdown_options = FormHelper.instance_method(:issue_dropdown_options)
allow_any_instance_of(FormHelper).to receive(:issue_dropdown_options).and_wrap_original do |original, *args|
original_issue_dropdown_options = EE::FormHelper.instance_method(:issue_assignees_dropdown_options)
allow_any_instance_of(EE::FormHelper).to receive(:issue_assignees_dropdown_options).and_wrap_original do |original, *args|
options = original_issue_dropdown_options.bind(original.receiver).call(*args)
options[:data][:per_page] = 2
......@@ -63,7 +65,7 @@ describe 'New/edit issue', :feature, :js do
end
end
xdescribe 'single assignee (CE)' do
describe 'single assignee' do
before do
click_button 'Unassigned'
......@@ -122,11 +124,10 @@ describe 'New/edit issue', :feature, :js do
click_link 'Assign to me'
assignee_ids = page.all('input[name="issue[assignee_ids][]"]', visible: false)
expect(assignee_ids[0].value).to match(user2.id.to_s)
expect(assignee_ids[1].value).to match(user.id.to_s)
expect(assignee_ids[0].value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content "#{user2.name} + 1 more"
expect(page).to have_content user.name
end
expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
......@@ -152,17 +153,11 @@ describe 'New/edit issue', :feature, :js do
expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
click_button 'Weight'
page.within '.dropdown-menu-weight' do
click_link '1'
end
click_button 'Submit issue'
page.within '.issuable-sidebar' do
page.within '.assignee' do
expect(page).to have_content "2 Assignees"
expect(page).to have_content "Assignee"
end
page.within '.milestone' do
......@@ -173,10 +168,6 @@ describe 'New/edit issue', :feature, :js do
expect(page).to have_content label.title
expect(page).to have_content label2.title
end
page.within '.weight' do
expect(page).to have_content '1'
end
end
page.within '.issuable-meta' do
......@@ -214,18 +205,13 @@ describe 'New/edit issue', :feature, :js do
end
expect(find('.js-assignee-search')).to have_content(user.name)
click_button user.name
page.within '.dropdown-menu-user' do
click_link user2.name
end
expect(page.all('input[name="issue[assignee_ids][]"]', visible: false)[0].value).to match(user.id.to_s)
expect(page.all('input[name="issue[assignee_ids][]"]', visible: false)[1].value).to match(user2.id.to_s)
expect(page.all('.dropdown-menu-user a.is-active').length).to eq(2)
expect(page.all('.dropdown-menu-user a.is-active')[0].first(:xpath, '..')['data-user-id']).to eq(user.id.to_s)
expect(page.all('.dropdown-menu-user a.is-active')[1].first(:xpath, '..')['data-user-id']).to eq(user2.id.to_s)
expect(find('.js-assignee-search')).to have_content(user2.name)
end
it 'description has autocomplete' do
......
......@@ -40,23 +40,11 @@
"additionalProperties": false
}
},
"assignees": {
"type": "array",
"items": {
"type": ["object", "null"],
"required": [
"id",
"name",
"username",
"avatar_url"
],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"username": { "type": "string" },
"avatar_url": { "type": "uri" }
}
}
"assignee": {
"id": { "type": "integer" },
"name": { "type": "string" },
"username": { "type": "string" },
"avatar_url": { "type": "uri" }
},
"assignees": {
"type": "array",
......
{
"type": "array",
"items": {
"type": "object",
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"project_id": { "type": "integer" },
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"milestone": {
"type": "object",
"properties": {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"project_id": { "type": "integer" },
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"due_date": { "type": "date" },
"start_date": { "type": "date" }
},
"additionalProperties": false
},
"assignees": {
"type": "array",
"items": {
"type": ["object", "null"],
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
}
},
"assignee": {
"type": ["object", "null"],
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
},
"author": {
"type": "object",
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
},
"user_notes_count": { "type": "integer" },
"upvotes": { "type": "integer" },
"downvotes": { "type": "integer" },
"due_date": { "type": ["date", "null"] },
"confidential": { "type": "boolean" },
"web_url": { "type": "uri" },
"weight": { "type": ["integer", "null"] }
},
"required": [
"id", "iid", "project_id", "title", "description",
"state", "created_at", "updated_at", "labels",
"milestone", "assignees", "author", "user_notes_count",
"upvotes", "downvotes", "due_date", "confidential",
"web_url", "weight"
],
"additionalProperties": false
}
}
......@@ -77,15 +77,14 @@
"downvotes": { "type": "integer" },
"due_date": { "type": ["date", "null"] },
"confidential": { "type": "boolean" },
"web_url": { "type": "uri" },
"weight": { "type": ["integer", "null"] }
"web_url": { "type": "uri" }
},
"required": [
"id", "iid", "project_id", "title", "description",
"state", "created_at", "updated_at", "labels",
"milestone", "assignees", "author", "user_notes_count",
"upvotes", "downvotes", "due_date", "confidential",
"web_url", "weight"
"web_url"
],
"additionalProperties": false
}
......
......@@ -15,6 +15,9 @@ import '~/filtered_search/filtered_search_token_keys_issues_ee';
let tokenKeys;
beforeEach(() => {
gl.FilteredSearchTokenKeysIssuesEE.init({
multipleAssignees: true,
});
tokenKeys = gl.FilteredSearchTokenKeysIssuesEE.get();
});
......
require 'spec_helper'
describe Issue do
describe '#allows_multiple_assignees?' do
it 'does not allow multiple assignees without license' do
stub_licensed_features(multiple_issue_assignees: false)
issue = build(:issue)
expect(issue.allows_multiple_assignees?).to be_falsey
end
it 'does not allow multiple assignees without license' do
stub_licensed_features(multiple_issue_assignees: true)
issue = build(:issue)
expect(issue.allows_multiple_assignees?).to be_truthy
end
end
describe '#weight' do
[
{ license: true, database: 5, expected: 5 },
......
......@@ -212,9 +212,19 @@ describe License do
end
describe '.features_for_plan' do
it 'returns features for given plan' do
it 'returns features for starter plan' do
expect(described_class.features_for_plan('starter'))
.to include({ 'GitLab_MultipleIssueAssignees' => 1 })
end
it 'returns features for premium plan' do
expect(described_class.features_for_plan('premium'))
.to include({ 'GitLab_MultipleIssueAssignees' => 1, 'GitLab_DeployBoard' => 1, 'GitLab_FileLocks' => 1 })
end
it 'returns features for early adopter plan' do
expect(described_class.features_for_plan('premium'))
.to include({ 'GitLab_DeployBoard' => 1, 'GitLab_FileLocks' => 1 })
.to include({ 'GitLab_DeployBoard' => 1, 'GitLab_FileLocks' => 1 } )
end
it 'returns empty Hash if no features for given plan' do
......
......@@ -106,6 +106,22 @@ describe MergeRequest, models: true do
end
end
describe '#assignee_ids' do
it 'returns an array of the assigned user id' do
subject.assignee_id = 123
expect(subject.assignee_ids).to eq([123])
end
end
describe '#assignee_ids=' do
it 'sets assignee_id to the last id in the array' do
subject.assignee_ids = [123, 456]
expect(subject.assignee_id).to eq(456)
end
end
describe '#assignee_or_author?' do
let(:user) { create(:user) }
......
require 'spec_helper'
describe API::Issues do # rubocop:disable RSpec/FilePath
include EmailHelpers
set(:user) { create(:user) }
set(:project) do
create(:empty_project, :public, creator_id: user.id, namespace: user.namespace)
end
let(:user2) { create(:user) }
set(:author) { create(:author) }
set(:assignee) { create(:assignee) }
let(:issue_title) { 'foo' }
let(:issue_description) { 'closed' }
let!(:issue) do
create :issue,
author: user,
assignees: [user],
project: project,
milestone: milestone,
created_at: generate(:past_time),
updated_at: 1.hour.ago,
title: issue_title,
description: issue_description
end
set(:milestone) { create(:milestone, title: '1.0.0', project: project) }
before(:all) do
project.team << [user, :reporter]
end
describe "GET /issues" do
context "when authenticated" do
it 'matches V4 response schema' do
get api('/issues', user)
expect(response).to have_http_status(200)
expect(response).to match_response_schema('public_api/v4/ee/issues')
end
end
end
describe "POST /projects/:id/issues" do
it 'creates a new project issue' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', weight: 3,
assignee_ids: [user2.id]
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['confidential']).to be_falsy
expect(json_response['weight']).to eq(3)
expect(json_response['assignee']['name']).to eq(user2.name)
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
end
describe 'PUT /projects/:id/issues/:issue_id to update weight' do
it 'updates an issue with no weight' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: 5
expect(response).to have_http_status(200)
expect(json_response['weight']).to eq(5)
end
it 'removes a weight from an issue' do
weighted_issue = create(:issue, project: project, weight: 2)
put api("/projects/#{project.id}/issues/#{weighted_issue.iid}", user), weight: nil
expect(response).to have_http_status(200)
expect(json_response['weight']).to be_nil
end
it 'returns 400 if weight is less than minimum weight' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: -1
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('weight does not have a valid value')
end
it 'returns 400 if weight is more than maximum weight' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: 10
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('weight does not have a valid value')
end
context 'issuable weights unlicensed' do
before do
stub_licensed_features(issue_weights: false)
end
it 'ignores the update' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: 5
expect(response).to have_http_status(200)
expect(json_response['weight']).to be_nil
expect(issue.reload.read_attribute(:weight)).to be_nil
end
end
end
def expect_paginated_array_response(size: nil)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(size) if size
end
end
......@@ -63,6 +63,10 @@ describe API::Issues do
project.team << [guest, :guest]
end
before do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
end
describe "GET /issues" do
context "when unauthenticated" do
it "returns authentication error" do
......@@ -691,7 +695,6 @@ describe API::Issues do
expect(json_response['assignee']).to be_a Hash
expect(json_response['author']).to be_a Hash
expect(json_response['confidential']).to be_falsy
expect(json_response['weight']).to be_nil
end
it "returns a project issue by internal id" do
......@@ -773,6 +776,17 @@ describe API::Issues do
end
end
context 'single assignee restrictions' do
it 'creates a new project issue with no more than one assignee' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', assignee_ids: [user2.id, guest.id]
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['assignees'].count).to eq(1)
end
end
it 'creates a new project issue' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', weight: 3,
......@@ -783,7 +797,6 @@ describe API::Issues do
expect(json_response['description']).to be_nil
expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['confidential']).to be_falsy
expect(json_response['weight']).to eq(3)
expect(json_response['assignee']['name']).to eq(user2.name)
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
......@@ -1113,6 +1126,17 @@ describe API::Issues do
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
context 'single assignee restrictions' do
it 'updates an issue with several assignees but only one has been applied' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
assignee_ids: [user2.id, guest.id]
expect(response).to have_http_status(200)
expect(json_response['assignees'].size).to eq(1)
end
end
end
describe 'PUT /projects/:id/issues/:issue_iid to update labels' do
......@@ -1218,52 +1242,6 @@ describe API::Issues do
end
end
describe 'PUT /projects/:id/issues/:issue_id to update weight' do
it 'updates an issue with no weight' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: 5
expect(response).to have_http_status(200)
expect(json_response['weight']).to eq(5)
end
it 'removes a weight from an issue' do
weighted_issue = create(:issue, project: project, weight: 2)
put api("/projects/#{project.id}/issues/#{weighted_issue.iid}", user), weight: nil
expect(response).to have_http_status(200)
expect(json_response['weight']).to be_nil
end
it 'returns 400 if weight is less than minimum weight' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: -1
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('weight does not have a valid value')
end
it 'returns 400 if weight is more than maximum weight' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: 10
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('weight does not have a valid value')
end
context 'issuable weights unlicensed' do
before do
stub_licensed_features(issue_weights: false)
end
it 'ignores the update' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: 5
expect(response).to have_http_status(200)
expect(json_response['weight']).to be_nil
expect(issue.reload.read_attribute(:weight)).to be_nil
end
end
end
describe "DELETE /projects/:id/issues/:issue_iid" do
it "rejects a non member from deleting an issue" do
delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member)
......
......@@ -11,6 +11,8 @@ describe API::Milestones do
before do
project.team << [user, :developer]
stub_licensed_features(issue_weights: false)
end
describe 'GET /projects/:id/milestones' do
......
require 'spec_helper'
describe QuickActions::InterpretService, services: true do # rubocop:disable RSpec/FilePath
let(:user) { create(:user) }
let(:developer) { create(:user) }
let(:developer2) { create(:user) }
let(:project) { create(:empty_project, :public) }
let(:issue) { create(:issue, project: project) }
let(:service) { described_class.new(project, developer) }
before do
stub_licensed_features(multiple_issue_assignees: true)
project.add_developer(developer)
end
describe '#execute' do
context 'assign command' do
let(:content) { "/assign @#{developer.username}" }
context 'Issue' do
it 'fetches assignees and populates them if content contains /assign' do
issue.assignees << user
_, updates = service.execute(content, issue)
expect(updates[:assignee_ids]).to match_array([developer.id, user.id])
end
context 'assign command with multiple assignees' do
let(:content) { "/assign @#{developer.username} @#{developer2.username}" }
before do
project.add_developer(developer2)
end
it 'fetches assignee and populates assignee_ids if content contains /assign' do
_, updates = service.execute(content, issue)
expect(updates[:assignee_ids]).to match_array([developer.id, developer2.id])
end
end
end
end
context 'unassign command' do
let(:content) { '/unassign' }
context 'Issue' do
it 'unassigns user if content contains /unassign @user' do
issue.update(assignee_ids: [developer.id, developer2.id])
_, updates = service.execute("/unassign @#{developer2.username}", issue)
expect(updates).to eq(assignee_ids: [developer.id])
end
it 'unassigns both users if content contains /unassign @user @user1' do
user = create(:user)
issue.update(assignee_ids: [developer.id, developer2.id, user.id])
_, updates = service.execute("/unassign @#{developer2.username} @#{developer.username}", issue)
expect(updates).to eq(assignee_ids: [user.id])
end
it 'unassigns all the users if content contains /unassign' do
issue.update(assignee_ids: [developer.id, developer2.id])
_, updates = service.execute('/unassign', issue)
expect(updates[:assignee_ids]).to be_empty
end
end
end
context 'reassign command' do
let(:content) { "/reassign @#{user.username}" }
context 'Merge Request' do
let(:merge_request) { create(:merge_request, source_project: project) }
it 'does not recognize /reassign @user' do
_, updates = service.execute(content, merge_request)
expect(updates).to be_empty
end
end
context 'Issue' do
let(:content) { "/reassign @#{user.username}" }
before do
issue.update(assignee_ids: [developer.id])
end
context 'unlicensed' do
before do
stub_licensed_features(multiple_issue_assignees: false)
end
it 'does not recognize /reassign @user' do
_, updates = service.execute(content, issue)
expect(updates).to be_empty
end
end
it 'reassigns user if content contains /reassign @user' do
_, updates = service.execute("/reassign @#{user.username}", issue)
expect(updates).to eq(assignee_ids: [user.id])
end
end
end
end
end
......@@ -11,6 +11,8 @@ describe QuickActions::InterpretService, services: true do
let(:note) { build(:note, commit_id: merge_request.diff_head_sha) }
before do
stub_licensed_features(multiple_issue_assignees: false)
project.team << [developer, :developer]
end
......@@ -399,24 +401,18 @@ describe QuickActions::InterpretService, services: true do
let(:content) { "/assign @#{developer.username}" }
context 'Issue' do
it 'fetches assignees and populates them if content contains /assign' do
user = create(:user)
issue.assignees << user
it 'fetches assignee and populates assignee_ids if content contains /assign' do
_, updates = service.execute(content, issue)
expect(updates[:assignee_ids]).to match_array([developer.id, user.id])
expect(updates[:assignee_ids]).to match_array([developer.id])
end
end
context 'Merge Request' do
it 'fetches assignee and populates assignee_id if content contains /assign' do
user = create(:user)
merge_request.update(assignee: user)
it 'fetches assignee and populates assignee_ids if content contains /assign' do
_, updates = service.execute(content, merge_request)
expect(updates).to eq(assignee_id: developer.id)
expect(updates).to eq(assignee_ids: [developer.id])
end
end
end
......@@ -429,18 +425,18 @@ describe QuickActions::InterpretService, services: true do
end
context 'Issue' do
it 'fetches assignee and populates assignee_id if content contains /assign' do
it 'fetches assignee and populates assignee_ids if content contains /assign' do
_, updates = service.execute(content, issue)
expect(updates[:assignee_ids]).to match_array([developer.id, developer2.id])
expect(updates[:assignee_ids]).to match_array([developer.id])
end
end
context 'Merge Request' do
it 'fetches assignee and populates assignee_id if content contains /assign' do
it 'fetches assignee and populates assignee_ids if content contains /assign' do
_, updates = service.execute(content, merge_request)
expect(updates).to eq(assignee_id: developer.id)
expect(updates).to eq(assignee_ids: [developer.id])
end
end
end
......@@ -459,55 +455,20 @@ describe QuickActions::InterpretService, services: true do
let(:content) { '/unassign' }
context 'Issue' do
it 'unassigns user if content contains /unassign @user' do
issue.update(assignee_ids: [developer.id, developer2.id])
_, updates = service.execute("/unassign @#{developer2.username}", issue)
expect(updates).to eq(assignee_ids: [developer.id])
end
it 'unassigns both users if content contains /unassign @user @user1' do
user = create(:user)
issue.update(assignee_ids: [developer.id, developer2.id, user.id])
_, updates = service.execute("/unassign @#{developer2.username} @#{developer.username}", issue)
expect(updates).to eq(assignee_ids: [user.id])
end
it 'unassigns all the users if content contains /unassign' do
issue.update(assignee_ids: [developer.id, developer2.id])
_, updates = service.execute('/unassign', issue)
expect(updates[:assignee_ids]).to be_empty
end
end
context 'reassign command' do
let(:content) { '/reassign' }
context 'Issue' do
it 'reassigns user if content contains /reassign @user' do
user = create(:user)
issue.update(assignee_ids: [developer.id, developer2.id])
_, updates = service.execute("/reassign @#{user.username}", issue)
it 'populates assignee_ids: [] if content contains /unassign' do
issue.update(assignee_ids: [developer.id])
_, updates = service.execute(content, issue)
expect(updates).to eq(assignee_ids: [user.id])
end
expect(updates).to eq(assignee_ids: [])
end
end
context 'Merge Request' do
it 'populates assignee_id: nil if content contains /unassign' do
merge_request.update(assignee_id: developer.id)
it 'populates assignee_ids: [] if content contains /unassign' do
merge_request.update(assignee_ids: [developer.id])
_, updates = service.execute(content, merge_request)
expect(updates).to eq(assignee_id: nil)
expect(updates).to eq(assignee_ids: [])
end
end
end
......@@ -1000,7 +961,7 @@ describe QuickActions::InterpretService, services: true do
it 'includes current assignee reference' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(["Removes assignee #{developer.to_reference}"])
expect(explanations).to eq(["Removes assignee @#{developer.username}."])
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