Rename PlanningElementTypeColors => Colors and add autocomplete

pull/6336/head
Oliver Günther 7 years ago
parent 8e8c578c65
commit e77aae007b
No known key found for this signature in database
GPG Key ID: 88872239EB414F99
  1. 31
      app/assets/stylesheets/content/_colors.sass
  2. 3
      app/assets/stylesheets/content/_forms.sass
  3. 29
      app/controllers/colors_controller.rb
  4. 3
      app/helpers/colors_helper.rb
  5. 18
      app/models/color.rb
  6. 2
      app/models/design_color.rb
  7. 2
      app/models/status.rb
  8. 2
      app/models/type.rb
  9. 6
      app/seeders/basic_data/color_seeder.rb
  10. 303
      app/seeders/basic_data/flat_color_seeder.rb
  11. 2
      app/seeders/basic_data/type_seeder.rb
  12. 2
      app/seeders/basic_data/workflow_seeder.rb
  13. 1
      app/seeders/basic_data_seeder.rb
  14. 2
      app/views/colors/_form.html.erb
  15. 0
      app/views/colors/confirm_destroy.html.erb
  16. 0
      app/views/colors/edit.html.erb
  17. 58
      app/views/colors/index.html.erb
  18. 0
      app/views/colors/new.html.erb
  19. 111
      app/views/planning_element_type_colors/index.html.erb
  20. 14
      app/views/statuses/_form.html.erb
  21. 2
      config/initializers/menus.rb
  22. 2
      config/locales/en.yml
  23. 2
      config/routes.rb
  24. 14
      db/migrate/20180510184732_rename_planning_elemnt_type_colors_to_colors.rb
  25. 2
      db/migrate/tables/planning_element_type_colors.rb
  26. 5
      frontend/app/angular4-modules.ts
  27. 31
      frontend/app/components/a11y/color-contrast.functions.ts
  28. 22
      frontend/app/components/colors/colors-autocompleter.component.html
  29. 139
      frontend/app/components/colors/colors-autocompleter.component.ts
  30. 48
      frontend/app/components/common/autocomplete-select-decoration/autocomplete-select-decoration.component.ts
  31. 2
      frontend/app/components/states/state-cache.service.ts
  32. 37
      frontend/app/components/wp-buttons/wp-status-button/wp-status-button.component.ts
  33. 4
      frontend/app/components/wp-buttons/wp-status-button/wp-status-button.html
  34. 2
      spec/controllers/colors_controller_spec.rb
  35. 3
      spec/factories/color_factory.rb
  36. 26
      spec/models/color_spec.rb

@ -38,8 +38,6 @@
&.standalone
margin-left: initial
vertical-align: middle
.color--text-preview
padding: 2px 4px
@ -57,3 +55,32 @@
.color--phase-icon
border-radius: 4px
.color--preview-patch-field
display: flex
flex-wrap: wrap
.color--preview-patch
// Square items
flex: 0 0 150px
height: 150px
border: 1px solid #CCCCCC
margin: 10px
a
// Align text centered
display: flex
width: 100%
height: 100%
align-items: center
justify-content: center
font-weight: bold
text-align: center
word-break: break-all
a.-bright
color: #333333
a.-dark
color: white

@ -463,6 +463,9 @@ fieldset.form--fieldset
font-style: italic
max-width: 500px // improve readability
&.-no-margin
margin: 0
&.-no-italic
font-style: normal

@ -27,7 +27,7 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
class PlanningElementTypeColorsController < ApplicationController
class ColorsController < ApplicationController
before_action :disable_api
before_action :require_admin_unless_readonly_api_request
@ -38,27 +38,27 @@ class PlanningElementTypeColorsController < ApplicationController
menu_item :colors
def index
@colors = PlanningElementTypeColor.all
@colors = Color.all
respond_to do |format|
format.html
end
end
def show
@color = PlanningElementTypeColor.find(params[:id])
@color = Color.find(params[:id])
respond_to do |_format|
end
end
def new
@color = PlanningElementTypeColor.new
@color = Color.new
respond_to do |format|
format.html
end
end
def create
@color = PlanningElementTypeColor.new(permitted_params.color)
@color = Color.new(permitted_params.color)
if @color.save
flash[:notice] = l(:notice_successful_create)
@ -70,14 +70,14 @@ class PlanningElementTypeColorsController < ApplicationController
end
def edit
@color = PlanningElementTypeColor.find(params[:id])
@color = Color.find(params[:id])
respond_to do |format|
format.html
end
end
def update
@color = PlanningElementTypeColor.find(params[:id])
@color = Color.find(params[:id])
if @color.update_attributes(permitted_params.color)
flash[:notice] = l(:notice_successful_update)
@ -88,26 +88,15 @@ class PlanningElementTypeColorsController < ApplicationController
end
end
def move
@color = PlanningElementTypeColor.find(params[:id])
if @color.update_attributes(permitted_params.color_move)
flash[:notice] = l(:notice_successful_update)
else
render action: 'edit'
end
redirect_to colors_path
end
def confirm_destroy
@color = PlanningElementTypeColor.find(params[:id])
@color = Color.find(params[:id])
respond_to do |format|
format.html
end
end
def destroy
@color = PlanningElementTypeColor.find(params[:id])
@color = Color.find(params[:id])
@color.destroy
flash[:notice] = l(:notice_successful_delete)

@ -31,9 +31,10 @@
module ColorsHelper
def options_for_colors(colored_thing, default_label: I18n.t(:label_none_parentheses), default_color: nil)
s = content_tag(:option, default_label, value: default_color)
PlanningElementTypeColor.find_each do |c|
Color.find_each do |c|
options = {}
options[:value] = c.id
options[:data] = { color: c.hexcode, bright: c.bright? }
options[:selected] = true if c.id == colored_thing.color_id
options[:style] = "appearance: none; background-color: #{c.hexcode}; color: #{c.contrasting_color}"

