Commit c8b2b3f7 authored by Douwe Maan's avatar Douwe Maan Committed by Rémy Coutable

Merge branch 'feature/group-level-labels' into 'master'

Add group level labels.

* `LabelsFinder`
* `Gitlab::Gfm::ReferenceRewriter`
* `Banzai::Filter::LabelReferenceFilter`

We'll be adding more feature that allow you to do cross-project management of issues.

loses #19997

See merge request !6425
Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parent 52a38f90
...@@ -17,6 +17,7 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -17,6 +17,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Add `/projects/visible` API endpoint (Ben Boeckel) - Add `/projects/visible` API endpoint (Ben Boeckel)
- Fix centering of custom header logos (Ashley Dumaine) - Fix centering of custom header logos (Ashley Dumaine)
- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
- Add group level labels. (!6425)
- Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun) - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun)
- Cancelled pipelines could be retried. !6927 - Cancelled pipelines could be retried. !6927
- Updating verbiage on git basics to be more intuitive - Updating verbiage on git basics to be more intuitive
......
...@@ -168,6 +168,8 @@ ...@@ -168,6 +168,8 @@
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new ShortcutsBlob(true); new ShortcutsBlob(true);
break; break;
case 'groups:labels:new':
case 'groups:labels:edit':
case 'projects:labels:new': case 'projects:labels:new':
case 'projects:labels:edit': case 'projects:labels:edit':
new Labels(); new Labels();
......
...@@ -69,6 +69,20 @@ ...@@ -69,6 +69,20 @@
} }
} }
.label-type {
display: block;
margin-bottom: 10px;
margin-left: 50px;
@media (min-width: $screen-sm-min) {
display: inline-block;
width: 100px;
margin-left: 10px;
margin-bottom: 0;
vertical-align: middle;
}
}
.label-description { .label-description {
display: block; display: block;
margin-bottom: 10px; margin-bottom: 10px;
...@@ -209,6 +223,13 @@ ...@@ -209,6 +223,13 @@
} }
.label-subscribe-button { .label-subscribe-button {
.label-subscribe-button-icon {
&[disabled] {
opacity: 0.5;
pointer-events: none;
}
}
.label-subscribe-button-loading { .label-subscribe-button-loading {
display: none; display: none;
} }
......
...@@ -2,6 +2,7 @@ module IssuableActions ...@@ -2,6 +2,7 @@ module IssuableActions
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
before_action :labels, only: [:show, :new, :edit]
before_action :authorize_destroy_issuable!, only: :destroy before_action :authorize_destroy_issuable!, only: :destroy
before_action :authorize_admin_issuable!, only: :bulk_update before_action :authorize_admin_issuable!, only: :bulk_update
end end
...@@ -25,6 +26,10 @@ module IssuableActions ...@@ -25,6 +26,10 @@ module IssuableActions
private private
def labels
@labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
end
def authorize_destroy_issuable! def authorize_destroy_issuable!
unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable) unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable)
return access_denied! return access_denied!
......
class Dashboard::LabelsController < Dashboard::ApplicationController class Dashboard::LabelsController < Dashboard::ApplicationController
def index def index
labels = Label.where(project_id: projects).select(:id, :title, :color).uniq(:title) labels = LabelsFinder.new(current_user).execute
respond_to do |format| respond_to do |format|
format.json { render json: labels } format.json { render json: labels.as_json(only: [:id, :title, :color]) }
end end
end end
end end
class Groups::LabelsController < Groups::ApplicationController
before_action :label, only: [:edit, :update, :destroy]
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy]
before_action :save_previous_label_path, only: [:edit]
respond_to :html
def index
respond_to do |format|
format.html do
@labels = @group.labels.page(params[:page])
end
format.json do
available_labels = LabelsFinder.new(current_user, group_id: @group.id).execute
render json: available_labels.as_json(only: [:id, :title, :color])
end
end
end
def new
@label = @group.labels.new
@previous_labels_path = previous_labels_path
end
def create
@label = @group.labels.create(label_params)
if @label.valid?
redirect_to group_labels_path(@group)
else
render :new
end
end
def edit
@previous_labels_path = previous_labels_path
end
def update
if @label.update_attributes(label_params)
redirect_back_or_group_labels_path
else
render :edit
end
end
def destroy
@label.destroy
respond_to do |format|
format.html do
redirect_to group_labels_path(@group), notice: 'Label was removed'
end
format.js
end
end
protected
def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @group)
end
def authorize_read_labels!
return render_404 unless can?(current_user, :read_label, @group)
end
def label
@label ||= @group.labels.find(params[:id])
end
def label_params
params.require(:label).permit(:title, :description, :color)
end
def redirect_back_or_group_labels_path(options = {})
redirect_to previous_labels_path, options
end
def previous_labels_path
session.fetch(:previous_labels_path, fallback_path)
end
def fallback_path
group_labels_path(@group)
end
def save_previous_label_path
session[:previous_labels_path] = URI(request.referer || '').path
end
end
...@@ -72,10 +72,10 @@ module Projects ...@@ -72,10 +72,10 @@ module Projects
def serialize_as_json(resource) def serialize_as_json(resource)
resource.as_json( resource.as_json(
labels: true,
only: [:iid, :title, :confidential], only: [:iid, :title, :confidential],
include: { include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, assignee: { only: [:id, :name, :username], methods: [:avatar_url] }
labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
}) })
end end
end end
......
...@@ -76,9 +76,8 @@ module Projects ...@@ -76,9 +76,8 @@ module Projects
resource.as_json( resource.as_json(
only: [:id, :list_type, :position], only: [:id, :list_type, :position],
methods: [:title], methods: [:title],
include: { label: true
label: { only: [:id, :title, :description, :color, :priority] } )
})
end end
end end
end end
......
...@@ -26,7 +26,9 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -26,7 +26,9 @@ class Projects::IssuesController < Projects::ApplicationController
@issues = issues_collection @issues = issues_collection
@issues = @issues.page(params[:page]) @issues = @issues.page(params[:page])
@labels = @project.labels.where(title: params[:label_name]) if params[:label_name].present?
@labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
end
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -3,21 +3,22 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -3,21 +3,22 @@ class Projects::LabelsController < Projects::ApplicationController
before_action :module_enabled before_action :module_enabled
before_action :label, only: [:edit, :update, :destroy] before_action :label, only: [:edit, :update, :destroy]
before_action :find_labels, only: [:index, :set_priorities, :remove_priority]
before_action :authorize_read_label! before_action :authorize_read_label!
before_action :authorize_admin_labels!, only: [ before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update,
:new, :create, :edit, :update, :generate, :destroy, :remove_priority, :set_priorities :generate, :destroy, :remove_priority,
] :set_priorities]
respond_to :js, :html respond_to :js, :html
def index def index
@labels = @project.labels.unprioritized.page(params[:page]) @prioritized_labels = @available_labels.prioritized(@project)
@prioritized_labels = @project.labels.prioritized @labels = @available_labels.unprioritized(@project).page(params[:page])
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
render json: @project.labels render json: @available_labels.as_json(only: [:id, :title, :color])
end end
end end
end end
...@@ -36,7 +37,7 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -36,7 +37,7 @@ class Projects::LabelsController < Projects::ApplicationController
end end
else else
respond_to do |format| respond_to do |format|
format.html { render 'new' } format.html { render :new }
format.json { render json: { message: @label.errors.messages }, status: 400 } format.json { render json: { message: @label.errors.messages }, status: 400 }
end end
end end
...@@ -49,7 +50,7 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -49,7 +50,7 @@ class Projects::LabelsController < Projects::ApplicationController
if @label.update_attributes(label_params) if @label.update_attributes(label_params)
redirect_to namespace_project_labels_path(@project.namespace, @project) redirect_to namespace_project_labels_path(@project.namespace, @project)
else else
render 'edit' render :edit
end end
end end
...@@ -68,6 +69,7 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -68,6 +69,7 @@ class Projects::LabelsController < Projects::ApplicationController
def destroy def destroy
@label.destroy @label.destroy
@labels = find_labels
respond_to do |format| respond_to do |format|
format.html do format.html do
...@@ -80,20 +82,24 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -80,20 +82,24 @@ class Projects::LabelsController < Projects::ApplicationController
def remove_priority def remove_priority
respond_to do |format| respond_to do |format|
if label.update_attribute(:priority, nil) label = @available_labels.find(params[:id])
if label.unprioritize!(project)
format.json { render json: label } format.json { render json: label }
else else
message = label.errors.full_messages.uniq.join('. ') format.json { head :unprocessable_entity }
format.json { render json: { message: message }, status: :unprocessable_entity }
end end
end end
end end
def set_priorities def set_priorities
Label.transaction do Label.transaction do
params[:label_ids].each_with_index do |label_id, index| available_labels_ids = @available_labels.where(id: params[:label_ids]).pluck(:id)
label = @project.labels.find_by_id(label_id) label_ids = params[:label_ids].select { |id| available_labels_ids.include?(id.to_i) }
label.update_attribute(:priority, index) if label
label_ids.each_with_index do |label_id, index|
label = @available_labels.find(label_id)
label.prioritize!(project, index)
end end
end end
...@@ -119,6 +125,10 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -119,6 +125,10 @@ class Projects::LabelsController < Projects::ApplicationController
end end
alias_method :subscribable_resource, :label alias_method :subscribable_resource, :label
def find_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute.includes(:priorities)
end
def authorize_admin_labels! def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @project) return render_404 unless can?(current_user, :admin_label, @project)
end end
......
...@@ -40,7 +40,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -40,7 +40,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(:target_project) @merge_requests = @merge_requests.preload(:target_project)
@labels = @project.labels.where(title: params[:label_name]) if params[:label_name].present?
labels_params = { project_id: @project.id, title: params[:label_name] }
@labels = LabelsFinder.new(current_user, labels_params).execute
end
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -569,6 +572,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -569,6 +572,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@note_counts = Note.where(commit_id: @commits.map(&:id)). @note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count group(:commit_id).count
@labels = LabelsFinder.new(current_user, project_id: @project.id).execute
define_pipelines_vars define_pipelines_vars
end end
......
...@@ -124,14 +124,11 @@ class IssuableFinder ...@@ -124,14 +124,11 @@ class IssuableFinder
def labels def labels
return @labels if defined?(@labels) return @labels if defined?(@labels)
@labels =
if labels? && !filter_by_no_label? if labels? && !filter_by_no_label?
@labels = Label.where(title: label_names) LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute
if projects
@labels = @labels.where(project: projects)
end
else else
@labels = Label.none Label.none
end end
end end
...@@ -274,8 +271,10 @@ class IssuableFinder ...@@ -274,8 +271,10 @@ class IssuableFinder
items = items.without_label items = items.without_label
else else
items = items.with_label(label_names, params[:sort]) items = items.with_label(label_names, params[:sort])
if projects if projects
items = items.where(labels: { project_id: projects }) label_ids = LabelsFinder.new(current_user, project_ids: projects).execute.select(:id)
items = items.where(labels: { id: label_ids })
end end
end end
end end
......
class LabelsFinder < UnionFinder
def initialize(current_user, params = {})
@current_user = current_user
@params = params
end
def execute(authorized_only: true)
@authorized_only = authorized_only
items = find_union(label_ids, Label)
items = with_title(items)
sort(items)
end
private
attr_reader :current_user, :params, :authorized_only
def label_ids
label_ids = []
if project
label_ids << project.group.labels if project.group.present?
label_ids << project.labels
else
label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id))
end
label_ids
end
def sort(items)
items.reorder(title: :asc)
end
def with_title(items)
items = items.where(title: title) if title
items
end
def group_id
params[:group_id].presence
end
def project_id
params[:project_id].presence
end
def projects_ids
params[:project_ids].presence
end
def title
params[:title].presence || params[:name].presence
end
def project
return @project if defined?(@project)
if project_id
@project = find_project
else
@project = nil
end
@project
end
def find_project
if authorized_only
available_projects.find_by(id: project_id)
else
Project.find_by(id: project_id)
end
end
def projects
return @projects if defined?(@projects)
@projects = authorized_only ? available_projects : Project.all
@projects = @projects.in_namespace(group_id) if group_id
@projects = @projects.where(id: projects_ids) if projects_ids
@projects = @projects.reorder(nil)
@projects
end
def available_projects
@available_projects ||= ProjectsFinder.new.execute(current_user)
end
end
...@@ -4,9 +4,8 @@ module LabelsHelper ...@@ -4,9 +4,8 @@ module LabelsHelper
# Link to a Label # Link to a Label
# #
# label - Label object to link to # label - Label object to link to
# project - Project object which will be used as the context for the label's # subject - Project/Group object which will be used as the context for the
# link. If omitted, defaults to `@project`, or the label's own # label's link. If omitted, defaults to the label's own group/project.
# project.
# type - The type of item the link will point to (:issue or # type - The type of item the link will point to (:issue or
# :merge_request). If omitted, defaults to :issue. # :merge_request). If omitted, defaults to :issue.
# block - An optional block that will be passed to `link_to`, forming the # block - An optional block that will be passed to `link_to`, forming the
...@@ -15,15 +14,14 @@ module LabelsHelper ...@@ -15,15 +14,14 @@ module LabelsHelper
# #
# Examples: # Examples:
# #
# # Allow the generated link to use the label's own project # # Allow the generated link to use the label's own subject
# link_to_label(label) # link_to_label(label)
# #
# # Force the generated link to use @project # # Force the generated link to use a provided group
# @project = Project.first # link_to_label(label, subject: Group.last)
# link_to_label(label)
# #
# # Force the generated link to use a provided project # # Force the generated link to use a provided project
# link_to_label(label, project: Project.last) # link_to_label(label, subject: Project.last)
# #
# # Force the generated link to point to merge requests instead of issues # # Force the generated link to point to merge requests instead of issues
# link_to_label(label, type: :merge_request) # link_to_label(label, type: :merge_request)
...@@ -32,9 +30,8 @@ module LabelsHelper ...@@ -32,9 +30,8 @@ module LabelsHelper
# link_to_label(label) { "My Custom Label Text" } # link_to_label(label) { "My Custom Label Text" }
# #
# Returns a String # Returns a String
def link_to_label(label, project: nil, type: :issue, tooltip: true, css_class: nil, &block) def link_to_label(label, subject: nil, type: :issue, tooltip: true, css_class: nil, &block)
project ||= @project || label.project link = label_filter_path(subject || label.subject, label, type: type)
link = label_filter_path(project, label, type: type)
if block_given? if block_given?
link_to link, class: css_class, &block link_to link, class: css_class, &block
...@@ -43,15 +40,40 @@ module LabelsHelper ...@@ -43,15 +40,40 @@ module LabelsHelper
end end
end end
def label_filter_path(project, label, type: issue) def label_filter_path(subject, label, type: :issue)
case subject
when Group
send("#{type.to_s.pluralize}_group_path",
subject,
label_name: [label.name])
when Project
send("namespace_project_#{type.to_s.pluralize}_path", send("namespace_project_#{type.to_s.pluralize}_path",
project.namespace, subject.namespace,
project, subject,
label_name: [label.name]) label_name: [label.name])
end end
end
def project_label_names def edit_label_path(label)
@project.labels.pluck(:title) case label
when GroupLabel then edit_group_label_path(label.group, label)
when ProjectLabel then edit_namespace_project_label_path(label.project.namespace, label.project, label)
end
end
def destroy_label_path(label)
case label
when GroupLabel then group_label_path(label.group, label)
when ProjectLabel then namespace_project_label_path(label.project.namespace, label.project, label)
end
end
def toggle_subscription_data(label)
return unless label.is_a?(ProjectLabel)
{
url: toggle_subscription_namespace_project_label_path(label.project.namespace, label.project, label)
}
end end
def render_colored_label(label, label_suffix = '', tooltip: true) def render_colored_label(label, label_suffix = '', tooltip: true)
...@@ -68,8 +90,8 @@ module LabelsHelper ...@@ -68,8 +90,8 @@ module LabelsHelper
span.html_safe span.html_safe
end end
def render_colored_cross_project_label(label, tooltip: true) def render_colored_cross_project_label(label, source_project = nil, tooltip: true)
label_suffix = label.project.name_with_namespace label_suffix = source_project ? source_project.name_with_namespace : label.project.name_with_namespace
label_suffix = " <i>in #{escape_once(label_suffix)}</i>" label_suffix = " <i>in #{escape_once(label_suffix)}</i>"
render_colored_label(label, label_suffix, tooltip: tooltip) render_colored_label(label, label_suffix, tooltip: tooltip)
end end
...@@ -115,7 +137,10 @@ module LabelsHelper ...@@ -115,7 +137,10 @@ module LabelsHelper
end end
def labels_filter_path def labels_filter_path
return group_labels_path(@group, :json) if @group
project = @target_project || @project project = @target_project || @project
if project if project
namespace_project_labels_path(project.namespace, project, :json) namespace_project_labels_path(project.namespace, project, :json)
else else
...@@ -124,11 +149,24 @@ module LabelsHelper ...@@ -124,11 +149,24 @@ module LabelsHelper
end end
def label_subscription_status(label) def label_subscription_status(label)
label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed' case label
when GroupLabel then 'Subscribing to group labels is currently not supported.'
when ProjectLabel then label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
end
end end
def label_subscription_toggle_button_text(label) def label_subscription_toggle_button_text(label)
label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe' case label
when GroupLabel then 'Subscribing to group labels is currently not supported.'
when ProjectLabel then label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
end
end
def label_deletion_confirm_text(label)
case label
when GroupLabel then 'Remove this label? This will affect all projects within the group. Are you sure?'
when ProjectLabel then 'Remove this label? Are you sure?'
end
end end
# Required for Banzai::Filter::LabelReferenceFilter # Required for Banzai::Filter::LabelReferenceFilter
......
...@@ -145,8 +145,14 @@ module Issuable ...@@ -145,8 +145,14 @@ module Issuable
end end
def order_labels_priority(excluded_labels: []) def order_labels_priority(excluded_labels: [])
condition_field = "#{table_name}.id" params = {
highest_priority = highest_label_priority(name, condition_field, excluded_labels: excluded_labels).to_sql target_type: name,
target_column: "#{table_name}.id",
project_column: "#{table_name}.#{project_foreign_key}",
excluded_labels: excluded_labels
}
highest_priority = highest_label_priority(params).to_sql
select("#{table_name}.*, (#{highest_priority}) AS highest_priority"). select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
group(arel_table[:id]). group(arel_table[:id]).
...@@ -230,18 +236,6 @@ module Issuable ...@@ -230,18 +236,6 @@ module Issuable
labels.order('title ASC').pluck(:title) labels.order('title ASC').pluck(:title)
end end
def remove_labels
labels.delete_all
end
def add_labels_by_names(label_names)
label_names.each do |label_name|
label = project.labels.create_with(color: Label::DEFAULT_COLOR).
find_or_create_by(title: label_name.strip)
self.labels << label
end
end
# Convert this Issuable class name to a format usable by Ability definitions # Convert this Issuable class name to a format usable by Ability definitions
# #
# Examples: # Examples:
......
...@@ -38,11 +38,13 @@ module Sortable ...@@ -38,11 +38,13 @@ module Sortable
private private
def highest_label_priority(object_types, condition_field, excluded_labels: []) def highest_label_priority(target_type:, target_column:, project_column:, excluded_labels: [])
query = Label.select(Label.arel_table[:priority].minimum). query = Label.select(LabelPriority.arel_table[:priority].minimum).
left_join_priorities.
joins(:label_links). joins(:label_links).
where(label_links: { target_type: object_types }). where("label_priorities.project_id = #{project_column}").
where("label_links.target_id = #{condition_field}"). where(label_links: { target_type: target_type }).
where("label_links.target_id = #{target_column}").
reorder(nil) reorder(nil)
query.where.not(title: excluded_labels) if excluded_labels.present? query.where.not(title: excluded_labels) if excluded_labels.present?
......
...@@ -19,6 +19,7 @@ class Group < Namespace ...@@ -19,6 +19,7 @@ class Group < Namespace
has_many :project_group_links, dependent: :destroy has_many :project_group_links, dependent: :destroy
has_many :shared_projects, through: :project_group_links, source: :project has_many :shared_projects, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source has_many :notification_settings, dependent: :destroy, as: :source
has_many :labels, class_name: 'GroupLabel'
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :visibility_level_allowed_by_projects validate :visibility_level_allowed_by_projects
......
class GroupLabel < Label
belongs_to :group
validates :group, presence: true
alias_attribute :subject, :group
def to_reference(source_project = nil, target_project = nil, format: :id)
super(source_project, target_project, format: format)
end
end
...@@ -138,6 +138,10 @@ class Issue < ActiveRecord::Base ...@@ -138,6 +138,10 @@ class Issue < ActiveRecord::Base
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end end
def self.project_foreign_key
'project_id'
end
def self.sort(method, excluded_labels: []) def self.sort(method, excluded_labels: [])
case method.to_s case method.to_s
when 'due_date_asc' then order_due_date_asc when 'due_date_asc' then order_due_date_asc
...@@ -274,4 +278,16 @@ class Issue < ActiveRecord::Base ...@@ -274,4 +278,16 @@ class Issue < ActiveRecord::Base
def check_for_spam? def check_for_spam?
project.public? project.public?
end end
def as_json(options = {})
super(options).tap do |json|
if options.has_key?(:labels)
json[:labels] = labels.as_json(
project: project,
only: [:id, :title, :description, :color],
methods: [:text_color]
)
end
end
end
end end
...@@ -15,34 +15,49 @@ class Label < ActiveRecord::Base ...@@ -15,34 +15,49 @@ class Label < ActiveRecord::Base
default_value_for :color, DEFAULT_COLOR default_value_for :color, DEFAULT_COLOR
belongs_to :project
has_many :lists, dependent: :destroy has_many :lists, dependent: :destroy
has_many :priorities, class_name: 'LabelPriority'
has_many :label_links, dependent: :destroy has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue' has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest' has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
validates :color, color: true, allow_blank: false validates :color, color: true, allow_blank: false
validates :project, presence: true, unless: Proc.new { |service| service.template? }
# Don't allow ',' for label titles # Don't allow ',' for label titles
validates :title, validates :title, presence: true, format: { with: /\A[^,]+\z/ }
presence: true, validates :title, uniqueness: { scope: [:group_id, :project_id] }
format: { with: /\A[^,]+\z/ },
uniqueness: { scope: :project_id }
before_save :nullify_priority
default_scope { order(title: :asc) } default_scope { order(title: :asc) }
scope :templates, -> { where(template: true) } scope :templates, -> { where(template: true) }
scope :with_title, ->(title) { where(title: title) }
def self.prioritized def self.prioritized(project)
where.not(priority: nil).reorder(:priority, :title) joins(:priorities)
.where(label_priorities: { project_id: project })
.reorder('label_priorities.priority ASC, labels.title ASC')
end end
def self.unprioritized def self.unprioritized(project)
where(priority: nil) labels = Label.arel_table
priorities = LabelPriority.arel_table
label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin).
on(labels[:id].eq(priorities[:label_id]).and(priorities[:project_id].eq(project.id))).
join_sources
joins(label_priorities).where(priorities[:priority].eq(nil))
end
def self.left_join_priorities
labels = Label.arel_table
priorities = LabelPriority.arel_table
label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin).
on(labels[:id].eq(priorities[:label_id])).
join_sources
joins(label_priorities)
end end
alias_attribute :name, :title alias_attribute :name, :title
...@@ -77,6 +92,44 @@ class Label < ActiveRecord::Base ...@@ -77,6 +92,44 @@ class Label < ActiveRecord::Base
nil nil
end end
def open_issues_count(user = nil, project = nil)
issues_count(user, project_id: project.try(:id) || project_id, state: 'opened')
end
def closed_issues_count(user = nil, project = nil)
issues_count(user, project_id: project.try(:id) || project_id, state: 'closed')
end
def open_merge_requests_count(user = nil, project = nil)
merge_requests_count(user, project_id: project.try(:id) || project_id, state: 'opened')
end
def prioritize!(project, value)
label_priority = priorities.find_or_initialize_by(project_id: project.id)
label_priority.priority = value
label_priority.save!
end
def unprioritize!(project)
priorities.where(project: project).delete_all
end
def priority(project)
priorities.find_by(project: project).try(:priority)
end
def template?
template
end
def text_color
LabelsHelper.text_color_for_bg(self.color)
end
def title=(value)
write_attribute(:title, sanitize_title(value)) if value.present?
end
## ##
# Returns the String necessary to reference this Label in Markdown # Returns the String necessary to reference this Label in Markdown
# #
...@@ -86,47 +139,45 @@ class Label < ActiveRecord::Base ...@@ -86,47 +139,45 @@ class Label < ActiveRecord::Base
# #
# Label.first.to_reference # => "~1" # Label.first.to_reference # => "~1"
# Label.first.to_reference(format: :name) # => "~\"bug\"" # Label.first.to_reference(format: :name) # => "~\"bug\""
# Label.first.to_reference(project) # => "gitlab-org/gitlab-ce~1" # Label.first.to_reference(project1, project2) # => "gitlab-org/gitlab-ce~1"
# #
# Returns a String # Returns a String
# #
def to_reference(from_project = nil, format: :id) def to_reference(source_project = nil, target_project = nil, format: :id)
format_reference = label_format_reference(format) format_reference = label_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}" reference = "#{self.class.reference_prefix}#{format_reference}"
if cross_project_reference?(from_project) if cross_project_reference?(source_project, target_project)
project.to_reference + reference source_project.to_reference + reference
else else
reference reference
end end
end end
def open_issues_count(user = nil) def as_json(options = {})
issues.visible_to_user(user).opened.count super(options).tap do |json|
json[:priority] = priority(options[:project]) if options.has_key?(:project)
end end
def closed_issues_count(user = nil)
issues.visible_to_user(user).closed.count
end end
def open_merge_requests_count private
merge_requests.opened.count
end
def template? def cross_project_reference?(source_project, target_project)
template source_project && target_project && source_project != target_project
end end
def text_color def issues_count(user, params = {})
LabelsHelper::text_color_for_bg(self.color) IssuesFinder.new(user, params.reverse_merge(label_name: title, scope: 'all'))
.execute
.count
end end
def title=(value) def merge_requests_count(user, params = {})
write_attribute(:title, sanitize_title(value)) if value.present? MergeRequestsFinder.new(user, params.reverse_merge(label_name: title, scope: 'all'))
.execute
.count
end end
private
def label_format_reference(format = :id) def label_format_reference(format = :id)
raise StandardError, 'Unknown format' unless [:id, :name].include?(format) raise StandardError, 'Unknown format' unless [:id, :name].include?(format)
...@@ -137,10 +188,6 @@ class Label < ActiveRecord::Base ...@@ -137,10 +188,6 @@ class Label < ActiveRecord::Base
end end
end end
def nullify_priority
self.priority = nil if priority.blank?
end
def sanitize_title(value) def sanitize_title(value)
CGI.unescapeHTML(Sanitize.clean(value.to_s)) CGI.unescapeHTML(Sanitize.clean(value.to_s))
end end
......
class LabelPriority < ActiveRecord::Base
belongs_to :project
belongs_to :label
validates :project, :label, :priority, presence: true
validates :label_id, uniqueness: { scope: :project_id }
validates :priority, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
end
...@@ -26,6 +26,17 @@ class List < ActiveRecord::Base ...@@ -26,6 +26,17 @@ class List < ActiveRecord::Base
label? ? label.name : list_type.humanize label? ? label.name : list_type.humanize
end end
def as_json(options = {})
super(options).tap do |json|
if options.has_key?(:label)
json[:label] = label.as_json(
project: board.project,
only: [:id, :title, :description, :color]
)
end
end
end
private private
def can_be_destroyed def can_be_destroyed
......
...@@ -137,6 +137,10 @@ class MergeRequest < ActiveRecord::Base ...@@ -137,6 +137,10 @@ class MergeRequest < ActiveRecord::Base
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end end
def self.project_foreign_key
'target_project_id'
end
# Returns all the merge requests from an ActiveRecord:Relation. # Returns all the merge requests from an ActiveRecord:Relation.
# #
# This method uses a UNION as it usually operates on the result of # This method uses a UNION as it usually operates on the result of
......
...@@ -107,7 +107,7 @@ class Project < ActiveRecord::Base ...@@ -107,7 +107,7 @@ class Project < ActiveRecord::Base
# Merge requests from source project should be kept when source project was removed # Merge requests from source project should be kept when source project was removed
has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest
has_many :issues, dependent: :destroy has_many :issues, dependent: :destroy
has_many :labels, dependent: :destroy has_many :labels, dependent: :destroy, class_name: 'ProjectLabel'
has_many :services, dependent: :destroy has_many :services, dependent: :destroy
has_many :events, dependent: :destroy has_many :events, dependent: :destroy
has_many :milestones, dependent: :destroy has_many :milestones, dependent: :destroy
...@@ -388,6 +388,10 @@ class Project < ActiveRecord::Base ...@@ -388,6 +388,10 @@ class Project < ActiveRecord::Base
Project.count Project.count
end end
end end
def group_ids
joins(:namespace).where(namespaces: { type: 'Group' }).pluck(:namespace_id)
end
end end
def lfs_enabled? def lfs_enabled?
...@@ -729,10 +733,8 @@ class Project < ActiveRecord::Base ...@@ -729,10 +733,8 @@ class Project < ActiveRecord::Base
def create_labels def create_labels
Label.templates.each do |label| Label.templates.each do |label|
label = label.dup params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
label.template = nil Labels::FindOrCreateService.new(owner, self, params).execute
label.project_id = self.id
label.save
end end
end end
......
class ProjectLabel < Label
MAX_NUMBER_OF_PRIORITIES = 1
belongs_to :project
validates :project, presence: true
validate :permitted_numbers_of_priorities
validate :title_must_not_exist_at_group_level
delegate :group, to: :project, allow_nil: true
alias_attribute :subject, :project
def to_reference(target_project = nil, format: :id)
super(project, target_project, format: format)
end
private
def title_must_not_exist_at_group_level
return unless group.present? && title_changed?
if group.labels.with_title(self.title).exists?
errors.add(:title, :label_already_exists_at_group_level, group: group.name)
end
end
def permitted_numbers_of_priorities
if priorities && priorities.size > MAX_NUMBER_OF_PRIORITIES
errors.add(:priorities, 'Number of permitted priorities exceeded')
end
end
end
...@@ -52,7 +52,13 @@ class Todo < ActiveRecord::Base ...@@ -52,7 +52,13 @@ class Todo < ActiveRecord::Base
# Todos with highest priority first then oldest todos # Todos with highest priority first then oldest todos
# Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue" # Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue"
def order_by_labels_priority def order_by_labels_priority
highest_priority = highest_label_priority(["Issue", "MergeRequest"], "todos.target_id").to_sql params = {
target_type: ['Issue', 'MergeRequest'],
target_column: "todos.target_id",
project_column: "todos.project_id"
}
highest_priority = highest_label_priority(params).to_sql
select("#{table_name}.*, (#{highest_priority}) AS highest_priority"). select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')). order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')).
......
class GroupLabelPolicy < BasePolicy
def rules
delegate! @subject.group
end
end
...@@ -19,6 +19,7 @@ class GroupPolicy < BasePolicy ...@@ -19,6 +19,7 @@ class GroupPolicy < BasePolicy
if master if master
can! :create_projects can! :create_projects
can! :admin_milestones can! :admin_milestones
can! :admin_label
end end
# Only group owner and administrators can admin group # Only group owner and administrators can admin group
......
class ProjectLabelPolicy < BasePolicy
def rules
delegate! @subject.project
end
end
...@@ -3,7 +3,7 @@ module Boards ...@@ -3,7 +3,7 @@ module Boards
class CreateService < BaseService class CreateService < BaseService
def execute(board) def execute(board)
List.transaction do List.transaction do
label = project.labels.find(params[:label_id]) label = available_labels.find(params[:label_id])
position = next_position(board) position = next_position(board)
create_list(board, label, position) create_list(board, label, position)
...@@ -12,6 +12,10 @@ module Boards ...@@ -12,6 +12,10 @@ module Boards
private private
def available_labels
LabelsFinder.new(current_user, project_id: project.id).execute
end
def next_position(board) def next_position(board)
max_position = board.lists.movable.maximum(:position) max_position = board.lists.movable.maximum(:position)
max_position.nil? ? 0 : max_position.succ max_position.nil? ? 0 : max_position.succ
......
...@@ -19,8 +19,7 @@ module Boards ...@@ -19,8 +19,7 @@ module Boards
end end
def find_or_create_label(params) def find_or_create_label(params)
project.labels.create_with(color: params[:color]) ::Labels::FindOrCreateService.new(current_user, project, params).execute
.find_or_create_by(name: params[:name])
end end
def label_params def label_params
......
...@@ -80,17 +80,18 @@ class IssuableBaseService < BaseService ...@@ -80,17 +80,18 @@ class IssuableBaseService < BaseService
def filter_labels_in_param(key) def filter_labels_in_param(key)
return if params[key].to_a.empty? return if params[key].to_a.empty?
params[key] = project.labels.where(id: params[key]).pluck(:id) params[key] = available_labels.where(id: params[key]).pluck(:id)
end end
def find_or_create_label_ids def find_or_create_label_ids
labels = params.delete(:labels) labels = params.delete(:labels)
return unless labels return unless labels
params[:label_ids] = labels.split(",").map do |label_name| params[:label_ids] = labels.split(',').map do |label_name|
project.labels.create_with(color: Label::DEFAULT_COLOR) service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip)
.find_or_create_by(title: label_name.strip) label = service.execute
.id
label.id
end end
end end
...@@ -111,6 +112,10 @@ class IssuableBaseService < BaseService ...@@ -111,6 +112,10 @@ class IssuableBaseService < BaseService
new_label_ids new_label_ids
end end
def available_labels
LabelsFinder.new(current_user, project_id: @project.id).execute
end
def merge_slash_commands_into_params!(issuable) def merge_slash_commands_into_params!(issuable)
description, command_params = description, command_params =
SlashCommands::InterpretService.new(project, current_user). SlashCommands::InterpretService.new(project, current_user).
......
...@@ -52,8 +52,12 @@ module Issues ...@@ -52,8 +52,12 @@ module Issues
end end
def cloneable_label_ids def cloneable_label_ids
@new_project.labels params = {
.where(title: @old_issue.labels.pluck(:title)).pluck(:id) project_id: @new_project.id,
title: @old_issue.labels.pluck(:title)
}
LabelsFinder.new(current_user, params).execute.pluck(:id)
end end
def cloneable_milestone_id def cloneable_milestone_id
......
module Labels
class FindOrCreateService
def initialize(current_user, project, params = {})
@current_user = current_user
@group = project.group
@project = project
@params = params.dup
end
def execute
find_or_create_label
end
private
attr_reader :current_user, :group, :project, :params
def available_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: project.id).execute
end
def find_or_create_label
new_label = available_labels.find_by(title: title)
new_label ||= project.labels.create(params)
new_label
end
def title
params[:title] || params[:name]
end
end
end
# Labels::TransferService class
#
# User for recreate the missing group labels at project level
#
module Labels
class TransferService
def initialize(current_user, old_group, project)
@current_user = current_user
@old_group = old_group
@project = project
end
def execute
return unless old_group.present?
Label.transaction do
labels_to_transfer.find_each do |label|
new_label_id = find_or_create_label!(label)
next if new_label_id == label.id
update_label_links(group_labels_applied_to_issues, old_label_id: label.id, new_label_id: new_label_id)
update_label_links(group_labels_applied_to_merge_requests, old_label_id: label.id, new_label_id: new_label_id)
update_label_priorities(old_label_id: label.id, new_label_id: new_label_id)
end
end
end
private
attr_reader :current_user, :old_group, :project
def labels_to_transfer
label_ids = []
label_ids << group_labels_applied_to_issues.select(:id)
label_ids << group_labels_applied_to_merge_requests.select(:id)
union = Gitlab::SQL::Union.new(label_ids)
Label.where("labels.id IN (#{union.to_sql})").reorder(nil).uniq
end
def group_labels_applied_to_issues
Label.joins(:issues).
where(
issues: { project_id: project.id },
labels: { type: 'GroupLabel', group_id: old_group.id }
)
end
def group_labels_applied_to_merge_requests
Label.joins(:merge_requests).
where(
merge_requests: { target_project_id: project.id },
labels: { type: 'GroupLabel', group_id: old_group.id }
)
end
def find_or_create_label!(label)
params = label.attributes.slice('title', 'description', 'color')
new_label = FindOrCreateService.new(current_user, project, params).execute
new_label.id
end
def update_label_links(labels, old_label_id:, new_label_id:)
LabelLink.joins(:label).
merge(labels).
where(label_id: old_label_id).
update_all(label_id: new_label_id)
end
def update_label_priorities(old_label_id:, new_label_id:)
LabelPriority.where(project_id: project.id, label_id: old_label_id).
update_all(label_id: new_label_id)
end
end
end
...@@ -13,7 +13,7 @@ module Projects ...@@ -13,7 +13,7 @@ module Projects
end end
def labels def labels
@project.labels.select([:title, :color]) LabelsFinder.new(current_user, project_id: project.id).execute.select([:title, :color])
end end
def commands(noteable, type) def commands(noteable, type)
......
...@@ -28,6 +28,7 @@ module Projects ...@@ -28,6 +28,7 @@ module Projects
Project.transaction do Project.transaction do
old_path = project.path_with_namespace old_path = project.path_with_namespace
old_namespace = project.namespace old_namespace = project.namespace
old_group = project.group
new_path = File.join(new_namespace.try(:path) || '', project.path) new_path = File.join(new_namespace.try(:path) || '', project.path)
if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present? if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present?
...@@ -57,6 +58,9 @@ module Projects ...@@ -57,6 +58,9 @@ module Projects
# Move wiki repo also if present # Move wiki repo also if present
gitlab_shell.mv_repository(project.repository_storage_path, "#{old_path}.wiki", "#{new_path}.wiki") gitlab_shell.mv_repository(project.repository_storage_path, "#{old_path}.wiki", "#{new_path}.wiki")
# Move missing group labels to project
Labels::TransferService.new(current_user, old_group, project).execute
# clear project cached events # clear project cached events
project.reset_events_cache project.reset_events_cache
......
...@@ -116,8 +116,10 @@ module SlashCommands ...@@ -116,8 +116,10 @@ module SlashCommands
desc 'Add label(s)' desc 'Add label(s)'
params '~label1 ~"label 2"' params '~label1 ~"label 2"'
condition do condition do
available_labels = LabelsFinder.new(current_user, project_id: project.id).execute
current_user.can?(:"admin_#{issuable.to_ability_name}", project) && current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
project.labels.any? available_labels.any?
end end
command :label do |labels_param| command :label do |labels_param|
label_ids = find_label_ids(labels_param) label_ids = find_label_ids(labels_param)
...@@ -248,7 +250,7 @@ module SlashCommands ...@@ -248,7 +250,7 @@ module SlashCommands
def find_label_ids(labels_param) def find_label_ids(labels_param)
label_ids_by_reference = extract_references(labels_param, :label).map(&:id) label_ids_by_reference = extract_references(labels_param, :label).map(&:id)
labels_ids_by_name = @project.labels.where(name: labels_param.split).select(:id) labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id)
label_ids_by_reference | labels_ids_by_name label_ids_by_reference | labels_ids_by_name
end end
......
- if @group.labels.empty?
$('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
- page_title 'Edit', @label.name, 'Labels'
%h3.page-title
Edit Label
%hr
= render 'shared/labels/form', url: group_label_path(@group, @label), back_path: @previous_labels_path
- page_title 'Labels'
.top-area.adjust
.nav-text
Labels can be applied to issues and merge requests. Group labels are available for any project within the group.
.nav-controls
- if can?(current_user, :admin_label, @group)
= link_to new_group_label_path(@group), class: "btn btn-new" do
New label
.labels
.other-labels
- if @labels.present?
%ul.content-list.manage-labels-list.js-other-labels
= render partial: 'shared/label', collection: @labels, as: :label
= paginate @labels, theme: 'gitlab'
- else
.nothing-here-block
No labels created yet.
- page_title 'New Label'
- header_title group_title(@group, 'Labels', group_labels_path(@group))
%h3.page-title
New Label
%hr
= render 'shared/labels/form', url: group_labels_path, back_path: @previous_labels_path
...@@ -13,6 +13,10 @@ ...@@ -13,6 +13,10 @@
= link_to activity_group_path(@group), title: 'Activity' do = link_to activity_group_path(@group), title: 'Activity' do
%span %span
Activity Activity
= nav_link(controller: [:group, :labels]) do
= link_to group_labels_path(@group), title: 'Labels' do
%span
Labels
= nav_link(controller: [:group, :milestones]) do = nav_link(controller: [:group, :milestones]) do
= link_to group_milestones_path(@group), title: 'Milestones' do = link_to group_milestones_path(@group), title: 'Milestones' do
%span %span
......
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
- if issue.labels.any? - if issue.labels.any?
&nbsp; &nbsp;
- issue.labels.each do |label| - issue.labels.each do |label|
= link_to_label(label, project: issue.project) = link_to_label(label, subject: issue.project)
- if issue.tasks? - if issue.tasks?
&nbsp; &nbsp;
%span.task-status %span.task-status
......
- if @project.labels.size == 0 - if @labels.empty?
$('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000) $('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
...@@ -6,4 +6,4 @@ ...@@ -6,4 +6,4 @@
%h3.page-title %h3.page-title
Edit Label Edit Label
%hr %hr
= render 'form' = render 'shared/labels/form', url: namespace_project_label_path(@project.namespace.becomes(Namespace), @project, @label), back_path: namespace_project_labels_path(@project.namespace, @project)
...@@ -16,21 +16,22 @@ ...@@ -16,21 +16,22 @@
.labels .labels
- if can?(current_user, :admin_label, @project) - if can?(current_user, :admin_label, @project)
-# Only show it in the first page -# Only show it in the first page
- hide = @project.labels.empty? || (params[:page].present? && params[:page] != '1') - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
.prioritized-labels{ class: ('hide' if hide) } .prioritized-labels{ class: ('hide' if hide) }
%h5 Prioritized Labels %h5 Prioritized Labels
%ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) } %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) }
%p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet %p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet
- if @prioritized_labels.present? - if @prioritized_labels.present?
= render @prioritized_labels = render partial: 'shared/label', collection: @prioritized_labels, as: :label
.other-labels .other-labels
- if can?(current_user, :admin_label, @project) - if can?(current_user, :admin_label, @project)
%h5{ class: ('hide' if hide) } Other Labels %h5{ class: ('hide' if hide) } Other Labels
- if @labels.present?
%ul.content-list.manage-labels-list.js-other-labels %ul.content-list.manage-labels-list.js-other-labels
= render @labels - if @labels.present?
= render partial: 'shared/label', collection: @labels, as: :label
= paginate @labels, theme: 'gitlab' = paginate @labels, theme: 'gitlab'
- else - if @labels.blank?
.nothing-here-block .nothing-here-block
- if can?(current_user, :admin_label, @project) - if can?(current_user, :admin_label, @project)
Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}. Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}.
......
...@@ -6,4 +6,4 @@ ...@@ -6,4 +6,4 @@
%h3.page-title %h3.page-title
New Label New Label
%hr %hr
= render 'form' = render 'shared/labels/form', url: namespace_project_labels_path(@project.namespace.becomes(Namespace), @project), back_path: namespace_project_labels_path(@project.namespace, @project)
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
- if merge_request.labels.any? - if merge_request.labels.any?
&nbsp; &nbsp;
- merge_request.labels.each do |label| - merge_request.labels.each do |label|
= link_to_label(label, project: merge_request.project, type: 'merge_request') = link_to_label(label, subject: merge_request.project, type: :merge_request)
- if merge_request.tasks? - if merge_request.tasks?
&nbsp; &nbsp;
%span.task-status %span.task-status
......
- label_css_id = dom_id(label) - label_css_id = dom_id(label)
- open_issues_count = label.open_issues_count(current_user, @project)
- open_merge_requests_count = label.open_merge_requests_count(current_user, @project)
%li{id: label_css_id, data: { id: label.id } } %li{id: label_css_id, data: { id: label.id } }
= render "shared/label_row", label: label = render "shared/label_row", label: label
...@@ -9,42 +12,42 @@ ...@@ -9,42 +12,42 @@
.dropdown-menu.dropdown-menu-align-right .dropdown-menu.dropdown-menu-align-right
%ul %ul
%li %li
= link_to_label(label, type: :merge_request) do = link_to_label(label, subject: @project, type: :merge_request) do
= pluralize label.open_merge_requests_count, 'merge request' = pluralize open_merge_requests_count, 'merge request'
%li %li
= link_to_label(label) do = link_to_label(label, subject: @project) do
= pluralize label.open_issues_count(current_user), 'open issue' = pluralize open_issues_count, 'open issue'
- if current_user - if current_user
%li.label-subscription{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } %li.label-subscription{ data: toggle_subscription_data(label) }
%a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } } %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } }
%span= label_subscription_toggle_button_text(label) %span= label_subscription_toggle_button_text(label)
- if can? current_user, :admin_label, @project - if can?(current_user, :admin_label, label)
%li %li
= link_to "Edit", edit_namespace_project_label_path(@project.namespace, @project, label) = link_to 'Edit', edit_label_path(label)
%li %li
= link_to "Delete", namespace_project_label_path(@project.namespace, @project, label), title: "Delete", method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} = link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, remote: true, data: {confirm: 'Remove this label? Are you sure?'}
.pull-right.hidden-xs.hidden-sm.hidden-md .pull-right.hidden-xs.hidden-sm.hidden-md
= link_to_label(label, type: :merge_request, css_class: 'btn btn-transparent btn-action') do = link_to_label(label, subject: @project, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
= pluralize label.open_merge_requests_count, 'merge request' = pluralize open_merge_requests_count, 'merge request'
= link_to_label(label, css_class: 'btn btn-transparent btn-action') do = link_to_label(label, subject: @project, css_class: 'btn btn-transparent btn-action') do
= pluralize label.open_issues_count(current_user), 'open issue' = pluralize open_issues_count, 'open issue'
- if current_user - if current_user
.label-subscription.inline{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } .label-subscription.inline{ data: toggle_subscription_data(label) }
%button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } } %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } }
%span.sr-only= label_subscription_toggle_button_text(label) %span.sr-only= label_subscription_toggle_button_text(label)
= icon('eye', class: 'label-subscribe-button-icon') = icon('eye', class: 'label-subscribe-button-icon', disabled: label.is_a?(GroupLabel))
= icon('spinner spin', class: 'label-subscribe-button-loading') = icon('spinner spin', class: 'label-subscribe-button-loading')
- if can? current_user, :admin_label, @project - if can?(current_user, :admin_label, label)
= link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
%span.sr-only Edit %span.sr-only Edit
= icon('pencil-square-o') = icon('pencil-square-o')
= link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?", toggle: "tooltip"} do = link_to destroy_label_path(label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: label_deletion_confirm_text(label), toggle: "tooltip"} do
%span.sr-only Delete %span.sr-only Delete
= icon('trash-o') = icon('trash-o')
- if current_user - if current_user && label.is_a?(ProjectLabel)
:javascript :javascript
new Subscription('##{dom_id(label)} .label-subscription'); new Subscription('##{dom_id(label)} .label-subscription');
...@@ -3,13 +3,16 @@ ...@@ -3,13 +3,16 @@
.draggable-handler .draggable-handler
= icon('bars') = icon('bars')
.js-toggle-priority.toggle-priority{ data: { url: remove_priority_namespace_project_label_path(@project.namespace, @project, label), .js-toggle-priority.toggle-priority{ data: { url: remove_priority_namespace_project_label_path(@project.namespace, @project, label),
dom_id: dom_id(label) } } dom_id: dom_id(label), type: label.type } }
%button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' } %button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' }
= icon('star-o') = icon('star-o')
%button.remove-priority.btn.has-tooltip{ title: 'Remove priority', :'data-placement' => 'top' } %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', :'data-placement' => 'top' }
= icon('star') = icon('star')
%span.label-name %span.label-name
= link_to_label(label, tooltip: false) = link_to_label(label, subject: @project, tooltip: false)
- if defined?(@project) && @project.group.present?
%span.label-type
= label.model_name.human.titleize
- if label.description - if label.description
%span.label-description %span.label-description
= markdown_field(label, :description) = markdown_field(label, :description)
- labels.each do |label| - labels.each do |label|
%span.label-row.btn-group{ role: "group", aria: { label: label.name }, style: "color: #{text_color_for_bg(label.color)}" } %span.label-row.btn-group{ role: "group", aria: { label: label.name }, style: "color: #{text_color_for_bg(label.color)}" }
= link_to_label(label, css_class: 'btn btn-transparent') = link_to_label(label, subject: @project, css_class: 'btn btn-transparent')
%button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } } %button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } }
= icon("times") = icon("times")
...@@ -77,11 +77,10 @@ ...@@ -77,11 +77,10 @@
= hidden_field_tag :state_event, params[:state_event] = hidden_field_tag :state_event, params[:state_event]
.filter-item.inline .filter-item.inline
= button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save" = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
- has_labels = @labels && @labels.any?
- if !@labels.nil? .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }
.row-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) } - if has_labels
- if @labels.any? = render 'shared/labels_row', labels: @labels
= render "shared/labels_row", labels: @labels
:javascript :javascript
new UsersSelect(); new UsersSelect();
......
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
.issuable-form-select-holder .issuable-form-select-holder
= render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_menu_above: true, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_menu_above: true, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
.form-group .form-group
- has_labels = issuable.project.labels.any? - has_labels = @labels && @labels.any?
= f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
= f.hidden_field :label_ids, multiple: true, value: '' = f.hidden_field :label_ids, multiple: true, value: ''
.col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
......
...@@ -107,7 +107,7 @@ ...@@ -107,7 +107,7 @@
= dropdown_content do = dropdown_content do
.js-due-date-calendar .js-due-date-calendar
- if issuable.project.labels.any? - if @labels && @labels.any?
- selected_labels = issuable.labels - selected_labels = issuable.labels
.block.labels .block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } } .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
......
= form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f| = form_for @label, as: :label, url: url, html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f|
= form_errors(@label) = form_errors(@label)
.form-group .form-group
...@@ -30,4 +30,4 @@ ...@@ -30,4 +30,4 @@
= f.submit 'Save changes', class: 'btn btn-save js-save-button' = f.submit 'Save changes', class: 'btn btn-save js-save-button'
- else - else
= f.submit 'Create Label', class: 'btn btn-create js-save-button' = f.submit 'Create Label', class: 'btn btn-create js-save-button'
= link_to "Cancel", namespace_project_labels_path(@project.namespace, @project), class: 'btn btn-cancel' = link_to 'Cancel', back_path, class: 'btn btn-cancel'
...@@ -5,6 +5,7 @@ en: ...@@ -5,6 +5,7 @@ en:
hello: "Hello world" hello: "Hello world"
errors: errors:
messages: messages:
label_already_exists_at_group_level: "already exists at group level for %{group}. Please choose another one."
wrong_size: "is the wrong size (should be %{file_size})" wrong_size: "is the wrong size (should be %{file_size})"
size_too_small: "is too small (should be at least %{file_size})" size_too_small: "is too small (should be at least %{file_size})"
size_too_big: "is too big (should be at most %{file_size})" size_too_big: "is too big (should be at most %{file_size})"
......
...@@ -28,5 +28,7 @@ resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do ...@@ -28,5 +28,7 @@ resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do
resource :avatar, only: [:destroy] resource :avatar, only: [:destroy]
resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
resources :labels, except: [:show], constraints: { id: /\d+/ }
end end
end end
class AddTypeToLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = true
DOWNTIME_REASON = 'Labels will not work as expected until this migration is complete.'
def change
add_column :labels, :type, :string
update_column_in_batches(:labels, :type, 'ProjectLabel') do |table, query|
query.where(table[:project_id].not_eq(nil))
end
end
end
class AddGroupIdToLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def change
add_column :labels, :group_id, :integer
add_foreign_key :labels, :namespaces, column: :group_id, on_delete: :cascade
add_concurrent_index :labels, :group_id
end
end
class CreateLabelPriorities < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = true
DOWNTIME_REASON = 'This migration adds foreign keys'
disable_ddl_transaction!
def up
create_table :label_priorities do |t|
t.references :project, foreign_key: { on_delete: :cascade }, null: false
t.references :label, foreign_key: { on_delete: :cascade }, null: false
t.integer :priority, null: false
t.timestamps null: false
end
add_concurrent_index :label_priorities, [:project_id, :label_id], unique: true
add_concurrent_index :label_priorities, :priority
end
def down
drop_table :label_priorities
end
end
class AddUniqueIndexToLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = true
DOWNTIME_REASON = 'This migration removes duplicated labels.'
disable_ddl_transaction!
def up
select_all('SELECT title, COUNT(id) as cnt FROM labels GROUP BY project_id, title HAVING COUNT(id) > 1').each do |label|
label_title = quote_string(label['title'])
duplicated_ids = select_all("SELECT id FROM labels WHERE title = '#{label_title}' ORDER BY id ASC").map{ |label| label['id'] }
label_id = duplicated_ids.first
duplicated_ids.delete(label_id)
execute("UPDATE label_links SET label_id = #{label_id} WHERE label_id IN(#{duplicated_ids.join(",")})")
execute("DELETE FROM labels WHERE id IN(#{duplicated_ids.join(",")})")
end
remove_index :labels, column: :project_id if index_exists?(:labels, :project_id)
remove_index :labels, column: :title if index_exists?(:labels, :title)
add_concurrent_index :labels, [:group_id, :project_id, :title], unique: true
end
def down
remove_index :labels, column: [:group_id, :project_id, :title] if index_exists?(:labels, [:group_id, :project_id, :title], unique: true)
add_concurrent_index :labels, :project_id
add_concurrent_index :labels, :title
end
end
class MigrateLabelsPriority < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = true
DOWNTIME_REASON = 'Prioritized labels will not work as expected until this migration is complete.'
disable_ddl_transaction!
def up
execute <<-EOF.strip_heredoc
INSERT INTO label_priorities (project_id, label_id, priority, created_at, updated_at)
SELECT labels.project_id, labels.id, labels.priority, NOW(), NOW()
FROM labels
WHERE labels.project_id IS NOT NULL
AND labels.priority IS NOT NULL;
EOF
end
def down
if Gitlab::Database.mysql?
execute <<-EOF.strip_heredoc
UPDATE labels
INNER JOIN label_priorities ON labels.id = label_priorities.label_id AND labels.project_id = label_priorities.project_id
SET labels.priority = label_priorities.priority;
EOF
else
execute <<-EOF.strip_heredoc
UPDATE labels
SET priority = label_priorities.priority
FROM label_priorities
WHERE labels.id = label_priorities.label_id
AND labels.project_id = label_priorities.project_id;
EOF
end
end
end
class RemovePriorityFromLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = true
DOWNTIME_REASON = 'This migration removes an existing column'
disable_ddl_transaction!
def up
remove_column :labels, :priority, :integer, index: true
end
def down
add_column :labels, :priority, :integer
add_concurrent_index :labels, :priority
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161012180455) do ActiveRecord::Schema.define(version: 20161018024550) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -517,6 +517,17 @@ ActiveRecord::Schema.define(version: 20161012180455) do ...@@ -517,6 +517,17 @@ ActiveRecord::Schema.define(version: 20161012180455) do
add_index "label_links", ["label_id"], name: "index_label_links_on_label_id", using: :btree add_index "label_links", ["label_id"], name: "index_label_links_on_label_id", using: :btree
add_index "label_links", ["target_id", "target_type"], name: "index_label_links_on_target_id_and_target_type", using: :btree add_index "label_links", ["target_id", "target_type"], name: "index_label_links_on_target_id_and_target_type", using: :btree
create_table "label_priorities", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "label_id", null: false
t.integer "priority", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "label_priorities", ["priority"], name: "index_label_priorities_on_priority", using: :btree
add_index "label_priorities", ["project_id", "label_id"], name: "index_label_priorities_on_project_id_and_label_id", unique: true, using: :btree
create_table "labels", force: :cascade do |t| create_table "labels", force: :cascade do |t|
t.string "title" t.string "title"
t.string "color" t.string "color"
...@@ -525,13 +536,13 @@ ActiveRecord::Schema.define(version: 20161012180455) do ...@@ -525,13 +536,13 @@ ActiveRecord::Schema.define(version: 20161012180455) do
t.datetime "updated_at" t.datetime "updated_at"
t.boolean "template", default: false t.boolean "template", default: false
t.string "description" t.string "description"
t.integer "priority"
t.text "description_html" t.text "description_html"
t.string "type"
t.integer "group_id"
end end
add_index "labels", ["priority"], name: "index_labels_on_priority", using: :btree add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree add_index "labels", ["group_id"], name: "index_labels_on_group_id", using: :btree
add_index "labels", ["title"], name: "index_labels_on_title", using: :btree
create_table "lfs_objects", force: :cascade do |t| create_table "lfs_objects", force: :cascade do |t|
t.string "oid", null: false t.string "oid", null: false
...@@ -1211,6 +1222,9 @@ ActiveRecord::Schema.define(version: 20161012180455) do ...@@ -1211,6 +1222,9 @@ ActiveRecord::Schema.define(version: 20161012180455) do
add_foreign_key "boards", "projects" add_foreign_key "boards", "projects"
add_foreign_key "issue_metrics", "issues", on_delete: :cascade add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "lists", "boards" add_foreign_key "lists", "boards"
add_foreign_key "lists", "labels" add_foreign_key "lists", "labels"
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
......
...@@ -22,7 +22,8 @@ with all their related data and be moved into a new GitLab instance. ...@@ -22,7 +22,8 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version | | GitLab version | Import/Export version |
| -------- | -------- | | -------- | -------- |
| 8.12.0 to current | 0.1.4 | | 8.13.0 to current | 0.1.5 |
| 8.12.0 | 0.1.4 |
| 8.10.3 | 0.1.3 | | 8.10.3 | 0.1.3 |
| 8.10.0 | 0.1.2 | | 8.10.0 | 0.1.2 |
| 8.9.5 | 0.1.1 | | 8.9.5 | 0.1.1 |
......
...@@ -8,7 +8,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps ...@@ -8,7 +8,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
end end
step 'I remove label \'bug\'' do step 'I remove label \'bug\'' do
page.within "#label_#{bug_label.id}" do page.within "#project_label_#{bug_label.id}" do
first(:link, 'Delete').click first(:link, 'Delete').click
end end
end end
......
...@@ -65,8 +65,8 @@ module API ...@@ -65,8 +65,8 @@ module API
requires :label_id, type: Integer, desc: 'The ID of an existing label' requires :label_id, type: Integer, desc: 'The ID of an existing label'
end end
post '/lists' do post '/lists' do
unless user_project.labels.exists?(params[:label_id]) unless available_labels.exists?(params[:label_id])
render_api_error!({ error: "Label not found!" }, 400) render_api_error!({ error: 'Label not found!' }, 400)
end end
authorize!(:admin_list, user_project) authorize!(:admin_list, user_project)
......
...@@ -71,6 +71,10 @@ module API ...@@ -71,6 +71,10 @@ module API
@project ||= find_project(params[:id]) @project ||= find_project(params[:id])
end end
def available_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
end
def find_project(id) def find_project(id)
project = Project.find_with_namespace(id) || Project.find_by(id: id) project = Project.find_with_namespace(id) || Project.find_by(id: id)
...@@ -118,7 +122,7 @@ module API ...@@ -118,7 +122,7 @@ module API
end end
def find_project_label(id) def find_project_label(id)
label = user_project.labels.find_by_id(id) || user_project.labels.find_by_title(id) label = available_labels.find_by_id(id) || available_labels.find_by_title(id)
label || not_found!('Label') label || not_found!('Label')
end end
...@@ -197,17 +201,12 @@ module API ...@@ -197,17 +201,12 @@ module API
def validate_label_params(params) def validate_label_params(params)
errors = {} errors = {}
if params[:labels].present? params[:labels].to_s.split(',').each do |label_name|
params[:labels].split(',').each do |label_name| label = available_labels.find_or_initialize_by(title: label_name.strip)
label = user_project.labels.create_with( next if label.valid?
color: Label::DEFAULT_COLOR).find_or_initialize_by(
title: label_name.strip)
if label.invalid?
errors[label.title] = label.errors errors[label.title] = label.errors
end end
end
end
errors errors
end end
......
...@@ -11,7 +11,7 @@ module API ...@@ -11,7 +11,7 @@ module API
# Example Request: # Example Request:
# GET /projects/:id/labels # GET /projects/:id/labels
get ':id/labels' do get ':id/labels' do
present user_project.labels, with: Entities::Label, current_user: current_user present available_labels, with: Entities::Label, current_user: current_user
end end
# Creates a new label # Creates a new label
......
...@@ -86,14 +86,11 @@ module API ...@@ -86,14 +86,11 @@ module API
render_api_error!({ labels: errors }, 400) render_api_error!({ labels: errors }, 400)
end end
attrs[:labels] = params[:labels] if params[:labels]
merge_request = ::MergeRequests::CreateService.new(user_project, current_user, attrs).execute merge_request = ::MergeRequests::CreateService.new(user_project, current_user, attrs).execute
if merge_request.valid? if merge_request.valid?
# Find or create labels and attach to issue
if params[:labels].present?
merge_request.add_labels_by_names(params[:labels].split(","))
end
present merge_request, with: Entities::MergeRequest, current_user: current_user present merge_request, with: Entities::MergeRequest, current_user: current_user
else else
handle_merge_request_errors! merge_request.errors handle_merge_request_errors! merge_request.errors
...@@ -195,15 +192,11 @@ module API ...@@ -195,15 +192,11 @@ module API
render_api_error!({ labels: errors }, 400) render_api_error!({ labels: errors }, 400)
end end
attrs[:labels] = params[:labels] if params[:labels]
merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request) merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request)
if merge_request.valid? if merge_request.valid?
# Find or create labels and attach to issue
unless params[:labels].nil?
merge_request.remove_labels
merge_request.add_labels_by_names(params[:labels].split(","))
end
present merge_request, with: Entities::MergeRequest, current_user: current_user present merge_request, with: Entities::MergeRequest, current_user: current_user
else else
handle_merge_request_errors! merge_request.errors handle_merge_request_errors! merge_request.errors
......
...@@ -9,7 +9,7 @@ module Banzai ...@@ -9,7 +9,7 @@ module Banzai
end end
def find_object(project, id) def find_object(project, id)
project.labels.find(id) find_labels(project).find(id)
end end
def self.references_in(text, pattern = Label.reference_pattern) def self.references_in(text, pattern = Label.reference_pattern)
...@@ -35,7 +35,11 @@ module Banzai ...@@ -35,7 +35,11 @@ module Banzai
return unless project return unless project
label_params = label_params(label_id, label_name) label_params = label_params(label_id, label_name)
project.labels.find_by(label_params) find_labels(project).find_by(label_params)
end
def find_labels(project)
LabelsFinder.new(nil, project_id: project.id).execute(authorized_only: false)
end end
# Parameters to pass to `Label.find_by` based on the given arguments # Parameters to pass to `Label.find_by` based on the given arguments
...@@ -60,11 +64,48 @@ module Banzai ...@@ -60,11 +64,48 @@ module Banzai
end end
def object_link_text(object, matches) def object_link_text(object, matches)
if context[:project] == object.project if same_group?(object) && namespace_match?(matches)
render_same_project_label(object)
elsif same_project?(object)
render_same_project_label(object)
else
render_cross_project_label(object, matches)
end
end
def same_group?(object)
object.is_a?(GroupLabel) && object.group == project.group
end
def namespace_match?(matches)
matches[:project].blank? || matches[:project] == project.path_with_namespace
end
def same_project?(object)
object.is_a?(ProjectLabel) && object.project == project
end
def user
context[:current_user] || context[:author]
end
def project
context[:project]
end
def render_same_project_label(object)
LabelsHelper.render_colored_label(object) LabelsHelper.render_colored_label(object)
end
def render_cross_project_label(object, matches)
source_project =
if matches[:project]
Project.find_with_namespace(matches[:project])
else else
LabelsHelper.render_colored_cross_project_label(object) object.project
end end
LabelsHelper.render_colored_cross_project_label(object, source_project)
end end
def unescape_html_entities(text) def unescape_html_entities(text)
......
...@@ -74,8 +74,8 @@ module Gitlab ...@@ -74,8 +74,8 @@ module Gitlab
end end
def create_label(name) def create_label(name)
color = nice_label_color(name) params = { title: name, color: nice_label_color(name) }
Label.create!(project_id: project.id, title: name, color: color) ::Labels::FindOrCreateService.new(project.owner, project, params).execute
end end
def user_info(person_id) def user_info(person_id)
...@@ -122,25 +122,21 @@ module Gitlab ...@@ -122,25 +122,21 @@ module Gitlab
author_id = user_info(bug['ixPersonOpenedBy'])[:gitlab_id] || project.creator_id author_id = user_info(bug['ixPersonOpenedBy'])[:gitlab_id] || project.creator_id
issue = Issue.create!( issue = Issue.create!(
iid: bug['ixBug'],
project_id: project.id, project_id: project.id,
title: bug['sTitle'], title: bug['sTitle'],
description: body, description: body,
author_id: author_id, author_id: author_id,
assignee_id: assignee_id, assignee_id: assignee_id,
state: bug['fOpen'] == 'true' ? 'opened' : 'closed' state: bug['fOpen'] == 'true' ? 'opened' : 'closed',
created_at: date,
updated_at: DateTime.parse(bug['dtLastUpdated'])
) )
issue.add_labels_by_names(labels)
if issue.iid != bug['ixBug'] issue_labels = ::LabelsFinder.new(project.owner, project_id: project.id, title: labels).execute
issue.update_attribute(:iid, bug['ixBug']) issue.update_attribute(:label_ids, issue_labels.pluck(:id))
end
import_issue_comments(issue, comments) import_issue_comments(issue, comments)
issue.update_attribute(:created_at, date)
last_update = DateTime.parse(bug['dtLastUpdated'])
issue.update_attribute(:updated_at, last_update)
end end
end end
......
...@@ -58,7 +58,7 @@ module Gitlab ...@@ -58,7 +58,7 @@ module Gitlab
referable = find_referable(reference) referable = find_referable(reference)
return reference unless referable return reference unless referable
cross_reference = referable.to_reference(target_project) cross_reference = build_cross_reference(referable, target_project)
return reference if reference == cross_reference return reference if reference == cross_reference
new_text = before + cross_reference + after new_text = before + cross_reference + after
...@@ -72,6 +72,14 @@ module Gitlab ...@@ -72,6 +72,14 @@ module Gitlab
extractor.all.first extractor.all.first
end end
def build_cross_reference(referable, target_project)
if referable.respond_to?(:project)
referable.to_reference(target_project)
else
referable.to_reference(@source_project, target_project)
end
end
def substitution_valid?(substituted) def substitution_valid?(substituted)
@original_html == markdown(substituted) @original_html == markdown(substituted)
end end
......
...@@ -14,9 +14,13 @@ module Gitlab ...@@ -14,9 +14,13 @@ module Gitlab
end end
def create! def create!
project.labels.find_or_create_by!(title: title) do |label| params = attributes.except(:project)
label.color = color service = ::Labels::FindOrCreateService.new(project.owner, project, params)
end label = service.execute
raise ActiveRecord::RecordInvalid.new(label) unless label.persisted?
label
end end
private private
......
...@@ -92,19 +92,17 @@ module Gitlab ...@@ -92,19 +92,17 @@ module Gitlab
end end
issue = Issue.create!( issue = Issue.create!(
iid: raw_issue['id'],
project_id: project.id, project_id: project.id,
title: raw_issue["title"], title: raw_issue['title'],
description: body, description: body,
author_id: project.creator_id, author_id: project.creator_id,
assignee_id: assignee_id, assignee_id: assignee_id,
state: raw_issue["state"] == "closed" ? "closed" : "opened" state: raw_issue['state'] == 'closed' ? 'closed' : 'opened'
) )
issue.add_labels_by_names(labels) issue_labels = ::LabelsFinder.new(project.owner, project_id: project.id, title: labels).execute
issue.update_attribute(:label_ids, issue_labels.pluck(:id))
if issue.iid != raw_issue["id"]
issue.update_attribute(:iid, raw_issue["id"])
end
import_issue_comments(issue, comments) import_issue_comments(issue, comments)
end end
...@@ -236,8 +234,8 @@ module Gitlab ...@@ -236,8 +234,8 @@ module Gitlab
end end
def create_label(name) def create_label(name)
color = nice_label_color(name) params = { name: name, color: nice_label_color(name) }
Label.create!(project_id: project.id, name: name, color: color) ::Labels::FindOrCreateService.new(project.owner, project, params).execute
end end
def format_content(raw_content) def format_content(raw_content)
......
...@@ -3,7 +3,7 @@ module Gitlab ...@@ -3,7 +3,7 @@ module Gitlab
extend self extend self
# For every version update, the version history in import_export.md has to be kept up to date. # For every version update, the version history in import_export.md has to be kept up to date.
VERSION = '0.1.4' VERSION = '0.1.5'
FILENAME_LIMIT = 50 FILENAME_LIMIT = 50
def export_path(relative_path:) def export_path(relative_path:)
......
module Gitlab module Gitlab
module ImportExport module ImportExport
class AttributeCleaner class AttributeCleaner
ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id']
def self.clean!(relation_hash:) def self.clean!(relation_hash:)
relation_hash.reject! do |key, _value| relation_hash.reject! do |key, _value|
......
# Model relationships to be included in the project import/export # Model relationships to be included in the project import/export
project_tree: project_tree:
- :labels - labels:
:priorities
- milestones: - milestones:
- :events - :events
- issues: - issues:
...@@ -9,7 +10,8 @@ project_tree: ...@@ -9,7 +10,8 @@ project_tree:
- :author - :author
- :events - :events
- label_links: - label_links:
- :label - label:
:priorities
- milestone: - milestone:
- :events - :events
- snippets: - snippets:
...@@ -26,7 +28,8 @@ project_tree: ...@@ -26,7 +28,8 @@ project_tree:
- :merge_request_diff - :merge_request_diff
- :events - :events
- label_links: - label_links:
- :label - label:
:priorities
- milestone: - milestone:
- :events - :events
- pipelines: - pipelines:
...@@ -71,6 +74,10 @@ excluded_attributes: ...@@ -71,6 +74,10 @@ excluded_attributes:
- :awardable_id - :awardable_id
methods: methods:
labels:
- :type
label:
- :type
statuses: statuses:
- :type - :type
services: services:
......
...@@ -65,11 +65,17 @@ module Gitlab ...@@ -65,11 +65,17 @@ module Gitlab
# +value+ existing model to be included in the hash # +value+ existing model to be included in the hash
# +parsed_hash+ the original hash # +parsed_hash+ the original hash
def parse_hash(value) def parse_hash(value)
return nil if already_contains_methods?(value)
@attributes_finder.parse(value) do |hash| @attributes_finder.parse(value) do |hash|
{ include: hash_or_merge(value, hash) } { include: hash_or_merge(value, hash) }
end end
end end
def already_contains_methods?(value)
value.is_a?(Hash) && value.values.detect { |val| val[:methods]}
end
# Adds new model configuration to an existing hash with key +current_key+ # Adds new model configuration to an existing hash with key +current_key+
# It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+ # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
# #
......
...@@ -110,7 +110,7 @@ module Gitlab ...@@ -110,7 +110,7 @@ module Gitlab
def create_relation(relation, relation_hash_list) def create_relation(relation, relation_hash_list)
relation_array = [relation_hash_list].flatten.map do |relation_hash| relation_array = [relation_hash_list].flatten.map do |relation_hash|
Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym, Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym,
relation_hash: relation_hash, relation_hash: parsed_relation_hash(relation_hash),
members_mapper: members_mapper, members_mapper: members_mapper,
user: @user, user: @user,
project_id: restored_project.id) project_id: restored_project.id)
...@@ -118,6 +118,10 @@ module Gitlab ...@@ -118,6 +118,10 @@ module Gitlab
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
end end
def parsed_relation_hash(relation_hash)
relation_hash.merge!('group_id' => restored_project.group.try(:id), 'project_id' => restored_project.id)
end
end end
end end
end end
...@@ -9,7 +9,10 @@ module Gitlab ...@@ -9,7 +9,10 @@ module Gitlab
builds: 'Ci::Build', builds: 'Ci::Build',
hooks: 'ProjectHook', hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel', merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel' }.freeze push_access_levels: 'ProtectedBranch::PushAccessLevel',
labels: :project_labels,
priorities: :label_priorities,
label: :project_label }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze
...@@ -19,9 +22,7 @@ module Gitlab ...@@ -19,9 +22,7 @@ module Gitlab
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
EXISTING_OBJECT_CHECK = %i[milestone milestones label labels].freeze EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels project_label group_label].freeze
FINDER_ATTRIBUTES = %w[title project_id].freeze
def self.create(*args) def self.create(*args)
new(*args).create new(*args).create
...@@ -56,6 +57,8 @@ module Gitlab ...@@ -56,6 +57,8 @@ module Gitlab
update_user_references update_user_references
update_project_references update_project_references
handle_group_label if group_label?
reset_ci_tokens if @relation_name == 'Ci::Trigger' reset_ci_tokens if @relation_name == 'Ci::Trigger'
@relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data'] @relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data']
set_st_diffs if @relation_name == :merge_request_diff set_st_diffs if @relation_name == :merge_request_diff
...@@ -123,6 +126,20 @@ module Gitlab ...@@ -123,6 +126,20 @@ module Gitlab
@relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id'] @relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id']
end end
def group_label?
@relation_hash['type'] == 'GroupLabel'
end
def handle_group_label
# If there's no group, move the label to a project label
if @relation_hash['group_id']
@relation_hash['project_id'] = nil
@relation_name = :group_label
else
@relation_hash['type'] = 'ProjectLabel'
end
end
def reset_ci_tokens def reset_ci_tokens
return unless Gitlab::ImportExport.reset_tokens? return unless Gitlab::ImportExport.reset_tokens?
...@@ -171,11 +188,9 @@ module Gitlab ...@@ -171,11 +188,9 @@ module Gitlab
# Otherwise always create the record, skipping the extra SELECT clause. # Otherwise always create the record, skipping the extra SELECT clause.
@existing_or_new_object ||= begin @existing_or_new_object ||= begin
if EXISTING_OBJECT_CHECK.include?(@relation_name) if EXISTING_OBJECT_CHECK.include?(@relation_name)
events = parsed_relation_hash.delete('events') attribute_hash = attribute_hash_for(['events', 'priorities'])
unless events.blank? existing_object.assign_attributes(attribute_hash) if attribute_hash.any?
existing_object.assign_attributes(events: events)
end
existing_object existing_object
else else
...@@ -184,14 +199,22 @@ module Gitlab ...@@ -184,14 +199,22 @@ module Gitlab
end end
end end
def attribute_hash_for(attributes)
attributes.inject({}) do |hash, value|
hash[value] = parsed_relation_hash.delete(value) if parsed_relation_hash[value]
hash
end
end
def existing_object def existing_object
@existing_object ||= @existing_object ||=
begin begin
finder_hash = parsed_relation_hash.slice(*FINDER_ATTRIBUTES) finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id]
finder_hash = parsed_relation_hash.slice(*finder_attributes)
existing_object = relation_class.find_or_create_by(finder_hash) existing_object = relation_class.find_or_create_by(finder_hash)
# Done in two steps, as MySQL behaves differently than PostgreSQL using # Done in two steps, as MySQL behaves differently than PostgreSQL using
# the +find_or_create_by+ method and does not return the ID the second time. # the +find_or_create_by+ method and does not return the ID the second time.
existing_object.update(parsed_relation_hash) existing_object.update!(parsed_relation_hash)
existing_object existing_object
end end
end end
......
...@@ -18,8 +18,8 @@ module Gitlab ...@@ -18,8 +18,8 @@ module Gitlab
{ title: "enhancement", color: green } { title: "enhancement", color: green }
] ]
labels.each do |label| labels.each do |params|
project.labels.create(label) ::Labels::FindOrCreateService.new(project.owner, project).execute(params)
end end
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe Projects::LabelsController do describe Projects::LabelsController do
let(:project) { create(:project) } let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
project.team << [user, :master] project.team << [user, :master]
sign_in(user) sign_in(user)
end end
describe 'GET #index' do describe 'GET #index' do
def create_label(attributes) let!(:label_1) { create(:label, project: project, priority: 1, title: 'Label 1') }
create(:label, attributes.merge(project: project)) let!(:label_2) { create(:label, project: project, priority: 3, title: 'Label 2') }
end let!(:label_3) { create(:label, project: project, priority: 1, title: 'Label 3') }
let!(:label_4) { create(:label, project: project, title: 'Label 4') }
let!(:label_5) { create(:label, project: project, title: 'Label 5') }
before do let!(:group_label_1) { create(:group_label, group: group, title: 'Group Label 1') }
15.times { |i| create_label(priority: (i % 3) + 1, title: "label #{15 - i}") } let!(:group_label_2) { create(:group_label, group: group, title: 'Group Label 2') }
5.times { |i| create_label(title: "label #{100 - i}") } let!(:group_label_3) { create(:group_label, group: group, title: 'Group Label 3') }
let!(:group_label_4) { create(:group_label, group: group, title: 'Group Label 4') }
get :index, namespace_id: project.namespace.to_param, project_id: project.to_param before do
create(:label_priority, project: project, label: group_label_1, priority: 3)
create(:label_priority, project: project, label: group_label_2, priority: 1)
end end
context '@prioritized_labels' do context '@prioritized_labels' do
let(:prioritized_labels) { assigns(:prioritized_labels) } before do
list_labels
end
it 'contains only prioritized labels' do it 'does not include labels without priority' do
expect(prioritized_labels).to all(have_attributes(priority: a_value > 0)) list_labels
expect(assigns(:prioritized_labels)).not_to include(group_label_3, group_label_4, label_4, label_5)
end end
it 'is sorted by priority, then label title' do it 'is sorted by priority, then label title' do
priorities_and_titles = prioritized_labels.pluck(:priority, :title) expect(assigns(:prioritized_labels)).to eq [group_label_2, label_1, label_3, group_label_1, label_2]
expect(priorities_and_titles.sort).to eq(priorities_and_titles)
end end
end end
context '@labels' do context '@labels' do
let(:labels) { assigns(:labels) } it 'is sorted by label title' do
list_labels
it 'contains only unprioritized labels' do expect(assigns(:labels)).to eq [group_label_3, group_label_4, label_4, label_5]
expect(labels).to all(have_attributes(priority: nil))
end end
it 'is sorted by label title' do it 'does not include labels with priority' do
titles = labels.pluck(:title) list_labels
expect(titles.sort).to eq(titles) expect(assigns(:labels)).not_to include(group_label_2, label_1, label_3, group_label_1, label_2)
end end
it 'does not include group labels when project does not belong to a group' do
project.update(namespace: create(:namespace))
list_labels
expect(assigns(:labels)).not_to include(group_label_3, group_label_4)
end
end
def list_labels
get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
end end
end end
end end
FactoryGirl.define do
factory :label_priority do
project factory: :empty_project
label
sequence(:priority)
end
end
FactoryGirl.define do FactoryGirl.define do
factory :label do factory :label, class: ProjectLabel do
sequence(:title) { |n| "label#{n}" } sequence(:title) { |n| "label#{n}" }
color "#990000" color "#990000"
project project
transient do
priority nil
end
after(:create) do |label, evaluator|
if evaluator.priority
label.priorities.create(project: label.project, priority: evaluator.priority)
end
end
end
factory :group_label, class: GroupLabel do
sequence(:title) { |n| "label#{n}" }
color "#990000"
group
end end
end end
...@@ -68,5 +68,15 @@ FactoryGirl.define do ...@@ -68,5 +68,15 @@ FactoryGirl.define do
factory :closed_merge_request, traits: [:closed] factory :closed_merge_request, traits: [:closed]
factory :reopened_merge_request, traits: [:reopened] factory :reopened_merge_request, traits: [:reopened]
factory :merge_request_with_diffs, traits: [:with_diffs] factory :merge_request_with_diffs, traits: [:with_diffs]
factory :labeled_merge_request do
transient do
labels []
end
after(:create) do |merge_request, evaluator|
merge_request.update_attributes(labels: evaluator.labels)
end
end
end end
end end
...@@ -3,18 +3,56 @@ require 'spec_helper' ...@@ -3,18 +3,56 @@ require 'spec_helper'
feature 'Prioritize labels', feature: true do feature 'Prioritize labels', feature: true do
include WaitForAjax include WaitForAjax
context 'when project belongs to user' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) } let(:group) { create(:group) }
let(:project) { create(:empty_project, :public, namespace: group) }
let!(:bug) { create(:label, project: project, title: 'bug') }
let!(:wontfix) { create(:label, project: project, title: 'wontfix') }
let!(:feature) { create(:group_label, group: group, title: 'feature') }
scenario 'user can prioritize a label', js: true do context 'when user belongs to project team' do
bug = create(:label, title: 'bug') before do
wontfix = create(:label, title: 'wontfix') project.team << [user, :developer]
project.labels << bug
project.labels << wontfix
login_as user login_as user
end
scenario 'user can prioritize a group label', js: true do
visit namespace_project_labels_path(project.namespace, project)
expect(page).to have_content('No prioritized labels yet')
page.within('.other-labels') do
all('.js-toggle-priority')[1].click
wait_for_ajax
expect(page).not_to have_content('feature')
end
page.within('.prioritized-labels') do
expect(page).not_to have_content('No prioritized labels yet')
expect(page).to have_content('feature')
end
end
scenario 'user can unprioritize a group label', js: true do
create(:label_priority, project: project, label: feature, priority: 1)
visit namespace_project_labels_path(project.namespace, project)
page.within('.prioritized-labels') do
expect(page).to have_content('feature')
first('.js-toggle-priority').click
wait_for_ajax
expect(page).not_to have_content('bug')
end
page.within('.other-labels') do
expect(page).to have_content('feature')
end
end
scenario 'user can prioritize a project label', js: true do
visit namespace_project_labels_path(project.namespace, project) visit namespace_project_labels_path(project.namespace, project)
expect(page).to have_content('No prioritized labels yet') expect(page).to have_content('No prioritized labels yet')
...@@ -31,19 +69,14 @@ feature 'Prioritize labels', feature: true do ...@@ -31,19 +69,14 @@ feature 'Prioritize labels', feature: true do
end end
end end
scenario 'user can unprioritize a label', js: true do scenario 'user can unprioritize a project label', js: true do
bug = create(:label, title: 'bug', priority: 1) create(:label_priority, project: project, label: bug, priority: 1)
wontfix = create(:label, title: 'wontfix')
project.labels << bug
project.labels << wontfix
login_as user
visit namespace_project_labels_path(project.namespace, project) visit namespace_project_labels_path(project.namespace, project)
page.within('.prioritized-labels') do
expect(page).to have_content('bug') expect(page).to have_content('bug')
page.within('.prioritized-labels') do
first('.js-toggle-priority').click first('.js-toggle-priority').click
wait_for_ajax wait_for_ajax
expect(page).not_to have_content('bug') expect(page).not_to have_content('bug')
...@@ -56,23 +89,20 @@ feature 'Prioritize labels', feature: true do ...@@ -56,23 +89,20 @@ feature 'Prioritize labels', feature: true do
end end
scenario 'user can sort prioritized labels and persist across reloads', js: true do scenario 'user can sort prioritized labels and persist across reloads', js: true do
bug = create(:label, title: 'bug', priority: 1) create(:label_priority, project: project, label: bug, priority: 1)
wontfix = create(:label, title: 'wontfix', priority: 2) create(:label_priority, project: project, label: feature, priority: 2)
project.labels << bug
project.labels << wontfix
login_as user
visit namespace_project_labels_path(project.namespace, project) visit namespace_project_labels_path(project.namespace, project)
expect(page).to have_content 'bug' expect(page).to have_content 'bug'
expect(page).to have_content 'feature'
expect(page).to have_content 'wontfix' expect(page).to have_content 'wontfix'
# Sort labels # Sort labels
find("#label_#{bug.id}").drag_to find("#label_#{wontfix.id}") find("#project_label_#{bug.id}").drag_to find("#group_label_#{feature.id}")
page.within('.prioritized-labels') do page.within('.prioritized-labels') do
expect(first('li')).to have_content('wontfix') expect(first('li')).to have_content('feature')
expect(page.all('li').last).to have_content('bug') expect(page.all('li').last).to have_content('bug')
end end
...@@ -80,7 +110,7 @@ feature 'Prioritize labels', feature: true do ...@@ -80,7 +110,7 @@ feature 'Prioritize labels', feature: true do
wait_for_ajax wait_for_ajax
page.within('.prioritized-labels') do page.within('.prioritized-labels') do
expect(first('li')).to have_content('wontfix') expect(first('li')).to have_content('feature')
expect(page.all('li').last).to have_content('bug') expect(page.all('li').last).to have_content('bug')
end end
end end
...@@ -88,28 +118,26 @@ feature 'Prioritize labels', feature: true do ...@@ -88,28 +118,26 @@ feature 'Prioritize labels', feature: true do
context 'as a guest' do context 'as a guest' do
it 'does not prioritize labels' do it 'does not prioritize labels' do
user = create(:user)
guest = create(:user) guest = create(:user)
project = create(:project, name: 'test', namespace: user.namespace)
create(:label, title: 'bug')
login_as guest login_as guest
visit namespace_project_labels_path(project.namespace, project) visit namespace_project_labels_path(project.namespace, project)
expect(page).to have_content 'bug'
expect(page).to have_content 'wontfix'
expect(page).to have_content 'feature'
expect(page).not_to have_css('.prioritized-labels') expect(page).not_to have_css('.prioritized-labels')
end end
end end
context 'as a non signed in user' do context 'as a non signed in user' do
it 'does not prioritize labels' do it 'does not prioritize labels' do
user = create(:user)
project = create(:project, name: 'test', namespace: user.namespace)
create(:label, title: 'bug')
visit namespace_project_labels_path(project.namespace, project) visit namespace_project_labels_path(project.namespace, project)
expect(page).to have_content 'bug'
expect(page).to have_content 'wontfix'
expect(page).to have_content 'feature'
expect(page).not_to have_css('.prioritized-labels') expect(page).not_to have_css('.prioritized-labels')
end end
end end
......
require 'spec_helper'
describe LabelsFinder do
describe '#execute' do
let(:group_1) { create(:group) }
let(:group_2) { create(:group) }
let(:group_3) { create(:group) }
let(:project_1) { create(:empty_project, namespace: group_1) }
let(:project_2) { create(:empty_project, namespace: group_2) }
let(:project_3) { create(:empty_project) }
let(:project_4) { create(:empty_project, :public) }
let(:project_5) { create(:empty_project, namespace: group_1) }
let!(:project_label_1) { create(:label, project: project_1, title: 'Label 1') }
let!(:project_label_2) { create(:label, project: project_2, title: 'Label 2') }
let!(:project_label_4) { create(:label, project: project_4, title: 'Label 4') }
let!(:project_label_5) { create(:label, project: project_5, title: 'Label 5') }
let!(:group_label_1) { create(:group_label, group: group_1, title: 'Label 1') }
let!(:group_label_2) { create(:group_label, group: group_1, title: 'Group Label 2') }
let!(:group_label_3) { create(:group_label, group: group_2, title: 'Group Label 3') }
let(:user) { create(:user) }
before do
create(:label, project: project_3, title: 'Label 3')
create(:group_label, group: group_3, title: 'Group Label 4')
project_1.team << [user, :developer]
end
context 'with no filter' do
it 'returns labels from projects the user have access' do
group_2.add_developer(user)
finder = described_class.new(user)
expect(finder.execute).to eq [group_label_2, group_label_3, project_label_1, group_label_1, project_label_2, project_label_4]
end
end
context 'filtering by group_id' do
it 'returns labels available for any project within the group' do
group_1.add_developer(user)
finder = described_class.new(user, group_id: group_1.id)
expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1, project_label_5]
end
end
context 'filtering by project_id' do
it 'returns labels available for the project' do
finder = described_class.new(user, project_id: project_1.id)
expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1]
end
end
context 'filtering by title' do
it 'returns label with that title' do
finder = described_class.new(user, title: 'Group Label 2')
expect(finder.execute).to eq [group_label_2]
end
end
end
end
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
"enum": ["backlog", "label", "done"] "enum": ["backlog", "label", "done"]
}, },
"label": { "label": {
"type": ["object"], "type": ["object", "null"],
"required": [ "required": [
"id", "id",
"color", "color",
......
...@@ -5,27 +5,26 @@ describe LabelsHelper do ...@@ -5,27 +5,26 @@ describe LabelsHelper do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:label) { create(:label, project: project) } let(:label) { create(:label, project: project) }
context 'with @project set' do context 'without subject' do
before do it "uses the label's project" do
@project = project expect(link_to_label(label)).to match %r{<a href="/#{label.project.to_reference}/issues\?label_name%5B%5D=#{label.name}">.*</a>}
end
it 'uses the instance variable' do
expect(link_to_label(label)).to match %r{<a href="/#{@project.to_reference}/issues\?label_name%5B%5D=#{label.name}"><span class="[\w\s\-]*has-tooltip".*</span></a>}
end end
end end
context 'without @project set' do context 'with a project as subject' do
it "uses the label's project" do let(:namespace) { build(:namespace, name: 'foo3') }
expect(link_to_label(label)).to match %r{<a href="/#{label.project.to_reference}/issues\?label_name%5B%5D=#{label.name}">.*</a>} let(:another_project) { build(:empty_project, namespace: namespace, name: 'bar3') }
it 'links to project issues page' do
expect(link_to_label(label, subject: another_project)).to match %r{<a href="/foo3/bar3/issues\?label_name%5B%5D=#{label.name}">.*</a>}
end end
end end
context 'with a project argument' do context 'with a group as subject' do
let(:another_project) { double('project', namespace: 'foo3', to_param: 'bar3') } let(:group) { build(:group, name: 'bar') }
it 'links to merge requests page' do it 'links to group issues page' do
expect(link_to_label(label, project: another_project)).to match %r{<a href="/foo3/bar3/issues\?label_name%5B%5D=#{label.name}">.*</a>} expect(link_to_label(label, subject: group)).to match %r{<a href="/groups/bar/issues\?label_name%5B%5D=#{label.name}">.*</a>}
end end
end end
......
...@@ -305,6 +305,58 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do ...@@ -305,6 +305,58 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end end
end end
describe 'group label references' do
let(:group) { create(:group) }
let(:project) { create(:empty_project, :public, namespace: group) }
let(:group_label) { create(:group_label, name: 'gfm references', group: group) }
context 'without project reference' do
let(:reference) { group_label.to_reference(format: :name) }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}", project: project)
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: group_label.name)
expect(doc.text).to eq 'See gfm references'
end
it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{group_label.name}</span></a>\.\)))
end
it 'ignores invalid label names' do
exp = act = %(Label #{Label.reference_prefix}"#{group_label.name.reverse}")
expect(reference_filter(act).to_html).to eq exp
end
end
context 'with project reference' do
let(:reference) { project.to_reference + group_label.to_reference(format: :name) }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}", project: project)
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: group_label.name)
expect(doc.text).to eq 'See gfm references'
end
it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{group_label.name}</span></a>\.\)))
end
it 'ignores invalid label names' do
exp = act = %(Label #{project.to_reference}#{Label.reference_prefix}"#{group_label.name.reverse}")
expect(reference_filter(act).to_html).to eq exp
end
end
end
describe 'cross project label references' do describe 'cross project label references' do
context 'valid project referenced' do context 'valid project referenced' do
let(:another_project) { create(:empty_project, :public) } let(:another_project) { create(:empty_project, :public) }
...@@ -339,4 +391,34 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do ...@@ -339,4 +391,34 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end end
end end
end end
describe 'cross group label references' do
context 'valid project referenced' do
let(:group) { create(:group) }
let(:project) { create(:empty_project, :public, namespace: group) }
let(:another_group) { create(:group) }
let(:another_project) { create(:empty_project, :public, namespace: another_group) }
let(:project_name) { another_project.name_with_namespace }
let(:group_label) { create(:group_label, group: another_group, color: '#00ff00') }
let(:reference) { another_project.to_reference + group_label.to_reference }
let!(:result) { reference_filter("See #{reference}", project: project) }
it 'points to referenced project issues page' do
expect(result.css('a').first.attr('href'))
.to eq urls.namespace_project_issues_url(another_project.namespace,
another_project,
label_name: group_label.name)
end
it 'has valid color' do
expect(result.css('a span').first.attr('style'))
.to match /background-color: #00ff00/
end
it 'contains cross project content' do
expect(result.css('a').first.text).to eq "#{group_label.name} in #{project_name}"
end
end
end
end end
...@@ -2,8 +2,8 @@ require 'spec_helper' ...@@ -2,8 +2,8 @@ require 'spec_helper'
describe Gitlab::Gfm::ReferenceRewriter do describe Gitlab::Gfm::ReferenceRewriter do
let(:text) { 'some text' } let(:text) { 'some text' }
let(:old_project) { create(:project) } let(:old_project) { create(:project, name: 'old') }
let(:new_project) { create(:project) } let(:new_project) { create(:project, name: 'new') }
let(:user) { create(:user) } let(:user) { create(:user) }
before { old_project.team << [user, :guest] } before { old_project.team << [user, :guest] }
...@@ -62,7 +62,7 @@ describe Gitlab::Gfm::ReferenceRewriter do ...@@ -62,7 +62,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
it { is_expected.to eq "#{ref}, `#1`, #{ref}, `#1`" } it { is_expected.to eq "#{ref}, `#1`, #{ref}, `#1`" }
end end
context 'description with labels' do context 'description with project labels' do
let!(:label) { create(:label, id: 123, name: 'test', project: old_project) } let!(:label) { create(:label, id: 123, name: 'test', project: old_project) }
let(:project_ref) { old_project.to_reference } let(:project_ref) { old_project.to_reference }
...@@ -76,6 +76,26 @@ describe Gitlab::Gfm::ReferenceRewriter do ...@@ -76,6 +76,26 @@ describe Gitlab::Gfm::ReferenceRewriter do
it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} } it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} }
end end
end end
context 'description with group labels' do
let(:old_group) { create(:group) }
let!(:group_label) { create(:group_label, id: 321, name: 'group label', group: old_group) }
let(:project_ref) { old_project.to_reference }
before do
old_project.update(namespace: old_group)
end
context 'label referenced by id' do
let(:text) { '#1 and ~321' }
it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} }
end
context 'label referenced by text' do
let(:text) { '#1 and ~"group label"' }
it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} }
end
end
end end
context 'reference contains milestone' do context 'reference contains milestone' do
......
...@@ -15,6 +15,7 @@ describe Gitlab::GoogleCodeImport::Importer, lib: true do ...@@ -15,6 +15,7 @@ describe Gitlab::GoogleCodeImport::Importer, lib: true do
subject { described_class.new(project) } subject { described_class.new(project) }
before do before do
project.team << [project.creator, :master]
project.create_import_data(data: import_data) project.create_import_data(data: import_data)
end end
......
...@@ -38,6 +38,7 @@ label: ...@@ -38,6 +38,7 @@ label:
- label_links - label_links
- issues - issues
- merge_requests - merge_requests
- priorities
milestone: milestone:
- project - project
- issues - issues
...@@ -186,3 +187,5 @@ project: ...@@ -186,3 +187,5 @@ project:
award_emoji: award_emoji:
- awardable - awardable
- user - user
priorities:
- label
\ No newline at end of file
...@@ -2,6 +2,21 @@ ...@@ -2,6 +2,21 @@
"description": "Nisi et repellendus ut enim quo accusamus vel magnam.", "description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
"visibility_level": 10, "visibility_level": 10,
"archived": false, "archived": false,
"labels": [
{
"id": 2,
"title": "test2",
"color": "#428bca",
"project_id": 8,
"created_at": "2016-07-22T08:55:44.161Z",
"updated_at": "2016-07-22T08:55:44.161Z",
"template": false,
"description": "",
"type": "ProjectLabel",
"priorities": [
]
}
],
"issues": [ "issues": [
{ {
"id": 40, "id": 40,
...@@ -64,7 +79,37 @@ ...@@ -64,7 +79,37 @@
"updated_at": "2016-07-22T08:55:44.161Z", "updated_at": "2016-07-22T08:55:44.161Z",
"template": false, "template": false,
"description": "", "description": "",
"priority": null "type": "ProjectLabel"
}
},
{
"id": 3,
"label_id": 3,
"target_id": 40,
"target_type": "Issue",
"created_at": "2016-07-22T08:57:02.841Z",
"updated_at": "2016-07-22T08:57:02.841Z",
"label": {
"id": 3,
"title": "test3",
"color": "#428bca",
"group_id": 8,
"created_at": "2016-07-22T08:55:44.161Z",
"updated_at": "2016-07-22T08:55:44.161Z",
"template": false,
"description": "",
"project_id": null,
"type": "GroupLabel",
"priorities": [
{
"id": 1,
"project_id": 5,
"label_id": 1,
"priority": 1,
"created_at": "2016-10-18T09:35:43.338Z",
"updated_at": "2016-10-18T09:35:43.338Z"
}
]
} }
} }
], ],
...@@ -536,7 +581,7 @@ ...@@ -536,7 +581,7 @@
"updated_at": "2016-07-22T08:55:44.161Z", "updated_at": "2016-07-22T08:55:44.161Z",
"template": false, "template": false,
"description": "", "description": "",
"priority": null "type": "ProjectLabel"
} }
} }
], ],
...@@ -2226,9 +2271,6 @@ ...@@ -2226,9 +2271,6 @@
} }
] ]
} }
],
"labels": [
], ],
"milestones": [ "milestones": [
{ {
......
...@@ -32,7 +32,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do ...@@ -32,7 +32,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
it 'has the same label associated to two issues' do it 'has the same label associated to two issues' do
restored_project_json restored_project_json
expect(Label.first.issues.count).to eq(2) expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2)
end end
it 'has milestones associated to two separate issues' do it 'has milestones associated to two separate issues' do
...@@ -107,6 +107,41 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do ...@@ -107,6 +107,41 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(Label.first.label_links.first.target).not_to be_nil expect(Label.first.label_links.first.target).not_to be_nil
end end
it 'has project labels' do
restored_project_json
expect(ProjectLabel.count).to eq(2)
end
it 'has no group labels' do
restored_project_json
expect(GroupLabel.count).to eq(0)
end
context 'with group' do
let!(:project) do
create(:empty_project,
name: 'project',
path: 'project',
builds_access_level: ProjectFeature::DISABLED,
issues_access_level: ProjectFeature::DISABLED,
group: create(:group))
end
it 'has group labels' do
restored_project_json
expect(GroupLabel.count).to eq(1)
end
it 'has label priorities' do
restored_project_json
expect(GroupLabel.first.priorities).not_to be_empty
end
end
it 'has a project feature' do it 'has a project feature' do
restored_project_json restored_project_json
......
...@@ -111,6 +111,18 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do ...@@ -111,6 +111,18 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty
end end
it 'has project and group labels' do
label_types = saved_project_json['issues'].first['label_links'].map { |link| link['label']['type']}
expect(label_types).to match_array(['ProjectLabel', 'GroupLabel'])
end
it 'has priorities associated to labels' do
priorities = saved_project_json['issues'].first['label_links'].map { |link| link['label']['priorities']}
expect(priorities.flatten).not_to be_empty
end
it 'saves the correct service type' do it 'saves the correct service type' do
expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService') expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService')
end end
...@@ -135,15 +147,20 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do ...@@ -135,15 +147,20 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
issue = create(:issue, assignee: user) issue = create(:issue, assignee: user)
snippet = create(:project_snippet) snippet = create(:project_snippet)
release = create(:release) release = create(:release)
group = create(:group)
project = create(:project, project = create(:project,
:public, :public,
issues: [issue], issues: [issue],
snippets: [snippet], snippets: [snippet],
releases: [release] releases: [release],
group: group
) )
label = create(:label, project: project) project_label = create(:label, project: project)
create(:label_link, label: label, target: issue) group_label = create(:group_label, group: group)
create(:label_link, label: project_label, target: issue)
create(:label_link, label: group_label, target: issue)
create(:label_priority, label: group_label, priority: 1)
milestone = create(:milestone, project: project) milestone = create(:milestone, project: project)
merge_request = create(:merge_request, source_project: project, milestone: milestone) merge_request = create(:merge_request, source_project: project, milestone: milestone)
commit_status = create(:commit_status, project: project) commit_status = create(:commit_status, project: project)
......
...@@ -60,11 +60,13 @@ LabelLink: ...@@ -60,11 +60,13 @@ LabelLink:
- target_type - target_type
- created_at - created_at
- updated_at - updated_at
Label: ProjectLabel:
- id - id
- title - title
- color - color
- group_id
- project_id - project_id
- type
- created_at - created_at
- updated_at - updated_at
- template - template
...@@ -329,3 +331,10 @@ AwardEmoji: ...@@ -329,3 +331,10 @@ AwardEmoji:
- awardable_type - awardable_type
- created_at - created_at
- updated_at - updated_at
LabelPriority:
- id
- project_id
- label_id
- priority
- created_at
- updated_at
\ No newline at end of file
require 'spec_helper'
describe GroupLabel, models: true do
describe 'relationships' do
it { is_expected.to belong_to(:group) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:group) }
end
describe '#subject' do
it 'aliases group to subject' do
subject = described_class.new(group: build(:group))
expect(subject.subject).to be(subject.group)
end
end
describe '#to_reference' do
let(:label) { create(:group_label) }
context 'using id' do
it 'returns a String reference to the object' do
expect(label.to_reference).to eq "~#{label.id}"
end
end
context 'using name' do
it 'returns a String reference to the object' do
expect(label.to_reference(format: :name)).to eq %(~"#{label.name}")
end
it 'uses id when name contains double quote' do
label = create(:label, name: %q{"irony"})
expect(label.to_reference(format: :name)).to eq "~#{label.id}"
end
end
context 'using invalid format' do
it 'raises error' do
expect { label.to_reference(format: :invalid) }
.to raise_error StandardError, /Unknown format/
end
end
end
end
...@@ -12,6 +12,7 @@ describe Group, models: true do ...@@ -12,6 +12,7 @@ describe Group, models: true do
it { is_expected.to have_many(:project_group_links).dependent(:destroy) } it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
it { is_expected.to have_many(:shared_projects).through(:project_group_links) } it { is_expected.to have_many(:shared_projects).through(:project_group_links) }
it { is_expected.to have_many(:notification_settings).dependent(:destroy) } it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
it { is_expected.to have_many(:labels).class_name('GroupLabel') }
describe '#members & #requesters' do describe '#members & #requesters' do
let(:requester) { create(:user) } let(:requester) { create(:user) }
......
require 'spec_helper'
describe LabelPriority, models: true do
describe 'relationships' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:label) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:label) }
it { is_expected.to validate_numericality_of(:priority).only_integer.is_greater_than_or_equal_to(0) }
it 'validates uniqueness of label_id scoped to project_id' do
create(:label_priority)
expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:project_id)
end
end
end
require 'spec_helper' require 'spec_helper'
describe Label, models: true do describe Label, models: true do
let(:label) { create(:label) } describe 'modules' do
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Subscribable) }
end
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:label_links).dependent(:destroy) }
it { is_expected.to have_many(:issues).through(:label_links).source(:target) } it { is_expected.to have_many(:issues).through(:label_links).source(:target) }
it { is_expected.to have_many(:label_links).dependent(:destroy) }
it { is_expected.to have_many(:lists).dependent(:destroy) } it { is_expected.to have_many(:lists).dependent(:destroy) }
end it { is_expected.to have_many(:priorities).class_name('LabelPriority') }
describe 'modules' do
subject { described_class }
it { is_expected.to include_module(Referable) }
end end
describe 'validation' do describe 'validation' do
it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_uniqueness_of(:title).scoped_to([:group_id, :project_id]) }
it 'validates color code' do it 'validates color code' do
expect(label).not_to allow_value('G-ITLAB').for(:color) is_expected.not_to allow_value('G-ITLAB').for(:color)
expect(label).not_to allow_value('AABBCC').for(:color) is_expected.not_to allow_value('AABBCC').for(:color)
expect(label).not_to allow_value('#AABBCCEE').for(:color) is_expected.not_to allow_value('#AABBCCEE').for(:color)
expect(label).not_to allow_value('GGHHII').for(:color) is_expected.not_to allow_value('GGHHII').for(:color)
expect(label).not_to allow_value('#').for(:color) is_expected.not_to allow_value('#').for(:color)
expect(label).not_to allow_value('').for(:color) is_expected.not_to allow_value('').for(:color)
expect(label).to allow_value('#AABBCC').for(:color) is_expected.to allow_value('#AABBCC').for(:color)
expect(label).to allow_value('#abcdef').for(:color) is_expected.to allow_value('#abcdef').for(:color)
end end
it 'validates title' do it 'validates title' do
expect(label).not_to allow_value('G,ITLAB').for(:title) is_expected.not_to allow_value('G,ITLAB').for(:title)
expect(label).not_to allow_value('').for(:title) is_expected.not_to allow_value('').for(:title)
expect(label).to allow_value('GITLAB').for(:title) is_expected.to allow_value('GITLAB').for(:title)
expect(label).to allow_value('gitlab').for(:title) is_expected.to allow_value('gitlab').for(:title)
expect(label).to allow_value('G?ITLAB').for(:title) is_expected.to allow_value('G?ITLAB').for(:title)
expect(label).to allow_value('G&ITLAB').for(:title) is_expected.to allow_value('G&ITLAB').for(:title)
expect(label).to allow_value("customer's request").for(:title) is_expected.to allow_value("customer's request").for(:title)
end end
end end
...@@ -51,45 +47,59 @@ describe Label, models: true do ...@@ -51,45 +47,59 @@ describe Label, models: true do
end end
end end
describe '#to_reference' do describe 'priorization' do
context 'using id' do subject(:label) { create(:label) }
it 'returns a String reference to the object' do
expect(label.to_reference).to eq "~#{label.id}" let(:project) { label.project }
describe '#prioritize!' do
context 'when label is not prioritized' do
it 'creates a label priority' do
expect { label.prioritize!(project, 1) }.to change(label.priorities, :count).by(1)
end
it 'sets label priority' do
label.prioritize!(project, 1)
expect(label.priorities.first.priority).to eq 1
end end
end end
context 'using name' do context 'when label is prioritized' do
it 'returns a String reference to the object' do let!(:priority) { create(:label_priority, project: project, label: label, priority: 0) }
expect(label.to_reference(format: :name)).to eq %(~"#{label.name}")
it 'does not create a label priority' do
expect { label.prioritize!(project, 1) }.not_to change(label.priorities, :count)
end end
it 'uses id when name contains double quote' do it 'updates label priority' do
label = create(:label, name: %q{"irony"}) label.prioritize!(project, 1)
expect(label.to_reference(format: :name)).to eq "~#{label.id}"
expect(priority.reload.priority).to eq 1
end
end end
end end
context 'using invalid format' do describe '#unprioritize!' do
it 'raises error' do it 'removes label priority' do
expect { label.to_reference(format: :invalid) } create(:label_priority, project: project, label: label, priority: 0)
.to raise_error StandardError, /Unknown format/
expect { label.unprioritize!(project) }.to change(label.priorities, :count).by(-1)
end end
end end
context 'cross project reference' do describe '#priority' do
let(:project) { create(:project) } context 'when label is not prioritized' do
it 'returns nil' do
context 'using name' do expect(label.priority(project)).to be_nil
it 'returns cross reference with label name' do
expect(label.to_reference(project, format: :name))
.to eq %Q(#{label.project.to_reference}~"#{label.name}")
end end
end end
context 'using id' do context 'when label is prioritized' do
it 'returns cross reference with label id' do it 'returns label priority' do
expect(label.to_reference(project, format: :id)) create(:label_priority, project: project, label: label, priority: 1)
.to eq %Q(#{label.project.to_reference}~#{label.id})
expect(label.priority(project)).to eq 1
end end
end end
end end
......
require 'spec_helper'
describe ProjectLabel, models: true do
describe 'relationships' do
it { is_expected.to belong_to(:project) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
context 'validates if title must not exist at group level' do
let(:group) { create(:group, name: 'gitlab-org') }
let(:project) { create(:empty_project, group: group) }
before do
create(:group_label, group: group, title: 'Bug')
end
it 'returns error if title already exists at group level' do
label = described_class.new(project: project, title: 'Bug')
label.valid?
expect(label.errors[:title]).to include 'already exists at group level for gitlab-org. Please choose another one.'
end
it 'does not returns error if title does not exist at group level' do
label = described_class.new(project: project, title: 'Security')
label.valid?
expect(label.errors[:title]).to be_empty
end
it 'does not returns error if project does not belong to group' do
another_project = create(:empty_project)
label = described_class.new(project: another_project, title: 'Bug')
label.valid?
expect(label.errors[:title]).to be_empty
end
it 'does not returns error when title does not change' do
project_label = create(:label, project: project, name: 'Security')
create(:group_label, group: group, name: 'Security')
project_label.description = 'Security related stuff.'
project_label.valid?
expect(project_label.errors[:title]).to be_empty
end
end
context 'when attempting to add more than one priority to the project label' do
it 'returns error' do
subject.priorities.build
subject.priorities.build
subject.valid?
expect(subject.errors[:priorities]).to include 'Number of permitted priorities exceeded'
end
end
end
describe '#subject' do
it 'aliases project to subject' do
subject = described_class.new(project: build(:empty_project))
expect(subject.subject).to be(subject.project)
end
end
describe '#to_reference' do
let(:label) { create(:label) }
context 'using id' do
it 'returns a String reference to the object' do
expect(label.to_reference).to eq "~#{label.id}"
end
end
context 'using name' do
it 'returns a String reference to the object' do
expect(label.to_reference(format: :name)).to eq %(~"#{label.name}")
end
it 'uses id when name contains double quote' do
label = create(:label, name: %q{"irony"})
expect(label.to_reference(format: :name)).to eq "~#{label.id}"
end
end
context 'using invalid format' do
it 'raises error' do
expect { label.to_reference(format: :invalid) }
.to raise_error StandardError, /Unknown format/
end
end
context 'cross project reference' do
let(:project) { create(:project) }
context 'using name' do
it 'returns cross reference with label name' do
expect(label.to_reference(project, format: :name))
.to eq %Q(#{label.project.to_reference}~"#{label.name}")
end
end
context 'using id' do
it 'returns cross reference with label id' do
expect(label.to_reference(project, format: :id))
.to eq %Q(#{label.project.to_reference}~#{label.id})
end
end
end
end
end
...@@ -56,7 +56,7 @@ describe Project, models: true do ...@@ -56,7 +56,7 @@ describe Project, models: true do
it { is_expected.to have_many(:runners) } it { is_expected.to have_many(:runners) }
it { is_expected.to have_many(:variables) } it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:triggers) } it { is_expected.to have_many(:triggers) }
it { is_expected.to have_many(:labels).dependent(:destroy) } it { is_expected.to have_many(:labels).class_name('ProjectLabel').dependent(:destroy) }
it { is_expected.to have_many(:users_star_projects).dependent(:destroy) } it { is_expected.to have_many(:users_star_projects).dependent(:destroy) }
it { is_expected.to have_many(:environments).dependent(:destroy) } it { is_expected.to have_many(:environments).dependent(:destroy) }
it { is_expected.to have_many(:deployments).dependent(:destroy) } it { is_expected.to have_many(:deployments).dependent(:destroy) }
......
...@@ -106,9 +106,20 @@ describe API::API, api: true do ...@@ -106,9 +106,20 @@ describe API::API, api: true do
describe "POST /projects/:id/board/lists" do describe "POST /projects/:id/board/lists" do
let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
it 'creates a new issue board list' do it 'creates a new issue board list for group labels' do
post api(base_url, user), group = create(:group)
label_id: ux_label.id group_label = create(:group_label, group: group)
project.update(group: group)
post api(base_url, user), label_id: group_label.id
expect(response).to have_http_status(201)
expect(json_response['label']['name']).to eq(group_label.title)
expect(json_response['position']).to eq(3)
end
it 'creates a new issue board list for project labels' do
post api(base_url, user), label_id: ux_label.id
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
expect(json_response['label']['name']).to eq(ux_label.title) expect(json_response['label']['name']).to eq(ux_label.title)
...@@ -116,15 +127,13 @@ describe API::API, api: true do ...@@ -116,15 +127,13 @@ describe API::API, api: true do
end end
it 'returns 400 when creating a new list if label_id is invalid' do it 'returns 400 when creating a new list if label_id is invalid' do
post api(base_url, user), post api(base_url, user), label_id: 23423
label_id: 23423
expect(response).to have_http_status(400) expect(response).to have_http_status(400)
end end
it "returns 403 for project members with guest role" do it 'returns 403 for project members with guest role' do
put api("#{base_url}/#{test_list.id}", guest), put api("#{base_url}/#{test_list.id}", guest), position: 1
position: 1
expect(response).to have_http_status(403) expect(response).to have_http_status(403)
end end
......
...@@ -12,12 +12,18 @@ describe API::API, api: true do ...@@ -12,12 +12,18 @@ describe API::API, api: true do
end end
describe 'GET /projects/:id/labels' do describe 'GET /projects/:id/labels' do
it 'returns project labels' do it 'returns all available labels to the project' do
group = create(:group)
group_label = create(:group_label, group: group)
project.update(group: group)
get api("/projects/#{project.id}/labels", user) get api("/projects/#{project.id}/labels", user)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.size).to eq(1) expect(json_response.size).to eq(2)
expect(json_response.first['name']).to eq(label1.name) expect(json_response.first['name']).to eq(group_label.name)
expect(json_response.second['name']).to eq(label1.name)
end end
end end
......
...@@ -9,6 +9,10 @@ describe Boards::Lists::CreateService, services: true do ...@@ -9,6 +9,10 @@ describe Boards::Lists::CreateService, services: true do
subject(:service) { described_class.new(project, user, label_id: label.id) } subject(:service) { described_class.new(project, user, label_id: label.id) }
before do
project.team << [user, :developer]
end
context 'when board lists is empty' do context 'when board lists is empty' do
it 'creates a new list at beginning of the list' do it 'creates a new list at beginning of the list' do
list = service.execute(board) list = service.execute(board)
......
...@@ -8,6 +8,10 @@ describe Boards::Lists::GenerateService, services: true do ...@@ -8,6 +8,10 @@ describe Boards::Lists::GenerateService, services: true do
subject(:service) { described_class.new(project, user) } subject(:service) { described_class.new(project, user) }
before do
project.team << [user, :developer]
end
context 'when board lists is empty' do context 'when board lists is empty' do
it 'creates the default lists' do it 'creates the default lists' do
expect { service.execute(board) }.to change(board.lists, :count).by(2) expect { service.execute(board) }.to change(board.lists, :count).by(2)
......
...@@ -67,6 +67,27 @@ describe Issues::CreateService, services: true do ...@@ -67,6 +67,27 @@ describe Issues::CreateService, services: true do
expect(Todo.where(attributes).count).to eq 1 expect(Todo.where(attributes).count).to eq 1
end end
context 'when label belongs to project group' do
let(:group) { create(:group) }
let(:group_labels) { create_pair(:group_label, group: group) }
let(:opts) do
{
title: 'Title',
description: 'Description',
label_ids: group_labels.map(&:id)
}
end
before do
project.update(group: group)
end
it 'assigns group labels' do
expect(issue.labels).to match_array group_labels
end
end
context 'when label belongs to different project' do context 'when label belongs to different project' do
let(:label) { create(:label) } let(:label) { create(:label) }
......
require 'spec_helper'
describe Labels::FindOrCreateService, services: true do
describe '#execute' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
let(:params) do
{
title: 'Security',
description: 'Security related stuff.',
color: '#FF0000'
}
end
subject(:service) { described_class.new(user, project, params) }
before do
project.team << [user, :developer]
end
context 'when label does not exist at group level' do
it 'creates a new label at project level' do
expect { service.execute }.to change(project.labels, :count).by(1)
end
end
context 'when label exists at group level' do
it 'returns the group label' do
group_label = create(:group_label, group: group, title: 'Security')
expect(service.execute).to eq group_label
end
end
context 'when label does not exist at group level' do
it 'creates a new label at project leve' do
expect { service.execute }.to change(project.labels, :count).by(1)
end
end
context 'when label exists at project level' do
it 'returns the project label' do
project_label = create(:label, project: project, title: 'Security')
expect(service.execute).to eq project_label
end
end
end
end
require 'spec_helper'
describe Labels::TransferService, services: true do
describe '#execute' do
let(:user) { create(:user) }
let(:group_1) { create(:group) }
let(:group_2) { create(:group) }
let(:group_3) { create(:group) }
let(:project_1) { create(:project, namespace: group_2) }
let(:project_2) { create(:project, namespace: group_3) }
let(:group_label_1) { create(:group_label, group: group_1, name: 'Group Label 1') }
let(:group_label_2) { create(:group_label, group: group_1, name: 'Group Label 2') }
let(:group_label_3) { create(:group_label, group: group_1, name: 'Group Label 3') }
let(:group_label_4) { create(:group_label, group: group_2, name: 'Group Label 4') }
let(:group_label_5) { create(:group_label, group: group_3, name: 'Group Label 5') }
let(:project_label_1) { create(:label, project: project_1, name: 'Project Label 1') }
subject(:service) { described_class.new(user, group_1, project_1) }
before do
create(:labeled_issue, project: project_1, labels: [group_label_1])
create(:labeled_issue, project: project_1, labels: [group_label_4])
create(:labeled_issue, project: project_1, labels: [project_label_1])
create(:labeled_issue, project: project_2, labels: [group_label_5])
create(:labeled_merge_request, source_project: project_1, labels: [group_label_1, group_label_2])
create(:labeled_merge_request, source_project: project_2, labels: [group_label_5])
end
it 'recreates the missing group labels at project level' do
expect { service.execute }.to change(project_1.labels, :count).by(2)
end
it 'recreates label priorities related to the missing group labels' do
create(:label_priority, project: project_1, label: group_label_1, priority: 1)
service.execute
new_project_label = project_1.labels.find_by(title: group_label_1.title)
expect(new_project_label.id).not_to eq group_label_1.id
expect(new_project_label.priorities).not_to be_empty
end
it 'does not recreate missing group labels that are not applied to issues or merge requests' do
service.execute
expect(project_1.labels.where(title: group_label_3.title)).to be_empty
end
it 'does not recreate missing group labels that already exist in the project group' do
service.execute
expect(project_1.labels.where(title: group_label_4.title)).to be_empty
end
end
end
...@@ -71,4 +71,14 @@ describe Projects::TransferService, services: true do ...@@ -71,4 +71,14 @@ describe Projects::TransferService, services: true do
it { expect(private_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) } it { expect(private_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) }
end end
end end
context 'missing group labels applied to issues or merge requests' do
it 'delegates tranfer to Labels::TransferService' do
group.add_owner(user)
expect_any_instance_of(Labels::TransferService).to receive(:execute).once.and_call_original
transfer_project(project, user, group)
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment