Skip to content
Snippets Groups Projects
Commit 387c4b2c authored by Valery Sizov's avatar Valery Sizov
Browse files

Backport of multiple_assignees_feature [ci skip]

parent 68c12e15
No related branches found
No related tags found
No related merge requests found
Showing
with 579 additions and 150 deletions
Loading
Loading
@@ -19,8 +19,8 @@
return label;
};
})(this),
clicked: function(item, $el, e) {
return e.preventDefault();
clicked: function(options) {
return options.e.preventDefault();
},
id: function(obj, el) {
return $(el).data("id");
Loading
Loading
This diff is collapsed.
Loading
Loading
@@ -93,3 +93,14 @@
align-self: center;
}
}
.avatar-counter {
background-color: $gray-darkest;
color: $white-light;
border: 1px solid $border-color;
border-radius: 1em;
font-family: $regular_font;
font-size: 9px;
line-height: 16px;
text-align: center;
}
Loading
Loading
@@ -251,11 +251,9 @@
}
 
.dropdown-header {
color: $gl-text-color;
color: $gl-text-color-secondary;
font-size: 13px;
font-weight: 600;
line-height: 22px;
text-transform: capitalize;
padding: 0 16px;
}
 
Loading
Loading
@@ -337,8 +335,8 @@
.dropdown-menu-user {
.avatar {
float: left;
width: 30px;
height: 30px;
width: 2 * $gl-padding;
height: 2 * $gl-padding;
margin: 0 10px 0 0;
}
}
Loading
Loading
@@ -381,6 +379,7 @@
.dropdown-menu-selectable {
a {
padding-left: 26px;
position: relative;
 
&.is-indeterminate,
&.is-active {
Loading
Loading
@@ -406,6 +405,9 @@
 
&.is-active::before {
content: "\f00c";
position: absolute;
top: 50%;
transform: translateY(-50%);
}
}
}
Loading
Loading
Loading
Loading
@@ -255,6 +255,7 @@ ul.controls {
.avatar-inline {
margin-left: 0;
margin-right: 0;
margin-bottom: 0;
}
}
}
Loading
Loading
Loading
Loading
@@ -207,8 +207,13 @@
margin-bottom: 5px;
}
 
&.is-active {
&.is-active,
&.is-active .card-assignee:hover a {
background-color: $row-hover;
&:first-child:not(:only-child) {
box-shadow: -10px 0 10px 1px $row-hover;
}
}
 
.label {
Loading
Loading
@@ -224,7 +229,7 @@
}
 
.card-title {
margin: 0;
margin: 0 30px 0 0;
font-size: 1em;
line-height: inherit;
 
Loading
Loading
@@ -240,10 +245,69 @@
min-height: 20px;
 
.card-assignee {
margin-left: auto;
margin-right: 5px;
padding-left: 10px;
display: flex;
justify-content: flex-end;
position: absolute;
right: 15px;
height: 20px;
width: 20px;
.avatar-counter {
display: none;
vertical-align: middle;
min-width: 20px;
line-height: 19px;
height: 20px;
padding-left: 2px;
padding-right: 2px;
border-radius: 2em;
}
img {
vertical-align: top;
}
a {
position: relative;
margin-left: -15px;
}
a:nth-child(1) {
z-index: 3;
}
a:nth-child(2) {
z-index: 2;
}
a:nth-child(3) {
z-index: 1;
}
a:nth-child(4) {
display: none;
}
&:hover {
.avatar-counter {
display: inline-block;
}
a {
position: static;
background-color: $white-light;
transition: background-color 0s;
margin-left: auto;
&:nth-child(4) {
display: block;
}
&:first-child:not(:only-child) {
box-shadow: -10px 0 10px 1px $white-light;
}
}
}
}
 
.avatar {
Loading
Loading
Loading
Loading
@@ -570,14 +570,7 @@
 
.diff-comments-more-count,
.diff-notes-collapse {
background-color: $gray-darkest;
color: $white-light;
border: 1px solid $white-light;
border-radius: 1em;
font-family: $regular_font;
font-size: 9px;
line-height: 17px;
text-align: center;
@extend .avatar-counter;
}
 
.diff-notes-collapse {
Loading
Loading
Loading
Loading
@@ -95,10 +95,15 @@
}
 
.right-sidebar {
a {
a,
.btn-link {
color: inherit;
}
 
.btn-link {
outline: none;
}
.issuable-header-text {
margin-top: 7px;
}
Loading
Loading
@@ -215,6 +220,10 @@
}
}
 
.assign-yourself .btn-link {
padding-left: 0;
}
.light {
font-weight: normal;
}
Loading
Loading
@@ -239,6 +248,10 @@
margin-left: 0;
}
 
.assignee .user-list .avatar {
margin: 0;
}
.username {
display: block;
margin-top: 4px;
Loading
Loading
@@ -301,6 +314,10 @@
margin-top: 0;
}
 
.sidebar-avatar-counter {
padding-top: 2px;
}
.todo-undone {
color: $gl-link-color;
}
Loading
Loading
@@ -309,10 +326,15 @@
display: none;
}
 
.avatar:hover {
.avatar:hover,
.avatar-counter:hover {
border-color: $issuable-sidebar-color;
}
 
.avatar-counter:hover {
color: $issuable-sidebar-color;
}
.btn-clipboard {
border: none;
color: $issuable-sidebar-color;
Loading
Loading
@@ -322,6 +344,17 @@
color: $gl-text-color;
}
}
&.multiple-users {
display: flex;
justify-content: center;
}
}
.sidebar-avatar-counter {
width: 24px;
height: 24px;
border-radius: 12px;
}
 