@ -27,11 +27,8 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
class PlanningElementTypeColor < ActiveRecord::Base
self.table_name = 'planning_element_type_colors'
acts_as_list
default_scope { order('position ASC') }
class Color < ActiveRecord::Base
self.table_name = 'colors'
has_many :planning_element_types, class_name: 'Type',
foreign_key: 'color_id',
@ -47,16 +44,21 @@ class PlanningElementTypeColor < ActiveRecord::Base
##
# Returns the best contrasting color, either white or black
# depending on the overall brightness.
# (note this is not HSL Lightness, but simply the sum of all RGB channels)
# https://gist.github.com/charliepark/480358
def contrasting_color(light_color: '#FFFFFF', dark_color: '#333333')
if brightness_yiq >= 128
if bright?
dark_color
else
light_color
end
end
##
# Returns whether the color is bright according to
# YIQ lightness.
def bright?
brightness_yiq >= 128
end
##
# Sum the color values of each channel
# Same as in frontend color-contrast.functions.ts

@ -74,7 +74,7 @@ class DesignColor < ActiveRecord::Base
protected
# This could be DRY! This method is taken from model PlanningElementTypeColor.
# This could be DRY! This method is taken from model Color.
def normalize_hexcode
if hexcode.present? and hexcode_changed?
self.hexcode = hexcode.strip.upcase

@ -36,7 +36,7 @@ class Status < ActiveRecord::Base
has_many :workflows, foreign_key: 'old_status_id'
acts_as_list
belongs_to :color, class_name: 'PlanningElementTypeColor', foreign_key: 'color_id'
belongs_to :color, class_name: 'Color', foreign_key: 'color_id'
before_destroy :delete_workflows

@ -51,7 +51,7 @@ class ::Type < ActiveRecord::Base
join_table: "#{table_name_prefix}custom_fields_types#{table_name_suffix}",
association_foreign_key: 'custom_field_id'
belongs_to :color, class_name: 'PlanningElementTypeColor',
belongs_to :color, class_name: 'Color',
foreign_key: 'color_id'
acts_as_list

@ -29,15 +29,15 @@
module BasicData
class ColorSeeder < Seeder
def seed_data!
PlanningElementTypeColor.transaction do
Color.transaction do
data.each do |attributes|
PlanningElementTypeColor.create(attributes)
Color.create(attributes)
end
end
end
def applicable?
PlanningElementTypeColor.all.empty?
Color.all.empty?
end
def not_applicable_message

