Commit c428bacd authored by Andrew Smith's avatar Andrew Smith

Implement colour attribute for epics

Refs https://gitlab.com/gitlab-org/gitlab/-/issues/7641

Changelog: added
Signed-off-by: default avatarAndrew Smith <espadav8@gmail.com>
parent ed050950
# frozen_string_literal: true
class AddColorToEpics < Gitlab::Database::Migration[1.0]
# rubocop:disable Migration/AddLimitToTextColumns
# limit is added in 20211021124715_add_text_limit_to_epics_color
def change
add_column :epics, :color, :text, default: '#1068bf'
end
# rubocop:enable Migration/AddLimitToTextColumns
end
# frozen_string_literal: true
class AddTextLimitToEpicsColor < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_text_limit :epics, :color, 7
end
def down
remove_text_limit :epics, :color
end
end
93960203e6703716f9c513dca340e17041a33792f9233dc4b7e35d1e19614191
\ No newline at end of file
406af18458c7f5ee8a4fa3860ed5fb87c358363926fed2830be8e8a55578822b
\ No newline at end of file
......@@ -14137,6 +14137,8 @@ CREATE TABLE epics (
due_date_sourcing_epic_id integer,
confidential boolean DEFAULT false NOT NULL,
external_key character varying(255),
color text DEFAULT '#1068bf'::text,
CONSTRAINT check_ca608c40b3 CHECK ((char_length(color) <= 7)),
CONSTRAINT check_fcfb4a93ff CHECK ((lock_version IS NOT NULL))
);
......@@ -131,6 +131,7 @@ Example response:
"labels": [],
"upvotes": 4,
"downvotes": 0,
"color": "#1068bf",
"_links":{
"self": "http://gitlab.example.com/api/v4/groups/7/epics/4",
"epic_issues": "http://gitlab.example.com/api/v4/groups/7/epics/4/issues",
......@@ -179,6 +180,7 @@ Example response:
"labels": [],
"upvotes": 4,
"downvotes": 0,
"color": "#1068bf",
"_links":{
"self": "http://gitlab.example.com/api/v4/groups/17/epics/35",
"epic_issues": "http://gitlab.example.com/api/v4/groups/17/epics/35/issues",
......@@ -252,6 +254,7 @@ Example response:
"labels": [],
"upvotes": 4,
"downvotes": 0,
"color": "#1068bf",
"subscribed": true,
"_links":{
"self": "http://gitlab.example.com/api/v4/groups/7/epics/5",
......@@ -283,6 +286,7 @@ POST /groups/:id/epics
| `title` | string | yes | The title of the epic |
| `labels` | string | no | The comma-separated list of labels |
| `description` | string | no | The description of the epic. Limited to 1,048,576 characters. |
| `color` | string | no | The color of the epic. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7641) in GitLab 14.8, behind a feature flag named `epic_highlight_color` (disabled by default) |
| `confidential` | boolean | no | Whether the epic should be confidential |
| `created_at` | string | no | When the epic was created. Date time string, ISO 8601 formatted, for example `2016-03-11T03:45:40Z` . Requires administrator or project/group owner privileges ([available](https://gitlab.com/gitlab-org/gitlab/-/issues/255309) in GitLab 13.5 and later) |
| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (in GitLab 11.3 and later) |
......@@ -340,6 +344,7 @@ Example response:
"labels": [],
"upvotes": 4,
"downvotes": 0,
"color": "#1068bf",
"_links":{
"self": "http://gitlab.example.com/api/v4/groups/7/epics/6",
"epic_issues": "http://gitlab.example.com/api/v4/groups/7/epics/6/issues",
......@@ -381,6 +386,7 @@ PUT /groups/:id/epics/:epic_iid
| `state_event` | string | no | State event for an epic. Set `close` to close the epic and `reopen` to reopen it (in GitLab 11.4 and later) |
| `title` | string | no | The title of an epic |
| `updated_at` | string | no | When the epic was updated. Date time string, ISO 8601 formatted, for example `2016-03-11T03:45:40Z` . Requires administrator or project/group owner privileges ([available](https://gitlab.com/gitlab-org/gitlab/-/issues/255309) in GitLab 13.5 and later) |
| `color` | string | no | The color of the epic. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7641) in GitLab 14.8, behind a feature flag named `epic_highlight_color` (disabled by default) |
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/1/epics/5?title=New%20Title&parent_id=29"
......@@ -430,7 +436,8 @@ Example response:
"closed_at": "2018-08-18T12:22:05.239Z",
"labels": [],
"upvotes": 4,
"downvotes": 0
"downvotes": 0,
"color": "#1068bf"
}
```
......
......@@ -1366,6 +1366,7 @@ Input type: `CreateEpicInput`
| ---- | ---- | ----------- |
| <a id="mutationcreateepicaddlabelids"></a>`addLabelIds` | [`[ID!]`](#id) | IDs of labels to be added to the epic. |
| <a id="mutationcreateepicclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationcreateepiccolor"></a>`color` | [`String`](#string) | Color of the epic. Available only when feature flag `epic_color_highlight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. |
| <a id="mutationcreateepicconfidential"></a>`confidential` | [`Boolean`](#boolean) | Indicates if the epic is confidential. |
| <a id="mutationcreateepicdescription"></a>`description` | [`String`](#string) | Description of the epic. |
| <a id="mutationcreateepicduedatefixed"></a>`dueDateFixed` | [`String`](#string) | End date of the epic. |
......@@ -4756,6 +4757,7 @@ Input type: `UpdateEpicInput`
| ---- | ---- | ----------- |
| <a id="mutationupdateepicaddlabelids"></a>`addLabelIds` | [`[ID!]`](#id) | IDs of labels to be added to the epic. |
| <a id="mutationupdateepicclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationupdateepiccolor"></a>`color` | [`String`](#string) | Color of the epic. Available only when feature flag `epic_color_highlight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. |
| <a id="mutationupdateepicconfidential"></a>`confidential` | [`Boolean`](#boolean) | Indicates if the epic is confidential. |
| <a id="mutationupdateepicdescription"></a>`description` | [`String`](#string) | Description of the epic. |
| <a id="mutationupdateepicduedatefixed"></a>`dueDateFixed` | [`String`](#string) | End date of the epic. |
......@@ -8850,6 +8852,7 @@ Represents an epic on an issue board.
| <a id="boardepicauthor"></a>`author` | [`UserCore!`](#usercore) | Author of the epic. |
| <a id="boardepicawardemoji"></a>`awardEmoji` | [`AwardEmojiConnection`](#awardemojiconnection) | List of award emojis associated with the epic. (see [Connections](#connections)) |
| <a id="boardepicclosedat"></a>`closedAt` | [`Time`](#time) | Timestamp of when the epic was closed. |
| <a id="boardepiccolor"></a>`color` | [`String!`](#string) | Color of the epic. Available only when feature flag `epic_color_highlight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. |
| <a id="boardepicconfidential"></a>`confidential` | [`Boolean`](#boolean) | Indicates if the epic is confidential. |
| <a id="boardepiccreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp of when the epic was created. |
| <a id="boardepicdescendantcounts"></a>`descendantCounts` | [`EpicDescendantCount`](#epicdescendantcount) | Number of open and closed descendant epics and issues. |
......@@ -8885,6 +8888,7 @@ Represents an epic on an issue board.
| <a id="boardepicstartdateisfixed"></a>`startDateIsFixed` | [`Boolean`](#boolean) | Indicates if the start date has been manually set. |
| <a id="boardepicstate"></a>`state` | [`EpicState!`](#epicstate) | State of the epic. |
| <a id="boardepicsubscribed"></a>`subscribed` | [`Boolean!`](#boolean) | Indicates the currently logged in user is subscribed to the epic. |
| <a id="boardepictextcolor"></a>`textColor` | [`String!`](#string) | Text color generated for the epic. Available only when feature flag `epic_color_highlight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. |
| <a id="boardepictitle"></a>`title` | [`String`](#string) | Title of the epic. |
| <a id="boardepictitlehtml"></a>`titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
| <a id="boardepicupdatedat"></a>`updatedAt` | [`Time`](#time) | Timestamp of when the epic was updated. |
......@@ -10378,6 +10382,7 @@ Represents an epic.
| <a id="epicauthor"></a>`author` | [`UserCore!`](#usercore) | Author of the epic. |
| <a id="epicawardemoji"></a>`awardEmoji` | [`AwardEmojiConnection`](#awardemojiconnection) | List of award emojis associated with the epic. (see [Connections](#connections)) |
| <a id="epicclosedat"></a>`closedAt` | [`Time`](#time) | Timestamp of when the epic was closed. |
| <a id="epiccolor"></a>`color` | [`String!`](#string) | Color of the epic. Available only when feature flag `epic_color_highlight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. |
| <a id="epicconfidential"></a>`confidential` | [`Boolean`](#boolean) | Indicates if the epic is confidential. |
| <a id="epiccreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp of when the epic was created. |
| <a id="epicdescendantcounts"></a>`descendantCounts` | [`EpicDescendantCount`](#epicdescendantcount) | Number of open and closed descendant epics and issues. |
......@@ -10413,6 +10418,7 @@ Represents an epic.
| <a id="epicstartdateisfixed"></a>`startDateIsFixed` | [`Boolean`](#boolean) | Indicates if the start date has been manually set. |
| <a id="epicstate"></a>`state` | [`EpicState!`](#epicstate) | State of the epic. |
| <a id="epicsubscribed"></a>`subscribed` | [`Boolean!`](#boolean) | Indicates the currently logged in user is subscribed to the epic. |
| <a id="epictextcolor"></a>`textColor` | [`String!`](#string) | Text color generated for the epic. Available only when feature flag `epic_color_highlight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. |
| <a id="epictitle"></a>`title` | [`String`](#string) | Title of the epic. |
| <a id="epictitlehtml"></a>`titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
| <a id="epicupdatedat"></a>`updatedAt` | [`Time`](#time) | Timestamp of when the epic was updated. |
......@@ -80,6 +80,7 @@ class Groups::EpicsController < Groups::ApplicationController
def epic_params_attributes
[
:color,
:title,
:description,
:start_date_fixed,
......
......@@ -51,6 +51,11 @@ module Mutations
[GraphQL::Types::ID],
required: false,
description: 'IDs of labels to be removed from the epic.'
argument :color,
GraphQL::Types::String,
required: false,
description: 'Color of the epic. Available only when feature flag `epic_color_highlight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice.'
end
def validate_arguments!(args)
......@@ -58,6 +63,8 @@ module Mutations
raise Gitlab::Graphql::Errors::ArgumentError,
'The list of epic attributes is empty'
end
args.delete(:color) unless Feature.enabled?(:epic_color_highlight)
end
end
end
......@@ -160,6 +160,14 @@ module Types
resolver: ::Resolvers::EpicAncestorsResolver,
description: 'Ancestors (parents) of the epic.'
field :color, GraphQL::Types::String, null: false,
description: 'Color of the epic.',
feature_flag: :epic_color_highlight
field :text_color, GraphQL::Types::String, null: false,
description: 'Text color generated for the epic.',
feature_flag: :epic_color_highlight
markdown_field :title_html, null: true
markdown_field :description_html, null: true
......
......@@ -21,11 +21,19 @@ module EE
include Todoable
include SortableTitle
DEFAULT_COLOR = '#1068bf'
default_value_for :color, allows_nil: false, value: DEFAULT_COLOR
enum state_id: {
opened: ::Epic.available_states[:opened],
closed: ::Epic.available_states[:closed]
}
validates :color, color: true, allow_blank: false
before_validation :strip_whitespace_from_color
alias_attribute :state, :state_id
belongs_to :closed_by, class_name: 'User'
......@@ -202,6 +210,20 @@ module EE
def usage_ping_record_epic_creation
::Gitlab::UsageDataCounters::EpicActivityUniqueCounter.track_epic_created_action(author: author)
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
def strip_whitespace_from_color
color.strip!
end
end
class_methods do
......@@ -347,6 +369,14 @@ module EE
end
end
def text_color
if light_color?(color)
'#333333'
else
'#FFFFFF'
end
end
def resource_parent
group
end
......
......@@ -23,6 +23,8 @@ class EpicEntity < IssuableEntity
expose :state
expose :lock_version
expose :confidential
expose :color
expose :text_color
expose :web_url do |epic|
group_epic_path(epic.group, epic)
......
---
name: epic_color_highlight
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79940
rollout_issue_url:
milestone: '14.9'
type: development
group: group::product planning
default_enabled: false
......@@ -91,6 +91,7 @@ module API
params do
requires :title, type: String, desc: 'The title of an epic'
optional :description, type: String, desc: 'The description of an epic'
optional :color, type: String, desc: 'The color of an epic'
optional :confidential, type: Boolean, desc: 'Indicates if the epic is confidential'
optional :created_at, type: DateTime, desc: 'Date time when the epic was created. Available only for admins and project owners.'
optional :start_date, as: :start_date_fixed, type: String, desc: 'The start date of an epic'
......@@ -105,6 +106,7 @@ module API
# Setting created_at is allowed only for admins and owners
params.delete(:created_at) unless current_user.can?(:set_epic_created_at, user_group)
params.delete(:color) unless Feature.enabled?(:epic_color_highlight)
epic = ::Epics::CreateService.new(group: user_group, current_user: current_user, params: declared_params(include_missing: false)).execute
if epic.valid?
......@@ -120,6 +122,7 @@ module API
params do
requires :epic_iid, type: Integer, desc: 'The internal ID of an epic'
optional :title, type: String, desc: 'The title of an epic'
optional :color, type: String, desc: 'The color of an epic'
optional :description, type: String, desc: 'The description of an epic'
optional :confidential, type: Boolean, desc: 'Indicates if the epic is confidential'
optional :updated_at, type: DateTime, desc: 'Date time when the epic was updated. Available only for admins and project owners.'
......@@ -132,13 +135,14 @@ module API
optional :remove_labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
optional :state_event, type: String, values: %w[reopen close], desc: 'State event for an epic'
optional :parent_id, type: Integer, desc: 'The id of a parent epic'
at_least_one_of :title, :description, :start_date_fixed, :start_date_is_fixed, :due_date_fixed, :due_date_is_fixed, :labels, :add_labels, :remove_labels, :state_event, :confidential, :parent_id
at_least_one_of :add_labels, :color, :confidential, :description, :due_date_fixed, :due_date_is_fixed, :labels, :parent_id, :remove_labels, :start_date_fixed, :start_date_is_fixed, :state_event, :title
end
put ':id/(-/)epics/:epic_iid' do
authorize_can_admin_epic!
# Setting updated_at is allowed only for admins and owners
params.delete(:updated_at) unless current_user.can?(:set_epic_updated_at, user_group)
params.delete(:color) unless Feature.enabled?(:epic_color_highlight)
update_params = declared_params(include_missing: false)
update_params.delete(:epic_iid)
......
......@@ -10,6 +10,8 @@ module EE
expose :id
expose :iid
expose :color
expose :text_color
expose :group_id
expose :parent_id
expose :parent_iid do |epic|
......
......@@ -27,7 +27,9 @@
"can_create_note": { "type": "boolean" }
},
"create_note_path": { "type": "string" },
"preview_note_path": { "type": "string" }
"preview_note_path": { "type": "string" },
"color": { "type": "string" },
"text_color": { "type": "string" }
},
"required": [
"id",
......
......@@ -62,7 +62,9 @@
"parent": { "type": "uri" },
"additionalProperties": false
}
}
},
"color": { "type": "string" },
"text_color": { "type": "string" }
},
"required": [
"id", "iid", "group_id", "title", "confidential", "_links"
......
......@@ -14,7 +14,7 @@ RSpec.describe GitlabSchema.types['Epic'] do
notes discussions relative_position subscribed participants
descendant_counts descendant_weight_sum upvotes downvotes
user_notes_count user_discussions_count health_status current_user_todos
award_emoji events ancestors
award_emoji events ancestors color text_color
]
end
......
......@@ -147,7 +147,8 @@ RSpec.describe Gitlab::Graphql::Loaders::BulkEpicAggregateLoader do
issues_weight_sum: issues_weight_sum,
parent_id: epic.parent_id,
issues_state_id: issues_state,
epic_state_id: Epic.available_states[epic.state_id]
epic_state_id: Epic.available_states[epic.state_id],
color: ::EE::Epic::DEFAULT_COLOR
}.stringify_keys
end
end
......@@ -818,7 +818,8 @@ RSpec.describe Epic do
"issues_count" => 2,
"issues_state_id" => 1,
"issues_weight_sum" => 5,
"parent_id" => epic1.id
"parent_id" => epic1.id,
"color" => ::EE::Epic::DEFAULT_COLOR
}, {
"epic_state_id" => 2,
"id" => epic3.id,
......@@ -826,7 +827,8 @@ RSpec.describe Epic do
"issues_count" => 1,
"issues_state_id" => 2,
"issues_weight_sum" => 0,
"parent_id" => epic2.id
"parent_id" => epic2.id,
"color" => ::EE::Epic::DEFAULT_COLOR
}]
expect(result).to match_array(expected)
end
......@@ -842,4 +844,22 @@ RSpec.describe Epic do
create(:epic)
end
end
context 'with coloured epics' do
using RSpec::Parameterized::TableSyntax
where(:epic_color, :expected_text_color) do
::EE::Epic::DEFAULT_COLOR | '#FFFFFF'
'#FFFFFF' | '#333333'
'#000000' | '#FFFFFF'
end
with_them do
it 'returns correct text color' do
epic = build(:epic, color: epic_color)
expect(epic.text_color).to eq(expected_text_color)
end
end
end
end
......@@ -857,6 +857,7 @@ Epic:
- health_status
- external_key
- confidential
- color
EpicIssue:
- id
- relative_position
......
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