Commit 86f1f833 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'epic/refactor-labels' into 'master'

Epic/refactor labels

Part of #1385

See merge request !1003
parents 4c8cbf88 d9d8d3b7
...@@ -94,7 +94,7 @@ ...@@ -94,7 +94,7 @@
} }
} }
.issue-show-labels .label { .issue-show-labels .color-label {
padding: 6px 10px; padding: 6px 10px;
} }
......
.suggest-colors {
margin-top: 5px;
a {
@include border-radius(4px);
width: 30px;
height: 30px;
display: inline-block;
margin-right: 10px;
}
}
.manage-labels-list {
.label {
padding: 9px;
font-size: 14px;
}
}
.color-label {
padding: 3px 4px;
}
...@@ -46,11 +46,11 @@ class DashboardController < ApplicationController ...@@ -46,11 +46,11 @@ class DashboardController < ApplicationController
@projects = @projects.where(namespace_id: Group.find_by(name: params[:group])) if params[:group].present? @projects = @projects.where(namespace_id: Group.find_by(name: params[:group])) if params[:group].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = @projects.includes(:namespace) @projects = @projects.includes(:namespace)
@projects = @projects.tagged_with(params[:label]) if params[:label].present? @projects = @projects.tagged_with(params[:tag]) if params[:tag].present?
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(30) @projects = @projects.page(params[:page]).per(30)
@labels = current_user.authorized_projects.tags_on(:labels) @tags = current_user.authorized_projects.tags_on(:tags)
@groups = current_user.authorized_groups @groups = current_user.authorized_groups
end end
......
...@@ -152,7 +152,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -152,7 +152,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params def issue_params
params.require(:issue).permit( params.require(:issue).permit(
:title, :assignee_id, :position, :description, :title, :assignee_id, :position, :description,
:milestone_id, :label_list, :state_event :milestone_id, :state_event, label_ids: []
) )
end end
end end
class Projects::LabelsController < Projects::ApplicationController class Projects::LabelsController < Projects::ApplicationController
before_filter :module_enabled before_filter :module_enabled
before_filter :label, only: [:edit, :update, :destroy]
before_filter :authorize_labels! before_filter :authorize_labels!
before_filter :authorize_admin_labels!, except: [:index]
respond_to :js, :html respond_to :js, :html
def index def index
@labels = @project.issues_labels @labels = @project.labels.order('title ASC').page(params[:page]).per(20)
end
def new
@label = @project.labels.new
end
def create
@label = @project.labels.create(label_params)
if @label.valid?
redirect_to project_labels_path(@project)
else
render 'new'
end
end
def edit
end
def update
if @label.update_attributes(label_params)
redirect_to project_labels_path(@project)
else
render 'edit'
end
end end
def generate def generate
...@@ -21,6 +47,12 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -21,6 +47,12 @@ class Projects::LabelsController < Projects::ApplicationController
end end
end end
def destroy
@label.destroy
redirect_to project_labels_path(@project), notice: 'Label was removed'
end
protected protected
def module_enabled def module_enabled
...@@ -28,4 +60,16 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -28,4 +60,16 @@ class Projects::LabelsController < Projects::ApplicationController
return render_404 return render_404
end end
end end
def label_params
params.require(:label).permit(:title, :color)
end
def label
@label = @project.labels.find(params[:id])
end
def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @project)
end
end end
...@@ -242,7 +242,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -242,7 +242,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
params.require(:merge_request).permit( params.require(:merge_request).permit(
:title, :assignee_id, :source_project_id, :source_branch, :title, :assignee_id, :source_project_id, :source_branch,
:target_project_id, :target_branch, :milestone_id, :target_project_id, :target_branch, :milestone_id,
:state_event, :description, :label_list :state_event, :description, label_ids: []
) )
end end
end end
...@@ -196,7 +196,7 @@ class ProjectsController < ApplicationController ...@@ -196,7 +196,7 @@ class ProjectsController < ApplicationController
def project_params def project_params
params.require(:project).permit( params.require(:project).permit(
:name, :path, :description, :issues_tracker, :label_list, :name, :path, :description, :issues_tracker, :tag_list,
:issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch, :issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch,
:wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id
) )
......
...@@ -125,7 +125,13 @@ class BaseFinder ...@@ -125,7 +125,13 @@ class BaseFinder
def by_label(items) def by_label(items)
if params[:label_name].present? if params[:label_name].present?
items = items.tagged_with(params[:label_name]) label_names = params[:label_name].split(",")
item_ids = LabelLink.joins(:label).
where('labels.title in (?)', label_names).
where(target_type: klass.name).pluck(:target_id)
items = items.where(id: item_ids)
end end
items items
......
module LabelsHelper module LabelsHelper
def issue_label_names def project_label_names
@project.issues_labels.map(&:name) @project.labels.pluck(:title)
end end
def labels_autocomplete_source def render_colored_label(label)
labels = @project.issues_labels label_color = label.color || "#428bca"
labels = labels.map{ |l| { label: l.name, value: l.name } } text_color = text_color_for_bg(label_color)
labels.to_json
content_tag :span, class: 'label color-label', style: "background:#{label_color};color:#{text_color}" do
label.name
end
end
def suggested_colors
[
'#d9534f',
'#f0ad4e',
'#428bca',
'#5cb85c',
'#34495e',
'#7f8c8d',
'#8e44ad',
'#FFECDB'
]
end end
def label_css_class(name) def text_color_for_bg(bg_color)
klass = Gitlab::IssuesLabels r, g, b = bg_color.slice(1,7).scan(/.{2}/).map(&:hex)
case name.downcase if (r + g + b) > 500
when *klass.warning_labels "#333"
'label-warning'
when *klass.neutral_labels
'label-primary'
when *klass.positive_labels
'label-success'
when *klass.important_labels
'label-danger'
else else
'label-info' "#FFF"
end end
end end
end end
...@@ -142,6 +142,7 @@ class Ability ...@@ -142,6 +142,7 @@ class Ability
:write_wiki, :write_wiki,
:modify_issue, :modify_issue,
:admin_issue, :admin_issue,
:admin_label,
:push_code :push_code
] ]
end end
......
...@@ -13,6 +13,8 @@ module Issuable ...@@ -13,6 +13,8 @@ module Issuable
belongs_to :assignee, class_name: "User" belongs_to :assignee, class_name: "User"
belongs_to :milestone belongs_to :milestone
has_many :notes, as: :noteable, dependent: :destroy has_many :notes, as: :noteable, dependent: :destroy
has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links
validates :author, presence: true validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 } validates :title, presence: true, length: { within: 0..255 }
...@@ -131,4 +133,15 @@ module Issuable ...@@ -131,4 +133,15 @@ module Issuable
object_attributes: self.attributes object_attributes: self.attributes
} }
end end
def label_names
labels.order('title ASC').pluck(:title)
end
def add_labels_by_names(label_names)
label_names.each do |label_name|
label = project.labels.find_or_create_by(title: label_name.strip)
self.labels << label
end
end
end end
...@@ -32,9 +32,6 @@ class Issue < ActiveRecord::Base ...@@ -32,9 +32,6 @@ class Issue < ActiveRecord::Base
scope :of_group, ->(group) { where(project_id: group.project_ids) } scope :of_group, ->(group) { where(project_id: group.project_ids) }
scope :of_user_team, ->(team) { where(project_id: team.project_ids, assignee_id: team.member_ids) } scope :of_user_team, ->(team) { where(project_id: team.project_ids, assignee_id: team.member_ids) }
acts_as_taggable_on :labels
scope :cared, ->(user) { where(assignee_id: user) } scope :cared, ->(user) { where(assignee_id: user) }
scope :open_for, ->(user) { opened.assigned_to(user) } scope :open_for, ->(user) { opened.assigned_to(user) }
......
class Label < ActiveRecord::Base
belongs_to :project
has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
validates :color, format: { with: /\A\#[0-9A-Fa-f]{6}+\Z/ }, allow_blank: true
validates :project, presence: true
# Dont allow '?', '&', and ',' for label titles
validates :title, presence: true, format: { with: /\A[^&\?,&]*\z/ }
def name
title
end
def open_issues_count
issues.opened.count
end
end
class LabelLink < ActiveRecord::Base
belongs_to :target, polymorphic: true
belongs_to :label
validates :target, presence: true
validates :label, presence: true
end
...@@ -47,9 +47,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -47,9 +47,6 @@ class MergeRequest < ActiveRecord::Base
attr_accessor :can_be_created, :compare_failed, attr_accessor :can_be_created, :compare_failed,
:compare_commits, :compare_diffs :compare_commits, :compare_diffs
ActsAsTaggableOn.strict_case_match = true
acts_as_taggable_on :labels
state_machine :state, initial: :opened do state_machine :state, initial: :opened do
event :close do event :close do
transition [:reopened, :opened] => :closed transition [:reopened, :opened] => :closed
......
...@@ -41,8 +41,7 @@ class Project < ActiveRecord::Base ...@@ -41,8 +41,7 @@ class Project < ActiveRecord::Base
default_value_for :snippets_enabled, gitlab_config_features.snippets default_value_for :snippets_enabled, gitlab_config_features.snippets
ActsAsTaggableOn.strict_case_match = true ActsAsTaggableOn.strict_case_match = true
acts_as_taggable_on :tags
acts_as_taggable_on :labels, :issues_default_labels
attr_accessor :new_default_branch attr_accessor :new_default_branch
...@@ -71,6 +70,7 @@ class Project < ActiveRecord::Base ...@@ -71,6 +70,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, -> { order "state DESC, created_at DESC" }, dependent: :destroy has_many :issues, -> { order "state DESC, created_at DESC" }, dependent: :destroy
has_many :labels, dependent: :destroy
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
...@@ -282,13 +282,6 @@ class Project < ActiveRecord::Base ...@@ -282,13 +282,6 @@ class Project < ActiveRecord::Base
self.id self.id
end end
# Tags are shared by issues and merge requests
def issues_labels
@issues_labels ||= (issues_default_labels +
merge_requests.tags_on(:labels) +
issues.tags_on(:labels)).uniq.sort_by(&:name)
end
def issue_exists?(issue_id) def issue_exists?(issue_id)
if used_default_issues_tracker? if used_default_issues_tracker?
self.issues.where(iid: issue_id).first.present? self.issues.where(iid: issue_id).first.present?
......
...@@ -44,12 +44,12 @@ ...@@ -44,12 +44,12 @@
- if @labels.present? - if @tags.present?
%fieldset %fieldset
%legend Labels %legend Tags
%ul.nav.nav-pills.nav-stacked.nav-small %ul.nav.nav-pills.nav-stacked.nav-small
- @labels.each do |label| - @tags.each do |tag|
%li{ class: (label.name == params[:label]) ? 'active' : 'light' } %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' }
= link_to projects_dashboard_filter_path(scope: params[:scope], label: label.name) do = link_to projects_dashboard_filter_path(scope: params[:scope], tag: tag.name) do
%i.icon-tag %i.icon-tag
= label.name = tag.name
...@@ -54,10 +54,10 @@ ...@@ -54,10 +54,10 @@
%span.label %span.label
%i.icon-archive %i.icon-archive
Archived Archived
- project.labels.each do |label| - project.tags.each do |tag|
%span.label.label-info %span.label.label-info
%i.icon-tag %i.icon-tag
= label.name = tag.name
- if project.description.present? - if project.description.present?
%p= truncate project.description, length: 100 %p= truncate project.description, length: 100
.last-activity .last-activity
......
...@@ -33,12 +33,12 @@ ...@@ -33,12 +33,12 @@
%fieldset.features %fieldset.features
%legend %legend
Labels: Tags:
.form-group .form-group
= f.label :label_list, "Labels", class: 'control-label' = f.label :tag_list, "Tags", class: 'control-label'
.col-sm-10 .col-sm-10
= f.text_field :label_list, maxlength: 2000, class: "form-control" = f.text_field :tag_list, maxlength: 2000, class: "form-control"
%p.hint Separate labels with commas. %p.hint Separate tags with commas.
%fieldset.features %fieldset.features
%legend %legend
......
...@@ -44,14 +44,11 @@ ...@@ -44,14 +44,11 @@
.col-sm-10= f.select(:milestone_id, milestone_options(@issue), { include_blank: "Select milestone" }, {class: 'select2'}) .col-sm-10= f.select(:milestone_id, milestone_options(@issue), { include_blank: "Select milestone" }, {class: 'select2'})
.form-group .form-group
= f.label :label_list, class: 'control-label' do = f.label :label_ids, class: 'control-label' do
%i.icon-tag %i.icon-tag
Labels Labels
.col-sm-10 .col-sm-10
= f.text_field :label_list, maxlength: 2000, class: "form-control" = f.collection_select :label_ids, @project.labels.all, :id, :name, { selected: @issue.label_ids }, multiple: true, class: 'select2'
%p.hint Separate labels with commas.
.form-actions .form-actions
- if @issue.new_record? - if @issue.new_record?
...@@ -63,35 +60,6 @@ ...@@ -63,35 +60,6 @@
= link_to "Cancel", cancel_path, class: 'btn btn-cancel' = link_to "Cancel", cancel_path, class: 'btn btn-cancel'
:javascript :javascript
$("#issue_label_list")
.bind( "keydown", function( event ) {
if ( event.keyCode === $.ui.keyCode.TAB &&
$( this ).data( "autocomplete" ).menu.active ) {
event.preventDefault();
}
})
.bind("click", function(event) {
$(this).autocomplete("search", "");
})
.autocomplete({
minLength: 0,
source: function( request, response ) {
response( $.ui.autocomplete.filter(
#{raw labels_autocomplete_source}, extractLast( request.term ) ) );
},
focus: function() {
return false;
},
select: function(event, ui) {
var terms = split( this.value );
terms.pop();
terms.push( ui.item.value );
terms.push( "" );
this.value = terms.join( ", " );
return false;
}
});
$('.assign-to-me-link').on('click', function(e){ $('.assign-to-me-link').on('click', function(e){
$('#issue_assignee_id').val("#{current_user.id}").trigger("change"); $('#issue_assignee_id').val("#{current_user.id}").trigger("change");
e.preventDefault(); e.preventDefault();
......
...@@ -31,9 +31,7 @@ ...@@ -31,9 +31,7 @@
.issue-labels .issue-labels
- issue.labels.each do |label| - issue.labels.each do |label|
%span{class: "label #{label_css_class(label.name)}"} = render_colored_label(label)
%i.icon-tag
= label.name
.issue-actions .issue-actions
- if can? current_user, :modify_issue, issue - if can? current_user, :modify_issue, issue
......
...@@ -68,9 +68,6 @@ ...@@ -68,9 +68,6 @@
.issue-show-labels.pull-right .issue-show-labels.pull-right
- @issue.labels.each do |label| - @issue.labels.each do |label|
%span{class: "label #{label_css_class(label.name)}"} = render_colored_label(label)
%i.icon-tag
= label.name
&nbsp;
.voting_notes#notes= render "projects/notes/notes_with_form" .voting_notes#notes= render "projects/notes/notes_with_form"
= form_for [@project, @label], html: { class: 'form-horizontal label-form' } do |f|
-if @label.errors.any?
.row
.col-sm-10.col-sm-offset-2
.bs-callout.bs-callout-danger
- @label.errors.full_messages.each do |msg|
%span= msg
%br
.form-group
= f.label :title, class: 'control-label'
.col-sm-10
= f.text_field :title, class: "form-control", required: true
.form-group
= f.label :color, "Background Color", class: 'control-label'
.col-sm-10
.input-group
.input-group-addon.label-color-preview &nbsp;
= f.text_field :color, placeholder: "#AA33EE", class: "form-control"
.help-block
6 character hex values starting with a # sign.
%br
Or you can choose one of suggested colors below
.suggest-colors
- suggested_colors.each do |color|
= link_to '#', style: "background-color: #{color}", data: { color: color } do
&nbsp;
.form-actions
= f.submit 'Save', class: 'btn btn-save'
:coffeescript
updateColorPreview = ->
previewColor = $('input#label_color').val()
$('div.label-color-preview').css('background-color', previewColor)
$('.suggest-colors a').on 'click', (e) ->
color = $(this).data("color")
$('input#label_color').val(color)
updateColorPreview()
e.preventDefault()
$('input#label_color').on 'input', ->
updateColorPreview()
updateColorPreview()
- frequency = @project.issues.tagged_with(label.name).count
%li %li
%span{class: "label #{label_css_class(label.name)}"} = render_colored_label(label)
%i.icon-tag
- if frequency.zero?
%span.light= label.name
- else
= label.name
.pull-right .pull-right
- unless frequency.zero? %strong.append-right-20
= link_to project_issues_path(label_name: label.name) do = link_to project_issues_path(@project, label_name: label.name) do
= pluralize(frequency, 'issue') = pluralize label.open_issues_count, 'open issue'
= "»"
- if can? current_user, :admin_label, @project
= link_to 'Edit', edit_project_label_path(@project, label), class: 'btn'
= link_to 'Remove', project_label_path(@project, label), class: 'btn btn-remove', method: :delete, data: {confirm: "Remove this label? Are you sure?"}
%h3
Edit label
%span.light #{@label.name}
.back-link
= link_to project_labels_path(@project) do
&larr; To labels list
%hr
= render 'form'
= render "projects/issues/head" = render "projects/issues/head"
- if can? current_user, :admin_label, @project
= link_to new_project_label_path(@project), class: "pull-right btn btn-new" do
New label
%h3.page-title
Labels
%hr
- if @labels.present? - if @labels.present?
%ul.bordered-list.labels-table %ul.bordered-list.manage-labels-list
- @labels.each do |label| = render @labels
= render 'label', label: label
- else - else
.light-well .light-well
......
%h3 New label
.back-link
= link_to project_labels_path(@project) do
&larr; To labels list
%hr
= render 'form'
...@@ -46,14 +46,12 @@ ...@@ -46,14 +46,12 @@
.col-sm-10= f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2'}) .col-sm-10= f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2'})
- if @merge_request.persisted? # Only allow labels on edit to avoid fork vs upstream repo labels issue
.form-group .form-group
= f.label :label_list, class: 'control-label' do = f.label :label_ids, class: 'control-label' do
%i.icon-tag %i.icon-tag
Labels Labels
.col-sm-10 .col-sm-10
= f.text_field :label_list, maxlength: 2000, class: "form-control" = f.collection_select :label_ids, @merge_request.target_project.labels.all, :id, :name, { selected: @merge_request.label_ids }, multiple: true, class: 'select2'
%p.hint Separate labels with commas.
.form-actions .form-actions
- if @merge_request.new_record? - if @merge_request.new_record?
...@@ -74,33 +72,4 @@ ...@@ -74,33 +72,4 @@
e.preventDefault(); e.preventDefault();
}); });
$("#merge_request_label_list")
.bind( "keydown", function( event ) {
if ( event.keyCode === $.ui.keyCode.TAB &&
$( this ).data( "autocomplete" ).menu.active ) {
event.preventDefault();
}
})
.bind("click", function(event) {
$(this).autocomplete("search", "");
})
.autocomplete({
minLength: 0,
source: function( request, response ) {
response( $.ui.autocomplete.filter(
#{raw labels_autocomplete_source}, extractLast( request.term ) ) );
},
focus: function() {
return false;
},
select: function(event, ui) {
var terms = split( this.value );
terms.pop();
terms.push( ui.item.value );
terms.push( "" );
this.value = terms.join( ", " );
return false;
}
});
window.project_image_path_upload = "#{upload_image_project_path @project}"; window.project_image_path_upload = "#{upload_image_project_path @project}";
...@@ -34,6 +34,4 @@ ...@@ -34,6 +34,4 @@
.merge-request-labels .merge-request-labels
- merge_request.labels.each do |label| - merge_request.labels.each do |label|
%span{class: "label #{label_css_class(label.name)}"} = render_colored_label(label)
%i.icon-tag
= label.name
...@@ -43,6 +43,13 @@ ...@@ -43,6 +43,13 @@
%i.icon-time %i.icon-time
Milestone Milestone
%div= f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2'}) %div= f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2'})
.form-group
= f.label :label_ids do
%i.icon-tag
Labels
%div
= f.collection_select :label_ids, @merge_request.target_project.labels.all, :id, :name, { selected: @merge_request.label_ids }, multiple: true, class: 'select2'
.panel-footer .panel-footer
- if contribution_guide_url(@target_project) - if contribution_guide_url(@target_project)
%p %p
......
...@@ -5,7 +5,4 @@ ...@@ -5,7 +5,4 @@
.merge-request-show-labels.pull-right .merge-request-show-labels.pull-right
- @merge_request.labels.each do |label| - @merge_request.labels.each do |label|
%span{class: "label #{label_css_class(label.name)}"} = render_colored_label(label)
%i.icon-tag
= label.name
&nbsp;
...@@ -36,17 +36,15 @@ ...@@ -36,17 +36,15 @@
%fieldset %fieldset
%legend Labels %legend Labels
%ul.nav.nav-pills.nav-stacked.nav-small.labels-filter %ul.nav.nav-pills.nav-stacked.nav-small.labels-filter
- issue_label_names.each do |label_name| - @project.labels.each do |label|
%li{class: label_filter_class(label_name)} %li{class: label_filter_class(label.name)}
= link_to labels_filter_path(label_name) do = link_to labels_filter_path(label.name) do
%span{class: "label #{label_css_class(label_name)}"} = render_colored_label(label)
%i.icon-tag - if selected_label?(label.name)
= label_name
- if selected_label?(label_name)
.pull-right .pull-right
%i.icon-remove %i.icon-remove
- if issue_label_names.empty? - if @project.labels.empty?
.light-well .light-well
Add first label to your issues Add first label to your issues
%br %br
......
...@@ -297,7 +297,7 @@ Gitlab::Application.routes.draw do ...@@ -297,7 +297,7 @@ Gitlab::Application.routes.draw do
end end
end end
resources :labels, only: [:index] do resources :labels, constraints: {id: /\d+/} do
collection do collection do
post :generate post :generate
end end
......
class CreateLabels < ActiveRecord::Migration
def change
create_table :labels do |t|
t.string :title
t.string :color
t.integer :project_id
t.timestamps
end
end
end
class CreateLabelLinks < ActiveRecord::Migration
def change
create_table :label_links do |t|
t.integer :label_id
t.integer :target_id
t.string :target_type
t.timestamps
end
end
end
class MigrateProjectTags < ActiveRecord::Migration
def up
ActsAsTaggableOn::Tagging.where(taggable_type: 'Project', context: 'labels').update_all(context: 'tags')
end
def down
ActsAsTaggableOn::Tagging.where(taggable_type: 'Project', context: 'tags').update_all(context: 'labels')
end
end
class MigrateTaggableLabels < ActiveRecord::Migration
def up
taggings = ActsAsTaggableOn::Tagging.where(taggable_type: ['Issue', 'MergeRequest'], context: 'labels')
taggings.find_each(batch_size: 500) do |tagging|
create_label_from_tagging(tagging)
end
end
def down
Label.destroy_all
LabelLink.destroy_all
end
private
def create_label_from_tagging(tagging)
target = tagging.taggable
label_name = tagging.tag.name
label = target.project.labels.find_or_create_by(title: label_name)
if LabelLink.create(label: label, target: target)
print '.'
else
print 'F'
end
end
end
class AddIndexToLabels < ActiveRecord::Migration
def change
add_index "labels", :project_id
add_index "label_links", :label_id
add_index "label_links", [:target_id, :target_type]
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: 20140625115202) do ActiveRecord::Schema.define(version: 20140730111702) 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"
...@@ -109,6 +109,27 @@ ActiveRecord::Schema.define(version: 20140625115202) do ...@@ -109,6 +109,27 @@ ActiveRecord::Schema.define(version: 20140625115202) do
add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree
create_table "label_links", force: true do |t|
t.integer "label_id"
t.integer "target_id"
t.string "target_type"
t.datetime "created_at"
t.datetime "updated_at"
end
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
create_table "labels", force: true do |t|
t.string "title"
t.string "color"
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
create_table "merge_request_diffs", force: true do |t| create_table "merge_request_diffs", force: true do |t|
t.string "state" t.string "state"
t.text "st_commits" t.text "st_commits"
......
...@@ -2,9 +2,10 @@ Feature: Project Filter Labels ...@@ -2,9 +2,10 @@ Feature: Project Filter Labels
Background: Background:
Given I sign in as a user Given I sign in as a user
And I own project "Shop" And I own project "Shop"
And project "Shop" has issue "Bugfix1" with tags: "bug", "feature" And project "Shop" has labels: "bug", "feature", "enhancement"
And project "Shop" has issue "Bugfix2" with tags: "bug", "enhancement" And project "Shop" has issue "Bugfix1" with labels: "bug", "feature"
And project "Shop" has issue "Feature1" with tags: "feature" And project "Shop" has issue "Bugfix2" with labels: "bug", "enhancement"
And project "Shop" has issue "Feature1" with labels: "feature"
Given I visit project "Shop" issues page Given I visit project "Shop" issues page
Scenario: I should see project issues Scenario: I should see project issues
...@@ -18,9 +19,12 @@ Feature: Project Filter Labels ...@@ -18,9 +19,12 @@ Feature: Project Filter Labels
And I should see "Bugfix2" in issues list And I should see "Bugfix2" in issues list
And I should not see "Feature1" in issues list And I should not see "Feature1" in issues list
Scenario: I filter by two labels # TODO: make labels filter works according to this scanario
Given I click link "bug" # right now it looks for label 1 OR label 2. Old behaviour (this test) was
And I click link "feature" # all issues that have both label 1 AND label 2
Then I should see "Bugfix1" in issues list #Scenario: I filter by two labels
And I should not see "Bugfix2" in issues list #Given I click link "bug"
And I should not see "Feature1" in issues list #And I click link "feature"
#Then I should see "Bugfix1" in issues list
#And I should not see "Bugfix2" in issues list
#And I should not see "Feature1" in issues list
...@@ -2,7 +2,7 @@ Feature: Project Labels ...@@ -2,7 +2,7 @@ Feature: Project Labels
Background: Background:
Given I sign in as a user Given I sign in as a user
And I own project "Shop" And I own project "Shop"
And project "Shop" have issues tags: "bug", "feature" And project "Shop" has labels: "bug", "feature", "enhancement"
Given I visit project "Shop" labels page Given I visit project "Shop" labels page
Scenario: I should see active milestones Scenario: I should see active milestones
......
...@@ -3,68 +3,77 @@ class ProjectFilterLabels < Spinach::FeatureSteps ...@@ -3,68 +3,77 @@ class ProjectFilterLabels < Spinach::FeatureSteps
include SharedProject include SharedProject
include SharedPaths include SharedPaths
Then 'I should see "bug" in labels filter' do step 'I should see "bug" in labels filter' do
within ".labels-filter" do within ".labels-filter" do
page.should have_content "bug" page.should have_content "bug"
end end
end end
And 'I should see "feature" in labels filter' do step 'I should see "feature" in labels filter' do
within ".labels-filter" do within ".labels-filter" do
page.should have_content "feature" page.should have_content "feature"
end end
end end
And 'I should see "enhancement" in labels filter' do step 'I should see "enhancement" in labels filter' do
within ".labels-filter" do within ".labels-filter" do
page.should have_content "enhancement" page.should have_content "enhancement"
end end
end end
Then 'I should see "Bugfix1" in issues list' do step 'I should see "Bugfix1" in issues list' do
within ".issues-list" do within ".issues-list" do
page.should have_content "Bugfix1" page.should have_content "Bugfix1"
end end
end end
And 'I should see "Bugfix2" in issues list' do step 'I should see "Bugfix2" in issues list' do
within ".issues-list" do within ".issues-list" do
page.should have_content "Bugfix2" page.should have_content "Bugfix2"
end end
end end
And 'I should not see "Bugfix2" in issues list' do step 'I should not see "Bugfix2" in issues list' do
within ".issues-list" do within ".issues-list" do
page.should_not have_content "Bugfix2" page.should_not have_content "Bugfix2"
end end
end end
And 'I should not see "Feature1" in issues list' do step 'I should not see "Feature1" in issues list' do
within ".issues-list" do within ".issues-list" do
page.should_not have_content "Feature1" page.should_not have_content "Feature1"
end end
end end
Given 'I click link "bug"' do step 'I click link "bug"' do
within ".labels-filter" do
click_link "bug" click_link "bug"
end end
end
Given 'I click link "feature"' do step 'I click link "feature"' do
within ".labels-filter" do
click_link "feature" click_link "feature"
end end
end
And 'project "Shop" has issue "Bugfix1" with tags: "bug", "feature"' do step 'project "Shop" has issue "Bugfix1" with labels: "bug", "feature"' do
project = Project.find_by(name: "Shop") project = Project.find_by(name: "Shop")
create(:issue, title: "Bugfix1", project: project, label_list: ['bug', 'feature']) issue = create(:issue, title: "Bugfix1", project: project)
issue.labels << project.labels.find_by(title: 'bug')
issue.labels << project.labels.find_by(title: 'feature')
end end
And 'project "Shop" has issue "Bugfix2" with tags: "bug", "enhancement"' do step 'project "Shop" has issue "Bugfix2" with labels: "bug", "enhancement"' do
project = Project.find_by(name: "Shop") project = Project.find_by(name: "Shop")
create(:issue, title: "Bugfix2", project: project, label_list: ['bug', 'enhancement']) issue = create(:issue, title: "Bugfix2", project: project)
issue.labels << project.labels.find_by(title: 'bug')
issue.labels << project.labels.find_by(title: 'enhancement')
end end
And 'project "Shop" has issue "Feature1" with tags: "feature"' do step 'project "Shop" has issue "Feature1" with labels: "feature"' do
project = Project.find_by(name: "Shop") project = Project.find_by(name: "Shop")
create(:issue, title: "Feature1", project: project, label_list: 'feature') issue = create(:issue, title: "Feature1", project: project)
issue.labels << project.labels.find_by(title: 'feature')
end end
end end
...@@ -4,21 +4,14 @@ class ProjectLabels < Spinach::FeatureSteps ...@@ -4,21 +4,14 @@ class ProjectLabels < Spinach::FeatureSteps
include SharedPaths include SharedPaths
Then 'I should see label "bug"' do Then 'I should see label "bug"' do
within ".labels-table" do within ".manage-labels-list" do
page.should have_content "bug" page.should have_content "bug"
end end
end end
And 'I should see label "feature"' do And 'I should see label "feature"' do
within ".labels-table" do within ".manage-labels-list" do
page.should have_content "feature" page.should have_content "feature"
end end
end end
And 'project "Shop" have issues tags: "bug", "feature"' do
project = Project.find_by(name: "Shop")
['bug', 'feature'].each do |label|
create(:issue, project: project, label_list: label)
end
end
end end
...@@ -123,10 +123,6 @@ module SharedProject ...@@ -123,10 +123,6 @@ module SharedProject
project.team << [user, :master] project.team << [user, :master]
end end
# ----------------------------------------
# Empty projects
# ----------------------------------------
step 'public empty project "Empty Public Project"' do step 'public empty project "Empty Public Project"' do
create :empty_project, :public, name: "Empty Public Project" create :empty_project, :public, name: "Empty Public Project"
end end
...@@ -135,4 +131,11 @@ module SharedProject ...@@ -135,4 +131,11 @@ module SharedProject
project = Project.find_by(name: "Community") project = Project.find_by(name: "Community")
2.times { create(:note_on_issue, project: project) } 2.times { create(:note_on_issue, project: project) }
end end
step 'project "Shop" has labels: "bug", "feature", "enhancement"' do
project = Project.find_by(name: "Shop")
create(:label, project: project, title: 'bug')
create(:label, project: project, title: 'feature')
create(:label, project: project, title: 'enhancement')
end
end end
...@@ -126,7 +126,7 @@ module API ...@@ -126,7 +126,7 @@ module API
end end
class Issue < ProjectEntity class Issue < ProjectEntity
expose :label_list, as: :labels expose :label_names, as: :labels
expose :milestone, using: Entities::Milestone expose :milestone, using: Entities::Milestone
expose :assignee, :author, using: Entities::UserBasic expose :assignee, :author, using: Entities::UserBasic
end end
...@@ -135,7 +135,7 @@ module API ...@@ -135,7 +135,7 @@ module API
expose :target_branch, :source_branch, :upvotes, :downvotes expose :target_branch, :source_branch, :upvotes, :downvotes
expose :author, :assignee, using: Entities::UserBasic expose :author, :assignee, using: Entities::UserBasic
expose :source_project_id, :target_project_id expose :source_project_id, :target_project_id
expose :label_list, as: :labels expose :label_names, as: :labels
end end
class SSHKey < Grape::Entity class SSHKey < Grape::Entity
......
...@@ -50,10 +50,15 @@ module API ...@@ -50,10 +50,15 @@ module API
post ":id/issues" do post ":id/issues" do
required_attributes! [:title] required_attributes! [:title]
attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id] attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id]
attrs[:label_list] = params[:labels] if params[:labels].present?
issue = ::Issues::CreateService.new(user_project, current_user, attrs).execute issue = ::Issues::CreateService.new(user_project, current_user, attrs).execute
if issue.valid? if issue.valid?
# Find or create labels and attach to issue
if params[:labels].present?
issue.add_labels_by_names(params[:labels].split(","))
end
present issue, with: Entities::Issue present issue, with: Entities::Issue
else else
not_found! not_found!
...@@ -76,13 +81,16 @@ module API ...@@ -76,13 +81,16 @@ module API
put ":id/issues/:issue_id" do put ":id/issues/:issue_id" do
issue = user_project.issues.find(params[:issue_id]) issue = user_project.issues.find(params[:issue_id])
authorize! :modify_issue, issue authorize! :modify_issue, issue
attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id, :state_event] attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id, :state_event]
attrs[:label_list] = params[:labels] if params[:labels].present?
issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue) issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue)
if issue.valid? if issue.valid?
# Find or create labels and attach to issue
if params[:labels].present?
issue.add_labels_by_names(params[:labels].split(","))
end
present issue, with: Entities::Issue present issue, with: Entities::Issue
else else
not_found! not_found!
......
...@@ -76,10 +76,14 @@ module API ...@@ -76,10 +76,14 @@ module API
authorize! :write_merge_request, user_project authorize! :write_merge_request, user_project
required_attributes! [:source_branch, :target_branch, :title] required_attributes! [:source_branch, :target_branch, :title]
attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description] attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description]
attrs[:label_list] = params[:labels] if params[:labels].present?
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 present merge_request, with: Entities::MergeRequest
else else
handle_merge_request_errors! merge_request.errors handle_merge_request_errors! merge_request.errors
...@@ -103,12 +107,16 @@ module API ...@@ -103,12 +107,16 @@ module API
# #
put ":id/merge_request/:merge_request_id" do put ":id/merge_request/:merge_request_id" do
attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :state_event, :description] attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :state_event, :description]
attrs[:label_list] = params[:labels] if params[:labels].present?
merge_request = user_project.merge_requests.find(params[:merge_request_id]) merge_request = user_project.merge_requests.find(params[:merge_request_id])
authorize! :modify_merge_request, merge_request authorize! :modify_merge_request, merge_request
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
if params[:labels].present?
merge_request.add_labels_by_names(params[:labels].split(","))
end
present merge_request, with: Entities::MergeRequest present merge_request, with: Entities::MergeRequest
else else
handle_merge_request_errors! merge_request.errors handle_merge_request_errors! merge_request.errors
......
...@@ -222,7 +222,7 @@ module API ...@@ -222,7 +222,7 @@ module API
# Example Request: # Example Request:
# GET /projects/:id/labels # GET /projects/:id/labels
get ':id/labels' do get ':id/labels' do
@labels = user_project.issues_labels @labels = user_project.labels
present @labels, with: Entities::Label present @labels, with: Entities::Label
end end
end end
......
module Gitlab module Gitlab
class IssuesLabels class IssuesLabels
class << self class << self
def important_labels def generate(project)
%w(bug critical confirmed) red = '#d9534f'
end yellow = '#f0ad4e'
blue = '#428bca'
def warning_labels green = '#5cb85c'
%w(documentation support)
end
def neutral_labels labels = [
%w(discussion suggestion) { title: "bug", color: red },
end { title: "critical", color: red },
{ title: "confirmed", color: red },
{ title: "documentation", color: yellow },
{ title: "support", color: yellow },
{ title: "discussion", color: blue },
{ title: "suggestion", color: blue },
{ title: "feature", color: green },
{ title: "enhancement", color: green }
]
def positive_labels labels.each do |label|
%w(feature enhancement) project.labels.create(label)
end end
def generate(project)
labels = important_labels + warning_labels + neutral_labels + positive_labels
project.issues_default_label_list = labels
project.save
end end
end end
end end
......
# Read about factories at https://github.com/thoughtbot/factory_girl
FactoryGirl.define do
factory :label_link do
label
target factory: :issue
end
end
# Read about factories at https://github.com/thoughtbot/factory_girl
FactoryGirl.define do
factory :label do
title "Bug"
color "#990000"
project
end
end
require 'spec_helper' require 'spec_helper'
describe LabelsHelper do describe LabelsHelper do
describe '#label_css_class' do it { expect(text_color_for_bg('#EEEEEE')).to eq('#333') }
it 'returns label-danger when given Bug as param' do it { expect(text_color_for_bg('#222E2E')).to eq('#FFF') }
expect(label_css_class('bug')).to eq('label-danger')
end
it 'returns label-danger when given Bug as param' do
expect(label_css_class('Bug')).to eq('label-danger')
end
end
end end
require 'spec_helper'
describe LabelLink do
let(:label) { create(:label_link) }
it { label.should be_valid }
it { should belong_to(:label) }
it { should belong_to(:target) }
end
require 'spec_helper'
describe Label do
let(:label) { create(:label) }
it { label.should be_valid }
it { should belong_to(:project) }
end
require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let!(:label1) { create(:label, title: 'label1', project: project) }
before do
project.team << [user, :master]
end
describe 'GET /projects/:id/labels' do
it 'should return project labels' do
get api("/projects/#{project.id}/labels", user)
response.status.should == 200
json_response.should be_an Array
json_response.size.should == 1
json_response.first['name'].should == label1.name
end
end
end
...@@ -10,13 +10,6 @@ describe API::API, api: true do ...@@ -10,13 +10,6 @@ describe API::API, api: true do
let(:snippet) { create(:project_snippet, author: user, project: project, title: 'example') } let(:snippet) { create(:project_snippet, author: user, project: project, title: 'example') }
let(:users_project) { create(:users_project, user: user, project: project, project_access: UsersProject::MASTER) } let(:users_project) { create(:users_project, user: user, project: project, project_access: UsersProject::MASTER) }
let(:users_project2) { create(:users_project, user: user3, project: project, project_access: UsersProject::DEVELOPER) } let(:users_project2) { create(:users_project, user: user3, project: project, project_access: UsersProject::DEVELOPER) }
let(:issue_with_labels) { create(:issue, author: user, assignee: user, project: project, :label_list => "label1, label2") }
let(:merge_request_with_labels) do
create(:merge_request, :simple, author: user, assignee: user,
source_project: project, target_project: project, title: 'Test',
label_list: 'label3, label4')
end
describe "GET /projects" do describe "GET /projects" do
before { project } before { project }
...@@ -634,46 +627,4 @@ describe API::API, api: true do ...@@ -634,46 +627,4 @@ describe API::API, api: true do
end end
end end
end end
describe 'GET /projects/:id/labels' do
context 'with an issue' do
before { issue_with_labels }
it 'should return project labels' do
get api("/projects/#{project.id}/labels", user)
response.status.should == 200
json_response.should be_an Array
json_response.first['name'].should == issue_with_labels.labels.first.name
json_response.last['name'].should == issue_with_labels.labels.last.name
end
end
context 'with a merge request' do
before { merge_request_with_labels }
it 'should return project labels' do
get api("/projects/#{project.id}/labels", user)
response.status.should == 200
json_response.should be_an Array
json_response.first['name'].should == merge_request_with_labels.labels.first.name
json_response.last['name'].should == merge_request_with_labels.labels.last.name
end
end
context 'with an issue and a merge request' do
before do
issue_with_labels
merge_request_with_labels
end
it 'should return project labels from both' do
get api("/projects/#{project.id}/labels", user)
response.status.should == 200
json_response.should be_an Array
all_labels = issue_with_labels.labels.map(&:name).to_a
.concat(merge_request_with_labels.labels.map(&:name).to_a)
json_response.map { |e| e['name'] }.should =~ all_labels
end
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