@ -0,0 +1,303 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
module BasicData
class FlatColorSeeder < Seeder
def seed_data!
Color.transaction do
data.each do |attributes|
Color.create(attributes)
end
end
end
def applicable?
Color.where(name: %w(blanchedalmond Razzmatazz)).empty?
end
def not_applicable_message
'Skipping flat colors as there are already some configured'
end
##
def data
[
{ name: 'alice blue', hexcode: '#E4F1FE' },
{ name: 'aliceblue', hexcode: '#F0F8FF' },
{ name: 'antiquewhite', hexcode: '#FAEBD7' },
{ name: 'aqua island', hexcode: '#A2DED0' },
{ name: 'aqua', hexcode: '#00FFFF' },
{ name: 'aquamarine', hexcode: '#7FFFD4' },
{ name: 'azure', hexcode: '#F0FFFF' },
{ name: 'beige', hexcode: '#F5F5DC' },
{ name: 'bisque', hexcode: '#FFE4C4' },
{ name: 'black', hexcode: '#000000' },
{ name: 'blanchedalmond', hexcode: '#FFEBCD' },
{ name: 'blue', hexcode: '#0000FF' },
{ name: 'blueviolet', hexcode: '#8A2BE2' },
{ name: 'brown', hexcode: '#A52A2A' },
{ name: 'burlywood', hexcode: '#DEB887' },
{ name: 'burnt orange', hexcode: '#D35400' },
{ name: 'buttercup', hexcode: '#F39C12' },
{ name: 'cabaret', hexcode: '#D2527F' },
{ name: 'cadetblue', hexcode: '#5F9EA0' },
{ name: 'california', hexcode: '#F89406' },
{ name: 'cape honey', hexcode: '#FDE3A7' },
{ name: 'cararra', hexcode: '#F2F1EF' },
{ name: 'caribbean green', hexcode: '#03C9A9' },
{ name: 'casablanca', hexcode: '#F4B350' },
{ name: 'cascade', hexcode: '#95A5A6' },
{ name: 'chambray', hexcode: '#3A539B' },
{ name: 'chartreuse', hexcode: '#7FFF00' },
{ name: 'chestnut rose', hexcode: '#D24D57' },
{ name: 'chocolate', hexcode: '#D2691E' },
{ name: 'cinnabar', hexcode: '#E74C3C' },
{ name: 'confetti', hexcode: '#E9D460' },
{ name: 'coral', hexcode: '#FF7F50' },
{ name: 'cornflowerblue', hexcode: '#6495ED' },
{ name: 'cornsilk', hexcode: '#FFF8DC' },
{ name: 'cream can', hexcode: '#F5D76E' },
{ name: 'crimson', hexcode: '#DC143C' },
{ name: 'crusta', hexcode: '#F2784B' },
{ name: 'curious blue', hexcode: '#3498DB' },
{ name: 'cyan', hexcode: '#00FFFF' },
{ name: 'dark sea green', hexcode: '#90C695' },
{ name: 'darkblue', hexcode: '#00008B' },
{ name: 'darkcyan', hexcode: '#008B8B' },
{ name: 'darkgoldenrod', hexcode: '#B8860B' },
{ name: 'darkgray', hexcode: '#A9A9A9' },
{ name: 'darkgreen', hexcode: '#006400' },
{ name: 'darkgrey', hexcode: '#A9A9A9' },
{ name: 'darkkhaki', hexcode: '#BDB76B' },
{ name: 'darkmagenta', hexcode: '#8B008B' },
{ name: 'darkolivegreen', hexcode: '#556B2F' },
{ name: 'darkorange', hexcode: '#FF8C00' },
{ name: 'darkorchid', hexcode: '#9932CC' },
{ name: 'darkred', hexcode: '#8B0000' },
{ name: 'darksalmon', hexcode: '#E9967A' },
{ name: 'darkseagreen', hexcode: '#8FBC8F' },
{ name: 'darkslateblue', hexcode: '#483D8B' },
{ name: 'darkslategray', hexcode: '#2F4F4F' },
{ name: 'darkslategrey', hexcode: '#2F4F4F' },
{ name: 'darkturquoise', hexcode: '#00CED1' },
{ name: 'darkviolet', hexcode: '#9400D3' },
{ name: 'deeppink', hexcode: '#FF1493' },
{ name: 'deepskyblue', hexcode: '#00BFFF' },
{ name: 'dimgray', hexcode: '#696969' },
{ name: 'dimgrey', hexcode: '#696969' },
{ name: 'dodger blue', hexcode: '#19B5FE' },
{ name: 'dodgerblue', hexcode: '#1E90FF' },
{ name: 'downy', hexcode: '#65C6BB' },
{ name: 'ebony clay', hexcode: '#22313F' },
{ name: 'ecstasy', hexcode: '#F9690E' },
{ name: 'edward', hexcode: '#ABB7B7' },
{ name: 'emerald', hexcode: '#3FC380' },
{ name: 'eucalyptus', hexcode: '#26A65B' },
{ name: 'fire bush', hexcode: '#EB9532' },
{ name: 'firebrick', hexcode: '#B22222' },
{ name: 'flamingo', hexcode: '#EF4836' },
{ name: 'floralwhite', hexcode: '#FFFAF0' },
{ name: 'forestgreen', hexcode: '#228B22' },
{ name: 'fountain blue', hexcode: '#5C97BF' },
{ name: 'fuchsia', hexcode: '#FF00FF' },
{ name: 'gainsboro', hexcode: '#DCDCDC' },
{ name: 'gallery', hexcode: '#EEEEEE' },
{ name: 'ghostwhite', hexcode: '#F8F8FF' },
{ name: 'gold', hexcode: '#FFD700' },
{ name: 'goldenrod', hexcode: '#DAA520' },
{ name: 'gossip', hexcode: '#87D37C' },
{ name: 'gray', hexcode: '#808080' },
{ name: 'green haze', hexcode: '#019875' },
{ name: 'green', hexcode: '#008000' },
{ name: 'greenyellow', hexcode: '#ADFF2F' },
{ name: 'grey', hexcode: '#808080' },
{ name: 'hoki', hexcode: '#67809F' },
{ name: 'honey flower', hexcode: '#674172' },
{ name: 'honeydew', hexcode: '#F0FFF0' },
{ name: 'hotpink', hexcode: '#FF69B4' },
{ name: 'humming bird', hexcode: '#C5EFF7' },
{ name: 'indianred', hexcode: '#CD5C5C' },
{ name: 'indigo', hexcode: '#4B0082' },
{ name: 'iron', hexcode: '#DADFE1' },
{ name: 'ivory', hexcode: '#FFFFF0' },
{ name: 'jacksons purple', hexcode: '#1F3A93' },
{ name: 'jade', hexcode: '#00B16A' },
{ name: 'jaffa', hexcode: '#F27935' },
{ name: 'jelly bean', hexcode: '#2574A9' },
{ name: 'jordy blue', hexcode: '#89C4F4' },
{ name: 'jungle green', hexcode: '#26C281' },
{ name: 'khaki', hexcode: '#F0E68C' },
{ name: 'lavender', hexcode: '#E6E6FA' },
{ name: 'lavenderblush', hexcode: '#FFF0F5' },
{ name: 'lawngreen', hexcode: '#7CFC00' },
{ name: 'lemonchiffon', hexcode: '#FFFACD' },
{ name: 'light sea green', hexcode: '#1BA39C' },
{ name: 'light wisteria', hexcode: '#BE90D4' },
{ name: 'lightblue', hexcode: '#ADD8E6' },
{ name: 'lightcoral', hexcode: '#F08080' },
{ name: 'lightcyan', hexcode: '#E0FFFF' },
{ name: 'lightgoldenrodyellow', hexcode: '#FAFAD2' },
{ name: 'lightgray', hexcode: '#D3D3D3' },
{ name: 'lightgreen', hexcode: '#90EE90' },
{ name: 'lightgrey', hexcode: '#D3D3D3' },
{ name: 'lightning yellow', hexcode: '#F5AB35' },
{ name: 'lightpink', hexcode: '#FFB6C1' },
{ name: 'lightsalmon', hexcode: '#FFA07A' },
{ name: 'lightseagreen', hexcode: '#20B2AA' },
{ name: 'lightskyblue', hexcode: '#87CEFA' },
{ name: 'lightslategray', hexcode: '#778899' },
{ name: 'lightslategrey', hexcode: '#778899' },
{ name: 'lightsteelblue', hexcode: '#B0C4DE' },
{ name: 'lightyellow', hexcode: '#FFFFE0' },
{ name: 'lime', hexcode: '#00FF00' },
{ name: 'limegreen', hexcode: '#32CD32' },
{ name: 'linen', hexcode: '#FAF0E6' },
{ name: 'lynch', hexcode: '#6C7A89' },
{ name: 'madang', hexcode: '#C8F7C5' },
{ name: 'madison', hexcode: '#2C3E50' },
{ name: 'magenta', hexcode: '#FF00FF' },
{ name: 'malibu', hexcode: '#6BB9F0' },
{ name: 'maroon', hexcode: '#800000' },
{ name: 'medium aquamarine', hexcode: '#66CC99' },
{ name: 'medium purple', hexcode: '#BF55EC' },
{ name: 'medium turquoise', hexcode: '#4ECDC4' },
{ name: 'mediumaquamarine', hexcode: '#66CDAA' },
{ name: 'mediumblue', hexcode: '#0000CD' },
{ name: 'mediumorchid', hexcode: '#BA55D3' },
{ name: 'mediumpurple', hexcode: '#9370DB' },
{ name: 'mediumseagreen', hexcode: '#3CB371' },
{ name: 'mediumslateblue', hexcode: '#7B68EE' },
{ name: 'mediumspringgreen', hexcode: '#00FA9A' },
{ name: 'mediumturquoise', hexcode: '#48D1CC' },
{ name: 'mediumvioletred', hexcode: '#C71585' },
{ name: 'midnightblue', hexcode: '#191970' },
{ name: 'ming', hexcode: '#336E7B' },
{ name: 'mintcream', hexcode: '#F5FFFA' },
{ name: 'mistyrose', hexcode: '#FFE4E1' },
{ name: 'moccasin', hexcode: '#FFE4B5' },
{ name: 'monza', hexcode: '#CF000F' },
{ name: 'mountain meadow', hexcode: '#1BBC9B' },
{ name: 'navajowhite', hexcode: '#FFDEAD' },
{ name: 'navy', hexcode: '#000080' },
{ name: 'new york pink', hexcode: '#E08283' },
{ name: 'niagara 1', hexcode: '#2ABB9B' },
{ name: 'niagara', hexcode: '#16A085' },
{ name: 'observatory', hexcode: '#049372' },
{ name: 'ocean green', hexcode: '#4DAF7C' },
{ name: 'old brick', hexcode: '#96281B' },
{ name: 'oldlace', hexcode: '#FDF5E6' },
{ name: 'olive', hexcode: '#808000' },
{ name: 'olivedrab', hexcode: '#6B8E23' },
{ name: 'orange', hexcode: '#FFA500' },
{ name: 'orangered', hexcode: '#FF4500' },
{ name: 'orchid', hexcode: '#DA70D6' },
{ name: 'palegoldenrod', hexcode: '#EEE8AA' },
{ name: 'palegreen', hexcode: '#98FB98' },
{ name: 'paleturquoise', hexcode: '#AFEEEE' },
{ name: 'palevioletred', hexcode: '#DB7093' },
{ name: 'papayawhip', hexcode: '#FFEFD5' },
{ name: 'peachpuff', hexcode: '#FFDAB9' },
{ name: 'peru', hexcode: '#CD853F' },
{ name: 'pickled bluewood', hexcode: '#34495E' },
{ name: 'picton blue 2', hexcode: '#22A7F0' },
{ name: 'picton blue', hexcode: '#59ABE3' },
{ name: 'pink', hexcode: '#FFC0CB' },
{ name: 'plum', hexcode: '#913D88' },
{ name: 'plum', hexcode: '#DDA0DD' },
{ name: 'pomegranate', hexcode: '#F22613' },
{ name: 'porcelain', hexcode: '#ECF0F1' },
{ name: 'powderblue', hexcode: '#B0E0E6' },
{ name: 'pumice', hexcode: '#D2D7D3' },
{ name: 'purple', hexcode: '#800080' },
{ name: 'radical red', hexcode: '#F62459' },
{ name: 'razzmatazz', hexcode: '#DB0A5B' },
{ name: 'rebeccapurple', hexcode: '#663399' },
{ name: 'red', hexcode: '#FF0000' },
{ name: 'ripe lemon', hexcode: '#F7CA18' },
{ name: 'riptide', hexcode: '#86E2D5' },
{ name: 'rosybrown', hexcode: '#BC8F8F' },
{ name: 'royal blue', hexcode: '#4183D7' },
{ name: 'royalblue', hexcode: '#4169E1' },
{ name: 'saddlebrown', hexcode: '#8B4513' },
{ name: 'saffron', hexcode: '#F4D03F' },
{ name: 'salem', hexcode: '#1E824C' },
{ name: 'salmon', hexcode: '#FA8072' },
{ name: 'san marino', hexcode: '#446CB3' },
{ name: 'sandstorm', hexcode: '#F9BF3B' },
{ name: 'sandybrown', hexcode: '#F4A460' },
{ name: 'sea buckthorn', hexcode: '#EB974E' },
{ name: 'seagreen', hexcode: '#2E8B57' },
{ name: 'seance', hexcode: '#9A12B3' },
{ name: 'seashell', hexcode: '#FFF5EE' },
{ name: 'shakespeare', hexcode: '#52B3D9' },
{ name: 'shamrock', hexcode: '#2ECC71' },
{ name: 'sienna', hexcode: '#A0522D' },
{ name: 'silver sand', hexcode: '#BDC3C7' },
{ name: 'silver tree', hexcode: '#68C3A3' },
{ name: 'silver', hexcode: '#BFBFBF' },
{ name: 'silver', hexcode: '#C0C0C0' },
{ name: 'skyblue', hexcode: '#87CEEB' },
{ name: 'slateblue', hexcode: '#6A5ACD' },
{ name: 'slategray', hexcode: '#708090' },
{ name: 'slategrey', hexcode: '#708090' },
{ name: 'snow', hexcode: '#FFFAFA' },
{ name: 'snuff', hexcode: '#DCC6E0' },
{ name: 'soft red', hexcode: '#EC644B' },
{ name: 'spray', hexcode: '#81CFE0' },
{ name: 'springgreen', hexcode: '#00FF7F' },
{ name: 'steel blue', hexcode: '#4B77BE' },
{ name: 'steelblue', hexcode: '#4682B4' },
{ name: 'studio', hexcode: '#8E44AD' },
{ name: 'summer sky', hexcode: '#1E8BC3' },
{ name: 'sunglo', hexcode: '#E26A6A' },
{ name: 'sunset orange', hexcode: '#F64747' },
{ name: 'tahiti gold', hexcode: '#E87E04' },
{ name: 'tall poppy', hexcode: '#C0392B' },
{ name: 'tan', hexcode: '#D2B48C' },
{ name: 'teal', hexcode: '#008080' },
{ name: 'thistle', hexcode: '#D8BFD8' },
{ name: 'thunderbird', hexcode: '#D91E18' },
{ name: 'tomato', hexcode: '#FF6347' },
{ name: 'turquoise', hexcode: '#36D7B7' },
{ name: 'turquoise', hexcode: '#40E0D0' },
{ name: 'valencia', hexcode: '#D64541' },
{ name: 'violet', hexcode: '#EE82EE' },
{ name: 'wax flower', hexcode: '#F1A9A0' },
{ name: 'wheat', hexcode: '#F5DEB3' },
{ name: 'white smoke', hexcode: '#ECECEC' },
{ name: 'white', hexcode: '#FFFFFF' },
{ name: 'whitesmoke', hexcode: '#F5F5F5' },
{ name: 'wisteria', hexcode: '#9B59B6' },
{ name: 'wistful', hexcode: '#AEA8D3' },
{ name: 'yellow', hexcode: '#FFFF00' },
{ name: 'yellowgreen', hexcode: '#9ACD32' },
{ name: 'zest', hexcode: '#E67E22' }
]
end
end
end

