Commit 3cca3c39 authored by Alex Kalderimis's avatar Alex Kalderimis Committed by Patrick Bair

Introduce a DB color type

Represents `Label#color` as a `Gitlab::Color`, which is a domain
object for a hex color.
parent 06645d16
...@@ -61,7 +61,7 @@ module LabelsHelper ...@@ -61,7 +61,7 @@ module LabelsHelper
render_label_text( render_label_text(
label.name, label.name,
suffix: suffix, suffix: suffix,
css_class: "gl-label-text #{text_color_class_for_bg(label.color)}", css_class: "gl-label-text #{label.text_color_class}",
bg_color: label.color bg_color: label.color
) )
end end
...@@ -114,30 +114,8 @@ module LabelsHelper ...@@ -114,30 +114,8 @@ module LabelsHelper
end end
end end
def text_color_class_for_bg(bg_color)
if light_color?(bg_color)
'gl-label-text-dark'
else
'gl-label-text-light'
end
end
def text_color_for_bg(bg_color) def text_color_for_bg(bg_color)
if light_color?(bg_color) ::Gitlab::Color.of(bg_color).contrast
'#333333'
else
'#FFFFFF'
end
end
def light_color?(color)
if color.length == 4
r, g, b = color[1, 4].scan(/./).map { |v| (v * 2).hex }
else
r, g, b = color[1, 7].scan(/.{2}/).map(&:hex)
end
(r + g + b) > 500
end end
def labels_filter_path_with_defaults(only_group_labels: false, include_ancestor_groups: true, include_descendant_groups: false) def labels_filter_path_with_defaults(only_group_labels: false, include_ancestor_groups: true, include_descendant_groups: false)
......
...@@ -12,8 +12,9 @@ class Label < ApplicationRecord ...@@ -12,8 +12,9 @@ class Label < ApplicationRecord
cache_markdown_field :description, pipeline: :single_line cache_markdown_field :description, pipeline: :single_line
DEFAULT_COLOR = '#6699cc' DEFAULT_COLOR = ::Gitlab::Color.of('#6699cc')
attribute :color, ::Gitlab::Database::Type::Color.new
default_value_for :color, DEFAULT_COLOR default_value_for :color, DEFAULT_COLOR
has_many :lists, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :lists, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
...@@ -22,9 +23,9 @@ class Label < ApplicationRecord ...@@ -22,9 +23,9 @@ class Label < ApplicationRecord
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'
before_validation :strip_whitespace_from_title_and_color before_validation :strip_whitespace_from_title
validates :color, color: true, allow_blank: false validates :color, color: true, presence: true
# Don't allow ',' for label titles # Don't allow ',' for label titles
validates :title, presence: true, format: { with: /\A[^,]+\z/ } validates :title, presence: true, format: { with: /\A[^,]+\z/ }
...@@ -212,7 +213,7 @@ class Label < ApplicationRecord ...@@ -212,7 +213,7 @@ class Label < ApplicationRecord
end end
def text_color def text_color
LabelsHelper.text_color_for_bg(self.color) color.contrast
end end
def title=(value) def title=(value)
...@@ -285,8 +286,8 @@ class Label < ApplicationRecord ...@@ -285,8 +286,8 @@ class Label < ApplicationRecord
CGI.unescapeHTML(Sanitize.clean(value.to_s)) CGI.unescapeHTML(Sanitize.clean(value.to_s))
end end
def strip_whitespace_from_title_and_color def strip_whitespace_from_title
%w(color title).each { |attr| self[attr] = self[attr]&.strip } self[:title] = title&.strip
end end
end end
......
...@@ -14,6 +14,10 @@ class LabelPresenter < Gitlab::View::Presenter::Delegated ...@@ -14,6 +14,10 @@ class LabelPresenter < Gitlab::View::Presenter::Delegated
end end
end end
def text_color_class
"gl-label-text-#{label.color.contrast.luminosity}"
end
def destroy_path def destroy_path
case label case label
when GroupLabel then group_label_path(label.group, label) when GroupLabel then group_label_path(label.group, label)
......
...@@ -57,7 +57,8 @@ module Analytics ...@@ -57,7 +57,8 @@ module Analytics
def html_description(event) def html_description(event)
options = {} options = {}
if event.label_based? if event.label_based?
options[:label_html] = render_label(event.label, link: '', small: true, tooltip: true) label = event.label.present
options[:label_html] = render_label(label, link: '', small: true, tooltip: true)
end end
content_tag(:p) { event.html_description(options).html_safe } content_tag(:p) { event.html_description(options).html_safe }
......
...@@ -4,7 +4,9 @@ class LabelEntity < Grape::Entity ...@@ -4,7 +4,9 @@ class LabelEntity < Grape::Entity
expose :id expose :id
expose :title expose :title
expose :color expose :color do |label|
label.color.to_s
end
expose :description expose :description
expose :group_id expose :group_id
expose :project_id expose :project_id
......
...@@ -2,162 +2,8 @@ ...@@ -2,162 +2,8 @@
module Labels module Labels
class BaseService < ::BaseService class BaseService < ::BaseService
COLOR_NAME_TO_HEX = {
black: '#000000',
silver: '#C0C0C0',
gray: '#808080',
white: '#FFFFFF',
maroon: '#800000',
red: '#FF0000',
purple: '#800080',
fuchsia: '#FF00FF',
green: '#008000',
lime: '#00FF00',
olive: '#808000',
yellow: '#FFFF00',
navy: '#000080',
blue: '#0000FF',
teal: '#008080',
aqua: '#00FFFF',
orange: '#FFA500',
aliceblue: '#F0F8FF',
antiquewhite: '#FAEBD7',
aquamarine: '#7FFFD4',
azure: '#F0FFFF',
beige: '#F5F5DC',
bisque: '#FFE4C4',
blanchedalmond: '#FFEBCD',
blueviolet: '#8A2BE2',
brown: '#A52A2A',
burlywood: '#DEB887',
cadetblue: '#5F9EA0',
chartreuse: '#7FFF00',
chocolate: '#D2691E',
coral: '#FF7F50',
cornflowerblue: '#6495ED',
cornsilk: '#FFF8DC',
crimson: '#DC143C',
darkblue: '#00008B',
darkcyan: '#008B8B',
darkgoldenrod: '#B8860B',
darkgray: '#A9A9A9',
darkgreen: '#006400',
darkgrey: '#A9A9A9',
darkkhaki: '#BDB76B',
darkmagenta: '#8B008B',
darkolivegreen: '#556B2F',
darkorange: '#FF8C00',
darkorchid: '#9932CC',
darkred: '#8B0000',
darksalmon: '#E9967A',
darkseagreen: '#8FBC8F',
darkslateblue: '#483D8B',
darkslategray: '#2F4F4F',
darkslategrey: '#2F4F4F',
darkturquoise: '#00CED1',
darkviolet: '#9400D3',
deeppink: '#FF1493',
deepskyblue: '#00BFFF',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1E90FF',
firebrick: '#B22222',
floralwhite: '#FFFAF0',
forestgreen: '#228B22',
gainsboro: '#DCDCDC',
ghostwhite: '#F8F8FF',
gold: '#FFD700',
goldenrod: '#DAA520',
greenyellow: '#ADFF2F',
grey: '#808080',
honeydew: '#F0FFF0',
hotpink: '#FF69B4',
indianred: '#CD5C5C',
indigo: '#4B0082',
ivory: '#FFFFF0',
khaki: '#F0E68C',
lavender: '#E6E6FA',
lavenderblush: '#FFF0F5',
lawngreen: '#7CFC00',
lemonchiffon: '#FFFACD',
lightblue: '#ADD8E6',
lightcoral: '#F08080',
lightcyan: '#E0FFFF',
lightgoldenrodyellow: '#FAFAD2',
lightgray: '#D3D3D3',
lightgreen: '#90EE90',
lightgrey: '#D3D3D3',
lightpink: '#FFB6C1',
lightsalmon: '#FFA07A',
lightseagreen: '#20B2AA',
lightskyblue: '#87CEFA',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#B0C4DE',
lightyellow: '#FFFFE0',
limegreen: '#32CD32',
linen: '#FAF0E6',
mediumaquamarine: '#66CDAA',
mediumblue: '#0000CD',
mediumorchid: '#BA55D3',
mediumpurple: '#9370DB',
mediumseagreen: '#3CB371',
mediumslateblue: '#7B68EE',
mediumspringgreen: '#00FA9A',
mediumturquoise: '#48D1CC',
mediumvioletred: '#C71585',
midnightblue: '#191970',
mintcream: '#F5FFFA',
mistyrose: '#FFE4E1',
moccasin: '#FFE4B5',
navajowhite: '#FFDEAD',
oldlace: '#FDF5E6',
olivedrab: '#6B8E23',
orangered: '#FF4500',
orchid: '#DA70D6',
palegoldenrod: '#EEE8AA',
palegreen: '#98FB98',
paleturquoise: '#AFEEEE',
palevioletred: '#DB7093',
papayawhip: '#FFEFD5',
peachpuff: '#FFDAB9',
peru: '#CD853F',
pink: '#FFC0CB',
plum: '#DDA0DD',
powderblue: '#B0E0E6',
rosybrown: '#BC8F8F',
royalblue: '#4169E1',
saddlebrown: '#8B4513',
salmon: '#FA8072',
sandybrown: '#F4A460',
seagreen: '#2E8B57',
seashell: '#FFF5EE',
sienna: '#A0522D',
skyblue: '#87CEEB',
slateblue: '#6A5ACD',
slategray: '#708090',
slategrey: '#708090',
snow: '#FFFAFA',
springgreen: '#00FF7F',
steelblue: '#4682B4',
tan: '#D2B48C',
thistle: '#D8BFD8',
tomato: '#FF6347',
turquoise: '#40E0D0',
violet: '#EE82EE',
wheat: '#F5DEB3',
whitesmoke: '#F5F5F5',
yellowgreen: '#9ACD32',
rebeccapurple: '#663399'
}.freeze
def convert_color_name_to_hex def convert_color_name_to_hex
color = params[:color] ::Gitlab::Color.of(params[:color])
color_name = color.strip.downcase
return color if color_name.start_with?('#')
COLOR_NAME_TO_HEX[color_name.to_sym] || color
end end
end end
end end
...@@ -12,11 +12,13 @@ ...@@ -12,11 +12,13 @@
# end # end
# #
class ColorValidator < ActiveModel::EachValidator class ColorValidator < ActiveModel::EachValidator
PATTERN = /\A\#(?:[0-9A-Fa-f]{3}){1,2}\Z/.freeze
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
unless value =~ PATTERN case value
record.errors.add(attribute, "must be a valid color code") when NilClass then return
when ::Gitlab::Color then return if value.valid?
when ::String then return if ::Gitlab::Color.new(value).valid?
end end
record.errors.add(attribute, "must be a valid color code")
end end
end end
...@@ -13,11 +13,11 @@ module EE ...@@ -13,11 +13,11 @@ module EE
render_label_text( render_label_text(
label.scoped_label_key, label.scoped_label_key,
css_class: "gl-label-text #{text_color_class_for_bg(label.color)}", css_class: "gl-label-text #{label.text_color_class}",
bg_color: label.color bg_color: label.color
) + render_label_text( ) + render_label_text(
label.scoped_label_value, label.scoped_label_value,
css_class: "gl-label-text-scoped #{('gl-label-text-dark' if light_color?(label.color))}", css_class: "gl-label-text-scoped #{label.text_color_class}",
suffix: suffix suffix: suffix
) )
end end
......
...@@ -61,7 +61,7 @@ RSpec.describe BulkImports::Groups::Pipelines::EpicsPipeline do ...@@ -61,7 +61,7 @@ RSpec.describe BulkImports::Groups::Pipelines::EpicsPipeline do
expect(group.epics.first.labels.count).to eq(1) expect(group.epics.first.labels.count).to eq(1)
expect(label.title).to eq('title') expect(label.title).to eq('title')
expect(label.description).to eq('description') expect(label.description).to eq('description')
expect(label.color).to eq('#cd2c5c') expect(label.color).to be_color('#cd2c5c')
end end
it 'imports epic system note metadata' do it 'imports epic system note metadata' do
......
...@@ -51,7 +51,7 @@ RSpec.describe Gitlab::ImportExport::Group::LegacyTreeRestorer do ...@@ -51,7 +51,7 @@ RSpec.describe Gitlab::ImportExport::Group::LegacyTreeRestorer do
expect(group.epics.first.labels.count).to eq(1) expect(group.epics.first.labels.count).to eq(1)
expect(label.title).to eq('title') expect(label.title).to eq('title')
expect(label.description).to eq('description') expect(label.description).to eq('description')
expect(label.color).to eq('#cd2c5c') expect(label.color).to be_color('#cd2c5c')
end end
end end
......
...@@ -107,7 +107,7 @@ RSpec.describe Gitlab::ImportExport::Group::LegacyTreeSaver do ...@@ -107,7 +107,7 @@ RSpec.describe Gitlab::ImportExport::Group::LegacyTreeSaver do
epic_label = epic_json['label_links'].first['label'] epic_label = epic_json['label_links'].first['label']
expect(epic_label['title']).to eq(label.title) expect(epic_label['title']).to eq(label.title)
expect(epic_label['description']).to eq(label.description) expect(epic_label['description']).to eq(label.description)
expect(epic_label['color']).to eq(label.color) expect(epic_label['color']).to be_color(label.color)
end end
end end
......
...@@ -59,7 +59,7 @@ RSpec.describe Gitlab::ImportExport::Group::TreeRestorer do ...@@ -59,7 +59,7 @@ RSpec.describe Gitlab::ImportExport::Group::TreeRestorer do
expect(group.epics.first.labels.count).to eq(1) expect(group.epics.first.labels.count).to eq(1)
expect(label.title).to eq('title') expect(label.title).to eq('title')
expect(label.description).to eq('description') expect(label.description).to eq('description')
expect(label.color).to eq('#cd2c5c') expect(label.color).to be_color('#cd2c5c')
end end
end end
......
...@@ -105,7 +105,7 @@ RSpec.describe Gitlab::ImportExport::Group::TreeSaver do ...@@ -105,7 +105,7 @@ RSpec.describe Gitlab::ImportExport::Group::TreeSaver do
epic_label = epic_json['label_links'].first['label'] epic_label = epic_json['label_links'].first['label']
expect(epic_label['title']).to eq(label.title) expect(epic_label['title']).to eq(label.title)
expect(epic_label['description']).to eq(label.description) expect(epic_label['description']).to eq(label.description)
expect(epic_label['color']).to eq(label.color) expect(epic_label['color']).to be_color(label.color)
end end
end end
......
...@@ -32,7 +32,7 @@ RSpec.describe Security::AutoFixLabelService do ...@@ -32,7 +32,7 @@ RSpec.describe Security::AutoFixLabelService do
expect(result).to be_success expect(result).to be_success
expect(label.title).to eq(title) expect(label.title).to eq(title)
expect(label.color).to eq(color) expect(label.color).to be_color(color)
expect(label.description).to eq(description) expect(label.description).to eq(description)
end end
end end
......
...@@ -3,7 +3,11 @@ ...@@ -3,7 +3,11 @@
module API module API
module Entities module Entities
class LabelBasic < Grape::Entity class LabelBasic < Grape::Entity
expose :id, :name, :color, :description, :description_html, :text_color expose :id, :name, :description, :description_html, :text_color
expose :color do |label, options|
label.color.to_s
end
end end
end end
end end
# frozen_string_literal: true
module Gitlab
class Color
PATTERN = /\A\#(?:[0-9A-Fa-f]{3}){1,2}\Z/.freeze
def initialize(value)
@value = value&.strip&.freeze
end
module Constants
DARK = Color.new('#333333')
LIGHT = Color.new('#FFFFFF')
COLOR_NAME_TO_HEX = {
black: '#000000',
silver: '#C0C0C0',
gray: '#808080',
white: '#FFFFFF',
maroon: '#800000',
red: '#FF0000',
purple: '#800080',
fuchsia: '#FF00FF',
green: '#008000',
lime: '#00FF00',
olive: '#808000',
yellow: '#FFFF00',
navy: '#000080',
blue: '#0000FF',
teal: '#008080',
aqua: '#00FFFF',
orange: '#FFA500',
aliceblue: '#F0F8FF',
antiquewhite: '#FAEBD7',
aquamarine: '#7FFFD4',
azure: '#F0FFFF',
beige: '#F5F5DC',
bisque: '#FFE4C4',
blanchedalmond: '#FFEBCD',
blueviolet: '#8A2BE2',
brown: '#A52A2A',
burlywood: '#DEB887',
cadetblue: '#5F9EA0',
chartreuse: '#7FFF00',
chocolate: '#D2691E',
coral: '#FF7F50',
cornflowerblue: '#6495ED',
cornsilk: '#FFF8DC',
crimson: '#DC143C',
darkblue: '#00008B',
darkcyan: '#008B8B',
darkgoldenrod: '#B8860B',
darkgray: '#A9A9A9',
darkgreen: '#006400',
darkgrey: '#A9A9A9',
darkkhaki: '#BDB76B',
darkmagenta: '#8B008B',
darkolivegreen: '#556B2F',
darkorange: '#FF8C00',
darkorchid: '#9932CC',
darkred: '#8B0000',
darksalmon: '#E9967A',
darkseagreen: '#8FBC8F',
darkslateblue: '#483D8B',
darkslategray: '#2F4F4F',
darkslategrey: '#2F4F4F',
darkturquoise: '#00CED1',
darkviolet: '#9400D3',
deeppink: '#FF1493',
deepskyblue: '#00BFFF',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1E90FF',
firebrick: '#B22222',
floralwhite: '#FFFAF0',
forestgreen: '#228B22',
gainsboro: '#DCDCDC',
ghostwhite: '#F8F8FF',
gold: '#FFD700',
goldenrod: '#DAA520',
greenyellow: '#ADFF2F',
grey: '#808080',
honeydew: '#F0FFF0',
hotpink: '#FF69B4',
indianred: '#CD5C5C',
indigo: '#4B0082',
ivory: '#FFFFF0',
khaki: '#F0E68C',
lavender: '#E6E6FA',
lavenderblush: '#FFF0F5',
lawngreen: '#7CFC00',
lemonchiffon: '#FFFACD',
lightblue: '#ADD8E6',
lightcoral: '#F08080',
lightcyan: '#E0FFFF',
lightgoldenrodyellow: '#FAFAD2',
lightgray: '#D3D3D3',
lightgreen: '#90EE90',
lightgrey: '#D3D3D3',
lightpink: '#FFB6C1',
lightsalmon: '#FFA07A',
lightseagreen: '#20B2AA',
lightskyblue: '#87CEFA',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#B0C4DE',
lightyellow: '#FFFFE0',
limegreen: '#32CD32',
linen: '#FAF0E6',
mediumaquamarine: '#66CDAA',
mediumblue: '#0000CD',
mediumorchid: '#BA55D3',
mediumpurple: '#9370DB',
mediumseagreen: '#3CB371',
mediumslateblue: '#7B68EE',
mediumspringgreen: '#00FA9A',
mediumturquoise: '#48D1CC',
mediumvioletred: '#C71585',
midnightblue: '#191970',
mintcream: '#F5FFFA',
mistyrose: '#FFE4E1',
moccasin: '#FFE4B5',
navajowhite: '#FFDEAD',
oldlace: '#FDF5E6',
olivedrab: '#6B8E23',
orangered: '#FF4500',
orchid: '#DA70D6',
palegoldenrod: '#EEE8AA',
palegreen: '#98FB98',
paleturquoise: '#AFEEEE',
palevioletred: '#DB7093',
papayawhip: '#FFEFD5',
peachpuff: '#FFDAB9',
peru: '#CD853F',
pink: '#FFC0CB',
plum: '#DDA0DD',
powderblue: '#B0E0E6',
rosybrown: '#BC8F8F',
royalblue: '#4169E1',
saddlebrown: '#8B4513',
salmon: '#FA8072',
sandybrown: '#F4A460',
seagreen: '#2E8B57',
seashell: '#FFF5EE',
sienna: '#A0522D',
skyblue: '#87CEEB',
slateblue: '#6A5ACD',
slategray: '#708090',
slategrey: '#708090',
snow: '#FFFAFA',
springgreen: '#00FF7F',
steelblue: '#4682B4',
tan: '#D2B48C',
thistle: '#D8BFD8',
tomato: '#FF6347',
turquoise: '#40E0D0',
violet: '#EE82EE',
wheat: '#F5DEB3',
whitesmoke: '#F5F5F5',
yellowgreen: '#9ACD32',
rebeccapurple: '#663399'
}.stringify_keys.transform_values { Color.new(_1) }.freeze
end
def self.of(color)
raise ArgumentError, 'No color spec' unless color
return color if color.is_a?(self)
color = color.to_s.strip
Constants::COLOR_NAME_TO_HEX[color.downcase] || new(color)
end
def to_s
@value.to_s
end
def as_json(_options = nil)
to_s
end
def eql(other)
return false unless other.is_a?(self.class)
to_s == other.to_s
end
alias_method :==, :eql
def valid?
PATTERN.match?(@value)
end
def light?
valid? && rgb.sum > 500
end
def luminosity
return :light if light?
:dark
end
def contrast
return Constants::DARK if light?
Constants::LIGHT
end
private
def rgb
return [] unless valid?
@rgb ||= begin
if @value.length == 4
@value[1, 4].scan(/./).map { |v| (v * 2).hex }
else
@value[1, 7].scan(/.{2}/).map(&:hex)
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Database
module Type
class Color < ActiveModel::Type::Value
def serialize(value)
value.to_s if value
end
def serializable?(value)
value.nil? || value.is_a?(::String) || value.is_a?(::Gitlab::Color)
end
def cast_value(value)
::Gitlab::Color.new(value.to_s)
end
end
end
end
end
...@@ -114,16 +114,16 @@ RSpec.describe LabelsHelper do ...@@ -114,16 +114,16 @@ RSpec.describe LabelsHelper do
describe 'text_color_for_bg' do describe 'text_color_for_bg' do
it 'uses light text on dark backgrounds' do it 'uses light text on dark backgrounds' do
expect(text_color_for_bg('#222E2E')).to eq('#FFFFFF') expect(text_color_for_bg('#222E2E')).to be_color('#FFFFFF')
end end
it 'uses dark text on light backgrounds' do it 'uses dark text on light backgrounds' do
expect(text_color_for_bg('#EEEEEE')).to eq('#333333') expect(text_color_for_bg('#EEEEEE')).to be_color('#333333')
end end
it 'supports RGB triplets' do it 'supports RGB triplets' do
expect(text_color_for_bg('#FFF')).to eq '#333333' expect(text_color_for_bg('#FFF')).to be_color '#333333'
expect(text_color_for_bg('#000')).to eq '#FFFFFF' expect(text_color_for_bg('#000')).to be_color '#FFFFFF'
end end
end end
......
...@@ -277,7 +277,7 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter do ...@@ -277,7 +277,7 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter do
end end
context 'References with html entities' do context 'References with html entities' do
let!(:label) { create(:label, name: '&lt;html&gt;', project: project) } let!(:label) { create(:label, title: '&lt;html&gt;', project: project) }
it 'links to a valid reference' do it 'links to a valid reference' do
doc = reference_filter('See ~"&lt;html&gt;"') doc = reference_filter('See ~"&lt;html&gt;"')
......
...@@ -43,7 +43,7 @@ RSpec.describe BulkImports::Common::Pipelines::LabelsPipeline do ...@@ -43,7 +43,7 @@ RSpec.describe BulkImports::Common::Pipelines::LabelsPipeline do
expect(label.title).to eq('Label 1') expect(label.title).to eq('Label 1')
expect(label.description).to eq('Label 1') expect(label.description).to eq('Label 1')
expect(label.color).to eq('#6699cc') expect(label.color).to be_color('#6699cc')
expect(File.directory?(tmpdir)).to eq(false) expect(File.directory?(tmpdir)).to eq(false)
end end
end end
......
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Gitlab::Color do
describe ".of" do
described_class::Constants::COLOR_NAME_TO_HEX.each do |name, value|
it "parses #{name} to #{value}" do
expect(described_class.of(name)).to eq(value)
end
end
it 'parses hex literals as colors' do
expect(described_class.of('#fff')).to eq(described_class.new('#fff'))
expect(described_class.of('#fefefe')).to eq(described_class.new('#fefefe'))
end
it 'raises if the input is nil' do
expect { described_class.of(nil) }.to raise_error(ArgumentError)
end
it 'returns an invalid color if the input is not valid' do
expect(described_class.of('unknown color')).not_to be_valid
end
end
describe '#new' do
it 'handles nil values' do
expect(described_class.new(nil)).to eq(described_class.new(nil))
end
it 'strips input' do
expect(described_class.new(' abc ')).to eq(described_class.new('abc'))
end
end
describe '#valid?' do
described_class::Constants::COLOR_NAME_TO_HEX.each_key do |name|
specify "#{name} is a valid color" do
expect(described_class.of(name)).to be_valid
end
end
specify '#fff is a valid color' do
expect(described_class.new('#fff')).to be_valid
end
specify '#ffffff is a valid color' do
expect(described_class.new('#ffffff')).to be_valid
end
specify '#ABCDEF is a valid color' do
expect(described_class.new('#ABCDEF')).to be_valid
end
specify '#123456 is a valid color' do
expect(described_class.new('#123456')).to be_valid
end
specify '#1234567 is not a valid color' do
expect(described_class.new('#1234567')).not_to be_valid
end
specify 'fff is not a valid color' do
expect(described_class.new('fff')).not_to be_valid
end
specify '#deadbeaf is not a valid color' do
expect(described_class.new('#deadbeaf')).not_to be_valid
end
specify '#a1b2c3 is a valid color' do
expect(described_class.new('#a1b2c3')).to be_valid
end
specify 'nil is not a valid color' do
expect(described_class.new(nil)).not_to be_valid
end
end
describe '#light?' do
specify '#fff is light' do
expect(described_class.new('#fff')).to be_light
end
specify '#a7a7a7 is light' do
expect(described_class.new('#a7a7a7')).to be_light
end
specify '#a6a7a7 is dark' do
expect(described_class.new('#a6a7a7')).not_to be_light
end
specify '#000 is dark' do
expect(described_class.new('#000')).not_to be_light
end
specify 'invalid colors are not light' do
expect(described_class.new('not-a-color')).not_to be_light
end
end
describe '#contrast' do
context 'with light colors' do
it 'is dark' do
%w[#fff #fefefe #a7a7a7].each do |hex|
expect(described_class.new(hex)).to have_attributes(
contrast: described_class::Constants::DARK,
luminosity: :light
)
end
end
end
context 'with dark colors' do
it 'is light' do
%w[#000 #a6a7a7].each do |hex|
expect(described_class.new(hex)).to have_attributes(
contrast: described_class::Constants::LIGHT,
luminosity: :dark
)
end
end
end
end
describe 'as_json' do
it 'serializes correctly' do
expect(described_class.new('#f0f1f2').as_json).to eq('#f0f1f2')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Gitlab::Database::Type::Color do
subject(:type) { described_class.new }
let(:color) { ::Gitlab::Color.of('red') }
it 'serializes by calling #to_s' do
expect(type.serialize(color)).to eq(color.to_s)
end
it 'serializes nil to nil' do
expect(type.serialize(nil)).to be_nil
end
it 'casts by calling Color::new' do
expect(type.cast('#fff')).to eq(::Gitlab::Color.new('#fff'))
end
it 'accepts colors as arguments to cast' do
expect(type.cast(color)).to eq(color)
end
it 'allows nil database values' do
expect(type.cast(nil)).to be_nil
end
it 'tells us what is serializable' do
[nil, 'foo', color].each do |value|
expect(type.serializable?(value)).to be true
end
end
it 'tells us what is not serializable' do
[0, 3.2, true, Time.current, { some: 'hash' }].each do |value|
expect(type.serializable?(value)).to be false
end
end
end
...@@ -67,24 +67,21 @@ RSpec.describe Label do ...@@ -67,24 +67,21 @@ RSpec.describe Label do
label = described_class.new(color: ' #abcdef ') label = described_class.new(color: ' #abcdef ')
label.valid? label.valid?
expect(label.color).to eq('#abcdef') expect(label.color).to be_color('#abcdef')
end end
it 'uses default color if color is missing' do it 'uses default color if color is missing' do
label = described_class.new(color: nil) label = described_class.new(color: nil)
expect(label.color).to be(Label::DEFAULT_COLOR) expect(label.color).to be_color(Label::DEFAULT_COLOR)
end end
end end
describe '#text_color' do describe '#text_color' do
it 'uses default color if color is missing' do it 'uses default color if color is missing' do
expect(LabelsHelper).to receive(:text_color_for_bg).with(Label::DEFAULT_COLOR)
.and_return(spy)
label = described_class.new(color: nil) label = described_class.new(color: nil)
label.text_color expect(label.text_color).to eq(Label::DEFAULT_COLOR.contrast)
end end
end end
......
...@@ -140,7 +140,7 @@ RSpec.describe API::GroupLabels do ...@@ -140,7 +140,7 @@ RSpec.describe API::GroupLabels do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(group_label1.name) expect(json_response['name']).to eq(group_label1.name)
expect(json_response['color']).to eq(group_label1.color) expect(json_response['color']).to be_color(group_label1.color)
expect(json_response['description']).to eq(group_label1.description) expect(json_response['description']).to eq(group_label1.description)
end end
end end
...@@ -156,7 +156,7 @@ RSpec.describe API::GroupLabels do ...@@ -156,7 +156,7 @@ RSpec.describe API::GroupLabels do
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq(valid_new_label_title) expect(json_response['name']).to eq(valid_new_label_title)
expect(json_response['color']).to eq('#FFAABB') expect(json_response['color']).to be_color('#FFAABB')
expect(json_response['description']).to eq('test') expect(json_response['description']).to eq('test')
end end
...@@ -169,7 +169,7 @@ RSpec.describe API::GroupLabels do ...@@ -169,7 +169,7 @@ RSpec.describe API::GroupLabels do
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq(valid_new_label_title) expect(json_response['name']).to eq(valid_new_label_title)
expect(json_response['color']).to eq('#FFAABB') expect(json_response['color']).to be_color('#FFAABB')
expect(json_response['description']).to be_nil expect(json_response['description']).to be_nil
end end
...@@ -276,7 +276,7 @@ RSpec.describe API::GroupLabels do ...@@ -276,7 +276,7 @@ RSpec.describe API::GroupLabels do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(valid_new_label_title) expect(json_response['name']).to eq(valid_new_label_title)
expect(json_response['color']).to eq('#FFFFFF') expect(json_response['color']).to be_color('#FFFFFF')
expect(json_response['description']).to eq('test') expect(json_response['description']).to eq('test')
end end
...@@ -332,7 +332,7 @@ RSpec.describe API::GroupLabels do ...@@ -332,7 +332,7 @@ RSpec.describe API::GroupLabels do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(valid_new_label_title) expect(json_response['name']).to eq(valid_new_label_title)
expect(json_response['color']).to eq('#FFFFFF') expect(json_response['color']).to be_color('#FFFFFF')
expect(json_response['description']).to eq('test') expect(json_response['description']).to eq('test')
end end
......
...@@ -34,7 +34,7 @@ RSpec.describe API::Labels do ...@@ -34,7 +34,7 @@ RSpec.describe API::Labels do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(valid_label_title_2) expect(json_response['name']).to eq(valid_label_title_2)
expect(json_response['color']).to eq(label1.color) expect(json_response['color']).to be_color(label1.color)
end end
it "returns 200 if colors is changed (#{route_type} route)" do it "returns 200 if colors is changed (#{route_type} route)" do
...@@ -42,7 +42,7 @@ RSpec.describe API::Labels do ...@@ -42,7 +42,7 @@ RSpec.describe API::Labels do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(label1.name) expect(json_response['name']).to eq(label1.name)
expect(json_response['color']).to eq('#FFFFFF') expect(json_response['color']).to be_color('#FFFFFF')
end end
it "returns 200 if a priority is added (#{route_type} route)" do it "returns 200 if a priority is added (#{route_type} route)" do
...@@ -86,7 +86,7 @@ RSpec.describe API::Labels do ...@@ -86,7 +86,7 @@ RSpec.describe API::Labels do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(valid_label_title_2) expect(json_response['name']).to eq(valid_label_title_2)
expect(json_response['color']).to eq('#FFFFFF') expect(json_response['color']).to be_color('#FFFFFF')
expect(json_response['description']).to eq('test') expect(json_response['description']).to eq('test')
end end
...@@ -266,8 +266,8 @@ RSpec.describe API::Labels do ...@@ -266,8 +266,8 @@ RSpec.describe API::Labels do
'open_merge_requests_count' => 0, 'open_merge_requests_count' => 0,
'name' => group_label.name, 'name' => group_label.name,
'description' => nil, 'description' => nil,
'color' => a_string_matching(/^#\h{6}$/), 'color' => a_valid_color,
'text_color' => a_string_matching(/^#\h{6}$/), 'text_color' => a_valid_color,
'priority' => nil, 'priority' => nil,
'subscribed' => false, 'subscribed' => false,
'is_project_label' => false) 'is_project_label' => false)
...@@ -277,8 +277,8 @@ RSpec.describe API::Labels do ...@@ -277,8 +277,8 @@ RSpec.describe API::Labels do
'open_merge_requests_count' => 1, 'open_merge_requests_count' => 1,
'name' => priority_label.name, 'name' => priority_label.name,
'description' => nil, 'description' => nil,
'color' => a_string_matching(/^#\h{6}$/), 'color' => a_valid_color,
'text_color' => a_string_matching(/^#\h{6}$/), 'text_color' => a_valid_color,
'priority' => 3, 'priority' => 3,
'subscribed' => false, 'subscribed' => false,
'is_project_label' => true) 'is_project_label' => true)
...@@ -336,7 +336,7 @@ RSpec.describe API::Labels do ...@@ -336,7 +336,7 @@ RSpec.describe API::Labels do
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq(valid_label_title_2) expect(json_response['name']).to eq(valid_label_title_2)
expect(json_response['color']).to eq('#FFAABB') expect(json_response['color']).to be_color('#FFAABB')
expect(json_response['description']).to eq('test') expect(json_response['description']).to eq('test')
expect(json_response['priority']).to eq(2) expect(json_response['priority']).to eq(2)
end end
...@@ -350,7 +350,7 @@ RSpec.describe API::Labels do ...@@ -350,7 +350,7 @@ RSpec.describe API::Labels do
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq(valid_label_title_2) expect(json_response['name']).to eq(valid_label_title_2)
expect(json_response['color']).to eq('#FFAABB') expect(json_response['color']).to be_color('#FFAABB')
expect(json_response['description']).to be_nil expect(json_response['description']).to be_nil
expect(json_response['priority']).to be_nil expect(json_response['priority']).to be_nil
end end
...@@ -365,7 +365,7 @@ RSpec.describe API::Labels do ...@@ -365,7 +365,7 @@ RSpec.describe API::Labels do
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq(valid_label_title_2) expect(json_response['name']).to eq(valid_label_title_2)
expect(json_response['color']).to eq('#FFAABB') expect(json_response['color']).to be_color('#FFAABB')
expect(json_response['description']).to be_nil expect(json_response['description']).to be_nil
expect(json_response['priority']).to eq(3) expect(json_response['priority']).to eq(3)
end end
...@@ -552,7 +552,7 @@ RSpec.describe API::Labels do ...@@ -552,7 +552,7 @@ RSpec.describe API::Labels do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(label1.name) expect(json_response['name']).to eq(label1.name)
expect(json_response['color']).to eq(label1.color) expect(json_response['color']).to be_color(label1.color.to_s)
end end
context 'if group label already exists' do context 'if group label already exists' do
......
...@@ -40,7 +40,7 @@ RSpec.describe LabelSerializer do ...@@ -40,7 +40,7 @@ RSpec.describe LabelSerializer do
expect(subject.keys).to eq([:id, :title, :color, :project_id, :text_color]) expect(subject.keys).to eq([:id, :title, :color, :project_id, :text_color])
expect(subject[:id]).to eq(resource.id) expect(subject[:id]).to eq(resource.id)
expect(subject[:title]).to eq(resource.title) expect(subject[:title]).to eq(resource.title)
expect(subject[:color]).to eq(resource.color) expect(subject[:color]).to be_color(resource.color)
expect(subject[:text_color]).to eq(resource.text_color) expect(subject[:text_color]).to eq(resource.text_color)
expect(subject[:project_id]).to eq(resource.project_id) expect(subject[:project_id]).to eq(resource.project_id)
end end
......
...@@ -14,7 +14,7 @@ RSpec.describe Labels::CreateService do ...@@ -14,7 +14,7 @@ RSpec.describe Labels::CreateService do
let(:unknown_color) { 'unknown' } let(:unknown_color) { 'unknown' }
let(:no_color) { '' } let(:no_color) { '' }
let(:expected_saved_color) { hex_color } let(:expected_saved_color) { ::Gitlab::Color.of(hex_color) }
context 'in a project' do context 'in a project' do
context 'with color in hex-code' do context 'with color in hex-code' do
...@@ -47,7 +47,6 @@ RSpec.describe Labels::CreateService do ...@@ -47,7 +47,6 @@ RSpec.describe Labels::CreateService do
context 'with color surrounded by spaces' do context 'with color surrounded by spaces' do
it 'creates a label' do it 'creates a label' do
label = described_class.new(params_with(spaced_color)).execute(project: project) label = described_class.new(params_with(spaced_color)).execute(project: project)
expect(label).to be_persisted expect(label).to be_persisted
expect(label.color).to eq expected_saved_color expect(label.color).to eq expected_saved_color
end end
......
...@@ -202,7 +202,7 @@ RSpec.describe Labels::PromoteService do ...@@ -202,7 +202,7 @@ RSpec.describe Labels::PromoteService do
expect(new_label.title).to eq(promoted_label_name) expect(new_label.title).to eq(promoted_label_name)
expect(new_label.description).to eq(promoted_description) expect(new_label.description).to eq(promoted_description)
expect(new_label.color).to eq(promoted_color) expect(new_label.color).to be_color(promoted_color)
end end
it_behaves_like 'promoting a project label to a group label' it_behaves_like 'promoting a project label to a group label'
......
...@@ -13,7 +13,7 @@ RSpec.describe Labels::UpdateService do ...@@ -13,7 +13,7 @@ RSpec.describe Labels::UpdateService do
let(:unknown_color) { 'unknown' } let(:unknown_color) { 'unknown' }
let(:no_color) { '' } let(:no_color) { '' }
let(:expected_saved_color) { hex_color } let(:expected_saved_color) { ::Gitlab::Color.of(hex_color) }
before do before do
@label = Labels::CreateService.new(title: 'Initial', color: '#000000').execute(project: project) @label = Labels::CreateService.new(title: 'Initial', color: '#000000').execute(project: project)
......
...@@ -23,11 +23,11 @@ RSpec.describe Projects::CreateService, '#execute' do ...@@ -23,11 +23,11 @@ RSpec.describe Projects::CreateService, '#execute' do
end end
it 'creates labels on project creation' do it 'creates labels on project creation' do
created_label = project.labels.last expect(project.labels).to include have_attributes(
type: eq('ProjectLabel'),
expect(created_label.type).to eq('ProjectLabel') project_id: eq(project.id),
expect(created_label.project_id).to eq(project.id) title: eq('bug')
expect(created_label.title).to eq('bug') )
end end
context 'using gitlab project import' do context 'using gitlab project import' do
......
# frozen_string_literal: true
# Assert that this value is a valid color equal to the argument
#
# ```
# expect(value).to be_color('#fff')
# ```
RSpec::Matchers.define :be_color do |expected|
match do |actual|
next false unless actual.present?
if expected
::Gitlab::Color.of(actual) == ::Gitlab::Color.of(expected)
else
::Gitlab::Color.of(actual).valid?
end
end
end
RSpec::Matchers.alias_matcher :a_valid_color, :be_color
...@@ -70,7 +70,7 @@ RSpec.shared_examples 'incident management label service' do ...@@ -70,7 +70,7 @@ RSpec.shared_examples 'incident management label service' do
expect(execute).to be_success expect(execute).to be_success
expect(execute.payload).to eq(label: label) expect(execute.payload).to eq(label: label)
expect(label.title).to eq(title) expect(label.title).to eq(title)
expect(label.color).to eq(color) expect(label.color).to be_color(color)
expect(label.description).to eq(description) expect(label.description).to eq(description)
end end
end end
......
...@@ -23,7 +23,12 @@ RSpec.describe ColorValidator do ...@@ -23,7 +23,12 @@ RSpec.describe ColorValidator do
'#ffff' | false '#ffff' | false
'#000111222' | false '#000111222' | false
'invalid' | false 'invalid' | false
'red' | false
'000' | false '000' | false
nil | true # use presence to validate non-nil
'' | false
Time.current | false
::Gitlab::Color.of(:red) | true
end end
with_them do with_them do
...@@ -41,4 +46,22 @@ RSpec.describe ColorValidator do ...@@ -41,4 +46,22 @@ RSpec.describe ColorValidator do
Timeout.timeout(5.seconds) { subject.valid? } Timeout.timeout(5.seconds) { subject.valid? }
end.not_to raise_error end.not_to raise_error
end end
context 'when color must be present' do
subject do
Class.new do
include ActiveModel::Model
include ActiveModel::Validations
attr_accessor :color
validates :color, color: true, presence: true
end.new
end
it 'rejects nil' do
subject.color = nil
expect(subject).not_to be_valid
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