51359a1fb8
In different parts of the application, the same label was being rendered with a different foreground color. This was due to the "textColor" JSON property not being generated by the backend. This commit adds this property to JSON representation of labels.
249 lines
6.9 KiB
Ruby
249 lines
6.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Label < ActiveRecord::Base
|
|
include CacheMarkdownField
|
|
include Referable
|
|
include Subscribable
|
|
include Gitlab::SQL::Pattern
|
|
include OptionallySearch
|
|
include Sortable
|
|
include FromUnion
|
|
|
|
cache_markdown_field :description, pipeline: :single_line
|
|
|
|
DEFAULT_COLOR = '#428BCA'
|
|
NONE = 'no label'
|
|
|
|
default_value_for :color, DEFAULT_COLOR
|
|
|
|
has_many :lists, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
|
has_many :priorities, class_name: 'LabelPriority'
|
|
has_many :label_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
|
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
|
|
has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
|
|
|
|
before_validation :strip_whitespace_from_title_and_color
|
|
|
|
validates :color, color: true, allow_blank: false
|
|
|
|
# Don't allow ',' for label titles
|
|
validates :title, presence: true, format: { with: /\A[^,]+\z/ }
|
|
validates :title, uniqueness: { scope: [:group_id, :project_id] }
|
|
validates :title, length: { maximum: 255 }
|
|
|
|
default_scope { order(title: :asc) }
|
|
|
|
scope :templates, -> { where(template: true) }
|
|
scope :with_title, ->(title) { where(title: title) }
|
|
scope :with_lists_and_board, -> { joins(lists: :board).merge(List.movable) }
|
|
scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) }
|
|
scope :on_board, ->(board_id) { with_lists_and_board.where(boards: { id: board_id }) }
|
|
scope :order_name_asc, -> { reorder(title: :asc) }
|
|
scope :order_name_desc, -> { reorder(title: :desc) }
|
|
scope :subscribed_by, ->(user_id) { joins(:subscriptions).where(subscriptions: { user_id: user_id, subscribed: true }) }
|
|
|
|
def self.prioritized(project)
|
|
joins(:priorities)
|
|
.where(label_priorities: { project_id: project })
|
|
.reorder('label_priorities.priority ASC, labels.title ASC')
|
|
end
|
|
|
|
def self.unprioritized(project)
|
|
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
|
|
|
|
def self.optionally_subscribed_by(user_id)
|
|
if user_id
|
|
subscribed_by(user_id)
|
|
else
|
|
all
|
|
end
|
|
end
|
|
|
|
alias_attribute :name, :title
|
|
|
|
def self.reference_prefix
|
|
'~'
|
|
end
|
|
|
|
##
|
|
# Pattern used to extract label references from text
|
|
#
|
|
# This pattern supports cross-project references.
|
|
#
|
|
def self.reference_pattern
|
|
# NOTE: The id pattern only matches when all characters on the expression
|
|
# are digits, so it will match ~2 but not ~2fa because that's probably a
|
|
# label name and we want it to be matched as such.
|
|
@reference_pattern ||= %r{
|
|
(#{Project.reference_pattern})?
|
|
#{Regexp.escape(reference_prefix)}
|
|
(?:
|
|
(?<label_id>\d+(?!\S\w)\b)
|
|
| # Integer-based label ID, or
|
|
(?<label_name>
|
|
# String-based single-word label title, or
|
|
[A-Za-z0-9_\-\?\.&]+
|
|
(?<!\.|\?)
|
|
|
|
|
# String-based multi-word label surrounded in quotes
|
|
".+?"
|
|
)
|
|
)
|
|
}x
|
|
end
|
|
|
|
def self.link_reference_pattern
|
|
nil
|
|
end
|
|
|
|
# Searches for labels with a matching title or description.
|
|
#
|
|
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
|
|
#
|
|
# query - The search query as a String.
|
|
#
|
|
# Returns an ActiveRecord::Relation.
|
|
def self.search(query)
|
|
fuzzy_search(query, [:title, :description])
|
|
end
|
|
|
|
def open_issues_count(user = nil)
|
|
issues_count(user, state: 'opened')
|
|
end
|
|
|
|
def closed_issues_count(user = nil)
|
|
issues_count(user, state: 'closed')
|
|
end
|
|
|
|
def open_merge_requests_count(user = nil)
|
|
params = {
|
|
subject_foreign_key => subject.id,
|
|
label_name: title,
|
|
scope: 'all',
|
|
state: 'opened'
|
|
}
|
|
|
|
MergeRequestsFinder.new(user, params.with_indifferent_access).execute.count
|
|
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)
|
|
priority = if priorities.loaded?
|
|
priorities.first { |p| p.project == project }
|
|
else
|
|
priorities.find_by(project: project)
|
|
end
|
|
|
|
priority.try(:priority)
|
|
end
|
|
|
|
def priority?
|
|
priorities.present?
|
|
end
|
|
|
|
def template?
|
|
template
|
|
end
|
|
|
|
def color
|
|
super || DEFAULT_COLOR
|
|
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
|
|
#
|
|
# format - Symbol format to use (default: :id, optional: :name)
|
|
#
|
|
# Examples:
|
|
#
|
|
# Label.first.to_reference # => "~1"
|
|
# Label.first.to_reference(format: :name) # => "~\"bug\""
|
|
# Label.first.to_reference(project, target_project: same_namespace_project) # => "gitlab-ce~1"
|
|
# Label.first.to_reference(project, target_project: another_namespace_project) # => "gitlab-org/gitlab-ce~1"
|
|
#
|
|
# Returns a String
|
|
#
|
|
def to_reference(from = nil, target_project: nil, format: :id, full: false)
|
|
format_reference = label_format_reference(format)
|
|
reference = "#{self.class.reference_prefix}#{format_reference}"
|
|
|
|
if from
|
|
"#{from.to_reference(target_project, full: full)}#{reference}"
|
|
else
|
|
reference
|
|
end
|
|
end
|
|
|
|
def as_json(options = {})
|
|
super(options).tap do |json|
|
|
json[:type] = self.try(:type)
|
|
json[:priority] = priority(options[:project]) if options.key?(:project)
|
|
json[:textColor] = text_color
|
|
end
|
|
end
|
|
|
|
def hook_attrs
|
|
attributes
|
|
end
|
|
|
|
private
|
|
|
|
def issues_count(user, params = {})
|
|
params.merge!(subject_foreign_key => subject.id, label_name: title, scope: 'all')
|
|
IssuesFinder.new(user, params.with_indifferent_access).execute.count
|
|
end
|
|
|
|
def label_format_reference(format = :id)
|
|
raise StandardError, 'Unknown format' unless [:id, :name].include?(format)
|
|
|
|
if format == :name && !name.include?('"')
|
|
%("#{name}")
|
|
else
|
|
id
|
|
end
|
|
end
|
|
|
|
def sanitize_title(value)
|
|
CGI.unescapeHTML(Sanitize.clean(value.to_s))
|
|
end
|
|
|
|
def strip_whitespace_from_title_and_color
|
|
%w(color title).each { |attr| self[attr] = self[attr]&.strip }
|
|
end
|
|
end
|