@ -49,7 +49,7 @@ module BasicData
#
# @return [Array<Hash>] List of attributes for each type.
def data
colors = PlanningElementTypeColor.all
colors = Color.all
colors = colors.map { |c| { c.name => c.id } }.reduce({}, :merge)
type_table.map do |name, values|

@ -29,7 +29,7 @@
module BasicData
class WorkflowSeeder < Seeder
def seed_data!
colors = PlanningElementTypeColor.all
colors = Color.all
colors = colors.map { |c| { c.name => c.id } }.reduce({}, :merge)
if WorkPackage.where(type_id: nil).any? || Journal::WorkPackageJournal.where(type_id: nil).any?

@ -33,6 +33,7 @@ class BasicDataSeeder < CompositeSeeder
BasicData::RoleSeeder,
BasicData::ActivitySeeder,
BasicData::ColorSeeder,
BasicData::FlatColorSeeder,
BasicData::WorkflowSeeder,
BasicData::PrioritySeeder,
BasicData::ProjectTypeSeeder,

@ -39,7 +39,7 @@ See docs/COPYRIGHT.rdoc for more details.
</div>
<div class="form--field">
<label class="form--label -required">
<%= t('activerecord.attributes.planning_element_type_color.hexcode') %>
<%= t('activerecord.attributes.color.hexcode') %>
<span class="form--label-required" aria-hidden="true">*</span>
</label>
<span class="form--field-container">