.sidebar-collapsed-user {
Loading
Loading
@@ -332,6 +365,37 @@
.issuable-header-btn {
display: none;
}
.multiple-users {
height: 24px;
margin-bottom: 17px;
margin-top: 4px;
padding-bottom: 4px;
.btn-link {
padding: 0;
border: 0;
.avatar {
margin: 0;
}
}
.btn-link:first-child {
position: absolute;
left: 10px;
z-index: 1;
}
.btn-link:last-child {
position: absolute;
right: 10px;
&:hover {
text-decoration: none;
}
}
}
}
 
a {
Loading
Loading
@@ -380,17 +444,21 @@
}
 
.participants-list {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin: -5px;
}
 
.user-list {
display: flex;
flex-wrap: wrap;
}
.participants-author {
display: inline-block;
flex-basis: 14%;
padding: 5px;
 
&:nth-of-type(7n) {
padding-right: 0;
}
.author_link {
display: block;
}
Loading
Loading
@@ -400,13 +468,39 @@
}
}
 
.participants-more {
.user-item {
display: inline-block;
padding: 5px;
flex-basis: 20%;
.user-link {
display: inline-block;
}
}
.participants-more,
.user-list-more {
margin-top: 5px;
margin-left: 5px;
 
a {
a,
.btn-link {
color: $gl-text-color-secondary;
}
.btn-link {
outline: none;
padding: 0;
}
.btn-link:hover {
@extend a:hover;
text-decoration: none;
}
.btn-link:focus {
text-decoration: none;
}
}
 
.issuable-form-padding-top {
Loading
Loading
@@ -499,6 +593,19 @@
}
}
 
.issuable-list li,
.issue-info-container .controls {
.avatar-counter {
display: inline-block;
vertical-align: middle;
min-width: 16px;
line-height: 14px;
height: 16px;
padding-left: 2px;
padding-right: 2px;
}
}
.time_tracker {
padding-bottom: 0;
border-bottom: 0;
Loading
Loading
Loading
Loading
@@ -66,6 +66,7 @@ module IssuableActions
:milestone_id,
:state_event,
:subscription_event,
assignee_ids: [],
label_ids: [],
add_label_ids: [],
remove_label_ids: []
Loading
Loading
Loading
Loading
@@ -43,7 +43,7 @@ module IssuableCollections
end
 
def issues_collection
issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace)
issues_finder.execute.preload(:project, :author, :assignees, :labels, :milestone, project: :namespace)
end
 
def merge_requests_collection
Loading
Loading
Loading
Loading
@@ -82,7 +82,7 @@ module Projects
labels: true,
only: [:id, :iid, :title, :confidential, :due_date, :relative_position],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
milestone: { only: [:id, :title] }
},
user: current_user
Loading
Loading
Loading
Loading
@@ -67,7 +67,7 @@ class Projects::IssuesController < Projects::ApplicationController
 