@ -0,0 +1,58 @@
<%#-- copyright
OpenProject is a project management system.
Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2017 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See docs/COPYRIGHT.rdoc for more details.
++#%>
<% html_title l(:label_administration), l("timelines.admin_menu.colors") %>
<%= toolbar title: l('timelines.admin_menu.colors') do %>
<li class="toolbar-item">
<%= link_to new_color_path,
{ class: 'button -alt-highlight',
aria: {label: l('timelines.new_color')},
title: l('timelines.new_color')} do %>
<%= op_icon('button--icon icon-add') %>
<span class="button--text"><%= t('activerecord.attributes.type.color') %></span>
<% end %>
</li>
<% end %>
<% if @colors.any? %>
<div class="color--preview-patch-field">
<% @colors.each do |color| %>
<%= content_tag :div,
class: 'color--preview-patch',
style: "background-color: #{color.hexcode}" do %>
<%= link_to color.name,
edit_color_path(color),
class: color.bright? ? '-bright' : '-dark' %>
<% end %>
<% end %>
</div>
<% else %>
<%= no_results_box(action_url: new_color_path, display_action: true) unless @colors.any? %>
<% end %>

@ -1,111 +0,0 @@
<%#-- copyright
OpenProject is a project management system.
Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2017 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See docs/COPYRIGHT.rdoc for more details.
++#%>
<% html_title l(:label_administration), l("timelines.admin_menu.colors") %>
<%= toolbar title: l('timelines.admin_menu.colors') do %>
<li class="toolbar-item">
<%= link_to new_color_path,
{ class: 'button -alt-highlight',
aria: {label: l('timelines.new_color')},
title: l('timelines.new_color')} do %>
<%= op_icon('button--icon icon-add') %>
<span class="button--text"><%= t('activerecord.attributes.type.color') %></span>
<% end %>
</li>
<% end %>
<% if @colors.any? %>
<div class="generic-table--container">
<div class="generic-table--results-container">
<table class="generic-table">
<colgroup>
<col highlight-col>
<col highlight-col>
<col highlight-col>
<col>
</colgroup>
<thead>
<tr>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%= Type.human_attribute_name(:name) %>
</span>
</div>
</div>
</th>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%= Type.human_attribute_name(:color) %>
</span>
</div>
</div>
</th>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%=l(:button_sort)%>
</span>
</div>
</div>
</th>
<th><div class="generic-table--empty-header"></div></th>
</tr>
</thead>
<tbody>
<% @colors.each do |color| %>
<tr id="color-<%= color.id %>">
<td class="timelines-color-name"><%= link_to(h(color.name), edit_color_path(color)) %></td>
<td class="timelines-color-hexcode">
<%= colored_text(color) %>
<%= icon_for_color(color, class: 'standalone') %>
</td>
<td class="timelines-color-reorder"><%= reorder_links('color', {action: 'move', id: color}) %></td>
<td class="timelines-color-actions buttons">
<%= link_to(confirm_destroy_color_path(color),
class: 'icon icon-delete') do %>
<%= l(:button_delete) %>
<span class="hidden-for-sighted"><%=h color.name %></span>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<%= no_results_box(action_url: new_color_path, display_action: true) %>
<% end %>

@ -43,10 +43,16 @@ See docs/COPYRIGHT.rdoc for more details.
<% end %>
<div class="form--field">
<%= f.select :color_id, container_class: '-slim' do %>
<%= options_for_colors(@status) %>
<% end %>
<div class="form--field-instructions"><%= t('statuses.edit.status_color_text') %></div>
<label class="form--label" for="status_color_id">
<%= Status.human_attribute_name 'color' %>
</label>
<div class="form--field-container">
<div class="form--select-container -middle">
<colors-autocompleter label="<%= Status.human_attribute_name 'color' %>">
<%= select_tag 'status[color_id]', options_for_colors(@status), hidden: true %>
</colors-autocompleter>
<div class="form--field-instructions -no-margin"><%= t('statuses.edit.status_color_text') %></div>
</div>
</div>
<%= call_hook(:view_statuses_form, status: @status) %>