def new
params[:issue] ||= ActionController::Parameters.new(
assignee_id: ""
assignee_ids: ""
)
build_params = issue_params.merge(
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
Loading
Loading
@@ -150,7 +150,7 @@ class Projects::IssuesController < Projects::ApplicationController
if @issue.valid?
render json: @issue.to_json(methods: [:task_status, :task_status_short],
include: { milestone: {},
assignee: { only: [:name, :username], methods: [:avatar_url] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
labels: { methods: :text_color } })
else
render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
Loading
Loading
@@ -275,7 +275,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params
params.require(:issue).permit(
:title, :assignee_id, :position, :description, :confidential,
:milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: []
:milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: [], assignee_ids: [],
)
end
 
Loading
Loading
Loading
Loading
@@ -231,7 +231,7 @@ class IssuableFinder
when 'created-by-me', 'authored'
items.where(author_id: current_user.id)
when 'assigned-to-me'
items.where(assignee_id: current_user.id)
items.assigned_to(current_user)
else
items
end
Loading
Loading
Loading
Loading
@@ -26,17 +26,28 @@ class IssuesFinder < IssuableFinder
IssuesFinder.not_restricted_by_confidentiality(current_user)
end
 
def by_assignee(items)
if assignee
items.assigned_to(assignee)
elsif no_assignee?
items.unassigned
elsif assignee_id? || assignee_username? # assignee not found
items.none
else
items
end
end
def self.not_restricted_by_confidentiality(user)
return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
return Issue.where('issues.confidential IS NOT TRUE') if user.blank?
 
return Issue.all if user.admin?
 
Issue.where('
issues.confidential IS NULL
OR issues.confidential IS FALSE
issues.confidential IS NOT TRUE
OR (issues.confidential = TRUE
AND (issues.author_id = :user_id
OR issues.assignee_id = :user_id
OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id)
OR issues.project_id IN(:project_ids)))',
user_id: user.id,
project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
Loading
Loading
Loading
Loading
@@ -15,4 +15,37 @@ module FormHelper
end
end
end
def issue_dropdown_options(issuable, has_multiple_assignees = true)
options = {
toggle_class: 'js-user-search js-assignee-search',
title: 'Select assignee',
filter: true,
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee',
placeholder: 'Search users',
data: {
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][]",
default_label: 'Assignee',
'max-select': 1,
'dropdown-header': 'Assignee',
}
}
if has_multiple_assignees
options[:toggle_class] += ' js-multiselect js-save-user-data'
options[:title] = 'Select assignee(s)'
options[:data][:multi_select] = true
options[:data][:'input-meta'] = 'name'
options[:data][:'always-show-selectbox'] = true
options[:data][:current_user_info] = current_user.to_json(only: [:id, :name])
options[:data][:'dropdown-header'] = 'Assignee(s)'
options[:data].delete(:'max-select')
end
options
end
end
Loading
Loading
@@ -63,6 +63,16 @@ module IssuablesHelper
end
end
 
def users_dropdown_label(selected_users)
if selected_users.length == 0
"Unassigned"
elsif selected_users.length == 1
selected_users[0].name
else
"#{selected_users[0].name} + #{selected_users.length - 1} more"
end
end
def user_dropdown_label(user_id, default_label)
return default_label if user_id.nil?
return "Unassigned" if user_id == "0"
Loading
Loading
Loading
Loading
@@ -11,10 +11,12 @@ module Emails
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
 
def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id)
def reassigned_issue_email(recipient_id, issue_id, previous_assignee_ids, updated_by_user_id)
setup_issue_mail(issue_id, recipient_id)
 
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
@previous_assignees = []
@previous_assignees = User.where(id: previous_assignee_ids) if previous_assignee_ids.any?
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
 