@ -207,7 +207,7 @@ Redmine::MenuManager.map :admin_menu do |menu|
icon: 'icon2 icon-design'
menu.push :colors,
{ controller: '/planning_element_type_colors', action: 'index' },
{ controller: '/colors', action: 'index' },
caption: :'timelines.admin_menu.colors',
icon: 'icon2 icon-status'

@ -187,7 +187,7 @@ en:
statuses:
edit:
status_color_text: |
Assign a color to the status.
Click to assign or change the color of this status.
It is shown in the status button and can be used for highlighting work packages in the table.
index:
no_results_title_text: There are currently no work package statuses.

@ -516,7 +516,7 @@ OpenProject::Application.routes.draw do
get 'authentication' => 'authentication#index'
resources :colors, controller: 'planning_element_type_colors' do
resources :colors do
member do
get :confirm_destroy
get :move

@ -0,0 +1,14 @@
class RenamePlanningElemntTypeColorsToColors < ActiveRecord::Migration[5.1]
def up
rename_table :planning_element_type_colors, :colors
remove_column :colors, :position
end
def down
rename_table :colors, :planning_element_type_colors
change_table :planning_element_type_colors do
t.integer :position, default: 1, null: true
end
end
end

@ -28,7 +28,7 @@
require_relative 'base'
class Tables::PlanningElementTypeColors < Tables::Base
class Tables::Colors < Tables::Base
def self.table(migration)
create_table migration do |t|
t.column :name, :string, null: false

@ -221,6 +221,7 @@ import {FocusWithinDirective} from "core-components/common/focus/focus-within.up
import {AccessibleClickDirective} from "core-components/a11y/accessible-click.directive";
import {WorkPackageChildrenQueryComponent} from 'core-components/wp-relations/wp-relation-children/wp-children-query.component';
import {StatusCacheService} from 'core-components/status/status-cache.service';
import {ColorsAutocompleter} from 'core-components/colors/colors-autocompleter.component';
@NgModule({
imports: [
@ -361,6 +362,7 @@ import {StatusCacheService} from 'core-components/status/status-cache.service';
HideSectionLinkComponent,
AddSectionDropdownComponent,
AutocompleteSelectDecorationComponent,
ColorsAutocompleter,
// Split view
WorkPackageSplitViewComponent,
@ -535,6 +537,9 @@ import {StatusCacheService} from 'core-components/status/status-cache.service';
// External query configuration
ExternalQueryConfigurationComponent,
// Color autocompleter
ColorsAutocompleter,
]
})
export class OpenProjectModule {

@ -29,11 +29,11 @@
export namespace ColorContrast {
/**
* Computer the best color for contrasting the given color.
* Compute the best color for contrasting the given color.
* Based on http://24ways.org/2010/calculating-color-contrast
*
* Remember there is a background counterpart for this function
* in planning_element_type_color.rb
* in color.rb
*
* (#333/white)
* @param hexcolor The normalized hex color
@ -42,13 +42,32 @@ export namespace ColorContrast {
if (hexcolor == null) {
return null;
}
if (tooBrightForWhite(hexcolor)) {
return '#333333';
} else {
return '#FFFFFF';
}
}
export function getColorPatch(hexcolor:string):{ bg:string, fg:string } {
if (tooBrightForWhite(hexcolor)) {
return { fg: '#333333', bg: hexcolor };
} else {
return { bg: '#FFFFFF', fg: hexcolor };
}
}
export function tooBrightForWhite(hexcolor:string|null|undefined):boolean {
if (hexcolor == null) {
return false;
}
var r = parseInt(hexcolor.substr(0, 2), 16);
var g = parseInt(hexcolor.substr(2, 2), 16);
var b = parseInt(hexcolor.substr(4, 2), 16);
var r = parseInt(hexcolor.substr(1, 2), 16);
var g = parseInt(hexcolor.substr(3, 2), 16);
var b = parseInt(hexcolor.substr(5, 2), 16);
var yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
return (yiq >= 128) ? '#333333' : '#FFFFFF';
return (yiq >= 128);
}
}

@ -0,0 +1,22 @@
<ng-content style="display:none"></ng-content>
<a class="colors-autocompleter--selected-value"
role="button"
tabindex="0"
(click)="editUnlessMulti()"
*ngIf="selectedColor">
<span class="color--preview"
[style.background-color]="selectedColor"></span>
<span class="color--text-preview"
[textContent]="selectedItem.label"
[style.background-color]="bgColor"
[style.color]="fgColor"></span>
</a>
<input
[hidden]="selectedItems.length"
type="text"
class="form--input -autocomplete"
[attr.placeholder]="placeholderText">

@ -0,0 +1,139 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {Component, ElementRef, Inject, Input, OnInit} from '@angular/core';
import {opUiComponentsModule} from 'core-app/angular-modules';
import {downgradeComponent} from '@angular/upgrade/static';
import {I18nToken} from 'core-app/angular4-transition-utils';
import {AutocompleteSelectDecorationComponent} from 'core-components/common/autocomplete-select-decoration/autocomplete-select-decoration.component';
import {ColorContrast} from 'core-components/a11y/color-contrast.functions';
interface ColorAutocompleteItem {
id:number;
label:string;
color:string;
value:string;
}
@Component({
template: require('!!raw-loader!./colors-autocompleter.component.html'),
selector: 'colors-autocompleter',
})
export class ColorsAutocompleter extends AutocompleteSelectDecorationComponent<ColorAutocompleteItem> {
protected getItems() {
_.each(this.$select.find('option'), option => {
let $option = jQuery(option);
let text = $option.text();
let item = {
id: $option.prop('value'),
color: $option.data('color'),
label: text,
value: text
};
this.allItems.push(item);
if ($option.prop('selected')) {
this.selectedItems.push(item);
}
});
}
public get selectedColorIsBright():boolean {
return !!this.selectedColor && ColorContrast.tooBrightForWhite(this.selectedColor);
}
public get fgColor() {
if (this.selectedColor) {
return ColorContrast.getColorPatch(this.selectedColor).fg;
}
return null;
}
public get bgColor() {
if (this.selectedColor) {
return ColorContrast.getColorPatch(this.selectedColor).bg;
}
return null;
}
public get selectedItem():ColorAutocompleteItem|undefined {
return this.selectedItems.length > 0 ? this.selectedItems[0] : undefined;
}
public get selectedColor():string|undefined {
return this.selectedItem ? this.selectedItem.color : undefined;
}
protected setupAutocompleter() {
super.setupAutocompleter();
const autocompleter = this.$input.autocomplete('instance');
const menu = autocompleter.menu;
this.$input.focus(function() {
autocompleter.search();
});
autocompleter._renderItem = function(this:any, ul:JQuery, item:ColorAutocompleteItem) {
const term = this.element.val();
const patch = ColorContrast.getColorPatch(item.color);
const colorSquare = jQuery('<span>')
.addClass('color--preview')
.css('background-color', item.color);
const colorText = jQuery('<span>')
.addClass('color--text-preview')
.css('color', patch.fg)
.css('background-color', patch.bg)
.text(item.label);
const div = jQuery('<div>')
.append(colorSquare)
.append(colorText)
.addClass('ui-menu-item-wrapper');
const element = jQuery('<li>')
.append(div)
.appendTo(ul);
return element;
};
}
}
opUiComponentsModule.directive(
'colorsAutocompleter',
downgradeComponent({component: ColorsAutocompleter})
);

@ -42,14 +42,14 @@ interface AutocompleteSelectDecorationItem {
selector: 'autocomplete-select-decoration',
})
export class AutocompleteSelectDecorationComponent implements OnInit {
export class AutocompleteSelectDecorationComponent<T extends AutocompleteSelectDecorationItem> implements OnInit {
public selectedItems:AutocompleteSelectDecorationItem[] = [];
private allItems:AutocompleteSelectDecorationItem[] = [];
private $select:any = null;
private $input:any = null;
private isMulti:boolean = true;
private label:string;
public selectedItems:T[] = [];
protected allItems:T[] = [];
protected $select:any = null;
protected $input:any = null;
protected isMulti:boolean = true;
protected label:string;
@Input('label') labelOverride:string|null = null;
@ -57,7 +57,7 @@ export class AutocompleteSelectDecorationComponent implements OnInit {
@Inject(I18nToken) readonly I18n:op.I18n) {
}
public remove(item:AutocompleteSelectDecorationItem) {
public remove(item:T) {
_.remove(this.selectedItems, (selected) => selected.id === item.id);
let val = this.$select.val();
@ -97,15 +97,15 @@ export class AutocompleteSelectDecorationComponent implements OnInit {
return I18n.t(key, { name: this.label });
}
public removeItemText(item:AutocompleteSelectDecorationItem) {
public removeItemText(item:T) {
return I18n.t('js.autocomplete_select.remove', { name: item.value });
}
public ariaLabelText(item:AutocompleteSelectDecorationItem) {
public ariaLabelText(item:T) {
return I18n.t('js.autocomplete_select.active', { label: this.label, name: item.value });
}
private getItems() {
protected getItems() {
_.each(this.$select.find('option'), option => {
let $option = jQuery(option);
let text = $option.text();
@ -114,7 +114,7 @@ export class AutocompleteSelectDecorationComponent implements OnInit {
id: $option.prop('value'),
label: text,
value: text
};
} as T;
this.allItems.push(item);
@ -124,8 +124,17 @@ export class AutocompleteSelectDecorationComponent implements OnInit {
});
}
private setupAutocompleter() {
let autocompleteOptions = {
protected setupAutocompleter() {
this.$input.autocomplete(this.getAutocompleterOptions());
// Disable handling all dashes as dividers
// https://github.com/jquery/jquery-ui/blob/master/ui/widgets/menu.js#L347
// as we use them as placeholders.
(this.$input.autocomplete('instance')).menu._isDivider = () => false;
}
protected getAutocompleterOptions():any {
return {
delay: 100,
minLength: 0,
position: { my: 'left top', at: 'left bottom', collision: 'flip' },
@ -146,14 +155,7 @@ export class AutocompleteSelectDecorationComponent implements OnInit {
this.$input.val('');
return false;
}
} as any;
this.$input.autocomplete(autocompleteOptions);
// Disable handling all dashes as dividers
// https://github.com/jquery/jquery-ui/blob/master/ui/widgets/menu.js#L347
// as we use them as placeholders.
(this.$input.autocomplete('instance')).menu._isDivider = () => false;
};
}
private switchIds() {
@ -181,7 +183,7 @@ export class AutocompleteSelectDecorationComponent implements OnInit {
return jQuery(this.elementRef.nativeElement);
}
private setValue(item:AutocompleteSelectDecorationItem|null) {
protected setValue(item:T|null) {
if (item === null) {
this.selectedItems = [];
} else if (this.isMulti) {

@ -31,7 +31,7 @@ import {InputState, MultiInputState, State} from 'reactivestates';
export abstract class StateCacheService<T> {
private cacheDurationInMs:number;
constructor(private holdValuesForSeconds:number = 120) {
constructor(private holdValuesForSeconds:number = 3600) {
this.cacheDurationInMs = holdValuesForSeconds * 1000;
}

@ -32,24 +32,50 @@ import {Component, Inject, Input} from '@angular/core';
import {I18nToken} from 'core-app/angular4-transition-utils';
import {States} from 'core-components/states.service';
import {ColorContrast} from 'core-components/a11y/color-contrast.functions';
import {StatusCacheService} from 'core-app/components/status/status-cache.service';
import {StatusResource} from 'core-app/modules/hal/resources/status-resource';
import {OnInit, OnDestroy} from '@angular/core';
import {takeUntil} from 'rxjs/operators';
import {componentDestroyed} from 'ng2-rx-componentdestroyed';
@Component({
template: require('!!raw-loader!./wp-status-button.html'),
selector: 'wp-status-button',
})
export class WorkPackageStatusButtonComponent {
export class WorkPackageStatusButtonComponent implements OnInit, OnDestroy {
@Input('workPackage') public workPackage:WorkPackageResource;
@Input('allowed') public allowed:boolean;
public status:StatusResource;
public text = {
explanation: this.I18n.t('js.label_edit_status')
};
constructor(@Inject(I18nToken) readonly I18n:op.I18n,
readonly states:States,
readonly statusCache:StatusCacheService,
protected wpEditing:WorkPackageEditingService) {
}
public ngOnInit() {
this.status = this.workPackage.status;
this.wpEditing
.state(this.workPackage.id)
.values$()
.pipe(
takeUntil(componentDestroyed(this))
)
.subscribe(async (changeset) => {
const status:StatusResource = changeset.value('status');
this.status = await this.statusCache.require(status.idFromLink);
});
}
public ngOnDestroy() {
// Nothing to do.
}
public isDisabled() {
let changeset = this.wpEditing.changesetFor(this.workPackage);
return !this.allowed || changeset.inFlight;
@ -60,13 +86,6 @@ export class WorkPackageStatusButtonComponent {
}
public get bgColor() {
return this.getStatus.color;
}
public get getStatus() {
let changeset = this.wpEditing.changesetFor(this.workPackage);
let status = changeset.value('status');
return this.states.statuses.get(status.id).getValueOr(status);
return this.status.color;
}
}

@ -1,4 +1,4 @@
<div class="wp-status-button">
<div class="wp-status-button" *ngIf="status">
<button class="button"
[disabled]="isDisabled()"
[attr.aria-label]="text.explanation"
@ -10,7 +10,7 @@
<span class="button--text"
aria-hidden="true"
[textContent]="getStatus.name"></span>
[textContent]="status.name"></span>
<op-icon icon-classes="button--icon icon-small icon-pulldown"></op-icon>
</button>
</div>

@ -28,7 +28,7 @@
require 'spec_helper'
describe PlanningElementTypeColorsController, type: :controller do
describe ColorsController, type: :controller do
let(:current_user) { FactoryGirl.create(:admin) }
before do

@ -27,10 +27,9 @@
#++
FactoryGirl.define do
factory(:color, class: PlanningElementTypeColor) do
factory(:color, class: Color) do
sequence(:name) do |n| "Color No. #{n}" end
hexcode do ('#%0.6x' % rand(0xFFFFFF)).upcase end
sequence(:position) { |n| n }
end
end

@ -28,7 +28,7 @@
require 'spec_helper'
describe PlanningElementTypeColor, type: :model do
describe Color, type: :model do
describe '- Relations ' do
describe '#planning_element_types' do
it 'can read planning_element_types w/ the help of the has_many association' do
@ -65,7 +65,7 @@ describe PlanningElementTypeColor, type: :model do
describe 'name' do
it 'is invalid w/o a name' do
attributes[:name] = nil
color = PlanningElementTypeColor.new(attributes)
color = Color.new(attributes)
expect(color).not_to be_valid
@ -75,7 +75,7 @@ describe PlanningElementTypeColor, type: :model do
it 'is invalid w/ a name longer than 255 characters' do
attributes[:name] = 'A' * 500
color = PlanningElementTypeColor.new(attributes)
color = Color.new(attributes)
expect(color).not_to be_valid
@ -87,7 +87,7 @@ describe PlanningElementTypeColor, type: :model do
describe 'hexcode' do
it 'is invalid w/o a hexcode' do
attributes[:hexcode] = nil
color = PlanningElementTypeColor.new(attributes)
color = Color.new(attributes)
expect(color).not_to be_valid
@ -96,32 +96,32 @@ describe PlanningElementTypeColor, type: :model do
end
it 'is invalid w/ malformed hexcodes' do
expect(PlanningElementTypeColor.new(attributes.merge(hexcode: '0#FFFFFF'))).not_to be_valid
expect(PlanningElementTypeColor.new(attributes.merge(hexcode: '#FFFFFF0'))).not_to be_valid
expect(PlanningElementTypeColor.new(attributes.merge(hexcode: 'white'))). not_to be_valid
expect(Color.new(attributes.merge(hexcode: '0#FFFFFF'))).not_to be_valid
expect(Color.new(attributes.merge(hexcode: '#FFFFFF0'))).not_to be_valid
expect(Color.new(attributes.merge(hexcode: 'white'))). not_to be_valid
end
it 'fixes some wrong formats of hexcode automatically' do
color = PlanningElementTypeColor.new(attributes.merge(hexcode: 'FFCC33'))
color = Color.new(attributes.merge(hexcode: 'FFCC33'))
expect(color).to be_valid
expect(color.hexcode).to eq('#FFCC33')
color = PlanningElementTypeColor.new(attributes.merge(hexcode: '#ffcc33'))
color = Color.new(attributes.merge(hexcode: '#ffcc33'))
expect(color).to be_valid
expect(color.hexcode).to eq('#FFCC33')
color = PlanningElementTypeColor.new(attributes.merge(hexcode: 'fc3'))
color = Color.new(attributes.merge(hexcode: 'fc3'))
expect(color).to be_valid
expect(color.hexcode).to eq('#FFCC33')
color = PlanningElementTypeColor.new(attributes.merge(hexcode: '#fc3'))
color = Color.new(attributes.merge(hexcode: '#fc3'))
expect(color).to be_valid
expect(color.hexcode).to eq('#FFCC33')
end
it 'is valid w/ proper hexcodes' do
expect(PlanningElementTypeColor.new(attributes.merge(hexcode: '#FFFFFF'))). to be_valid
expect(PlanningElementTypeColor.new(attributes.merge(hexcode: '#FF00FF'))).to be_valid
expect(Color.new(attributes.merge(hexcode: '#FFFFFF'))). to be_valid
expect(Color.new(attributes.merge(hexcode: '#FF00FF'))).to be_valid
end
end
end
Loading…
Cancel
Save