Loading
Loading
Loading
Loading
@@ -26,7 +26,6 @@ module Issuable
cache_markdown_field :description, issuable_state_filter_enabled: true
 
belongs_to :author, class_name: "User"
belongs_to :assignee, class_name: "User"
belongs_to :updated_by, class_name: "User"
belongs_to :milestone
has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do
Loading
Loading
@@ -65,11 +64,8 @@ module Issuable
validates :title, presence: true, length: { maximum: 255 }
 
scope :authored, ->(user) { where(author_id: user) }
scope :assigned_to, ->(u) { where(assignee_id: u.id)}
scope :recent, -> { reorder(id: :desc) }
scope :order_position_asc, -> { reorder(position: :asc) }
scope :assigned, -> { where("assignee_id IS NOT NULL") }
scope :unassigned, -> { where("assignee_id IS NULL") }
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
Loading
Loading
@@ -92,7 +88,6 @@ module Issuable
attr_mentionable :description
 
participant :author
participant :assignee
participant :notes_with_associations
 
strip_attributes :title
Loading
Loading
@@ -102,13 +97,6 @@ module Issuable
after_save :update_assignee_cache_counts, if: :assignee_id_changed?
after_save :record_metrics, unless: :imported?
 
def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignees(if they exist)
previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was
previous_assignee&.update_cache_counts
assignee&.update_cache_counts
end
# We want to use optimistic lock for cases when only title or description are involved
# http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
def locking_enabled?
Loading
Loading
@@ -237,10 +225,6 @@ module Issuable
today? && created_at == updated_at
end
 
def is_being_reassigned?
assignee_id_changed?
end
def open?
opened? || reopened?
end
Loading
Loading
@@ -269,7 +253,11 @@ module Issuable
# DEPRECATED
repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
}
hook_data[:assignee] = assignee.hook_attrs if assignee
if self.is_a?(Issue)
hook_data[:assignees] = assignees.map(&:hook_attrs) if assignees.any?
else
hook_data[:assignee] = assignee.hook_attrs if assignee
end
 
hook_data
end
Loading
Loading
@@ -331,11 +319,6 @@ module Issuable
false
end
 
def assignee_or_author?(user)
# We're comparing IDs here so we don't need to load any associations.
author_id == user.id || assignee_id == user.id
end
def record_metrics
metrics = self.metrics || create_metrics
metrics.record!
Loading
Loading
Loading
Loading
@@ -40,7 +40,7 @@ module Milestoneish
def issues_visible_to_user(user)
memoize_per_user(user, :issues_visible_to_user) do
IssuesFinder.new(user, issues_finder_params)
.execute.where(milestone_id: milestoneish_ids)
.execute.includes(:assignees).where(milestone_id: milestoneish_ids)
end
end
 
Loading
Loading
Loading
Loading
@@ -36,7 +36,7 @@ class GlobalMilestone
closed = count_by_state(milestones_by_state_and_title, 'closed')
all = milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count
 
{
{
opened: opened,
closed: closed,
all: all
Loading
Loading
@@ -86,7 +86,7 @@ class GlobalMilestone
end
 
def issues
@issues ||= Issue.of_milestones(milestoneish_ids).includes(:project, :assignee, :labels)
@issues ||= Issue.of_milestones(milestoneish_ids).includes(:project, :assignees, :labels)
end
 
def merge_requests
Loading
Loading
@@ -94,7 +94,7 @@ class GlobalMilestone
end
 
def participants
@participants ||= milestones.includes(:participants).map(&:participants).flatten.compact.uniq
@participants ||= milestones.map(&:participants).flatten.uniq
end
 
def labels
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment