diff --git a/app/assets/javascripts/lib/utils/css_utils.js b/app/assets/javascripts/lib/utils/css_utils.js index 76ac442a470039987f7714695d9ba6267edda9ff..e4f68dd1b6cf39a5be30242082446822ccd8d05d 100644 --- a/app/assets/javascripts/lib/utils/css_utils.js +++ b/app/assets/javascripts/lib/utils/css_utils.js @@ -19,3 +19,7 @@ export function loadCSSFile(path) { } }); } + +export function getCssVariable(variable) { + return getComputedStyle(document.documentElement).getPropertyValue(variable).trim(); +} diff --git a/app/assets/javascripts/pages/profiles/preferences/show/index.js b/app/assets/javascripts/pages/profiles/preferences/show/index.js index 2922ff88721fe3417d8b6b1606655204144aef7b..769394346803aaa6aa87de2589f60ae2d132c81c 100644 --- a/app/assets/javascripts/pages/profiles/preferences/show/index.js +++ b/app/assets/javascripts/pages/profiles/preferences/show/index.js @@ -1,3 +1,5 @@ import initProfilePreferences from '~/profile/preferences/profile_preferences_bundle'; +import initProfilePreferencesDiffsColors from '~/profile/preferences/profile_preferences_diffs_colors'; initProfilePreferences(); +initProfilePreferencesDiffsColors(); diff --git a/app/assets/javascripts/profile/preferences/components/diffs_colors.vue b/app/assets/javascripts/profile/preferences/components/diffs_colors.vue new file mode 100644 index 0000000000000000000000000000000000000000..1992819ab82560ff607d9972981e2fe27e48354c --- /dev/null +++ b/app/assets/javascripts/profile/preferences/components/diffs_colors.vue @@ -0,0 +1,107 @@ + + diff --git a/app/assets/javascripts/profile/preferences/components/diffs_colors_preview.vue b/app/assets/javascripts/profile/preferences/components/diffs_colors_preview.vue new file mode 100644 index 0000000000000000000000000000000000000000..74dd2d5628a621ef52b3f251e03da69c5e2cf984 --- /dev/null +++ b/app/assets/javascripts/profile/preferences/components/diffs_colors_preview.vue @@ -0,0 +1,231 @@ + + diff --git a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue index 757a66ef1486d61406e35fb774f49888ccdfe706..7542f81a3610888339815a339450ba0eff964266 100644 --- a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue +++ b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue @@ -45,7 +45,7 @@ export default { return { isSubmitEnabled: true, darkModeOnCreate: null, - darkModeOnSubmit: null, + schemeOnCreate: null, }; }, computed: { @@ -61,6 +61,7 @@ export default { this.formEl.addEventListener('ajax:success', this.handleSuccess); this.formEl.addEventListener('ajax:error', this.handleError); this.darkModeOnCreate = this.darkModeSelected(); + this.schemeOnCreate = this.getSelectedScheme(); }, beforeDestroy() { this.formEl.removeEventListener('ajax:beforeSend', this.handleLoading); @@ -76,15 +77,19 @@ export default { const themeId = new FormData(this.formEl).get('user[theme_id]'); return this.applicationThemes[themeId] ?? null; }, + getSelectedScheme() { + return new FormData(this.formEl).get('user[color_scheme_id]'); + }, handleLoading() { this.isSubmitEnabled = false; - this.darkModeOnSubmit = this.darkModeSelected(); }, handleSuccess(customEvent) { // Reload the page if the theme has changed from light to dark mode or vice versa - // to correctly load all required styles. - const modeChanged = this.darkModeOnCreate ? !this.darkModeOnSubmit : this.darkModeOnSubmit; - if (modeChanged) { + // or if color scheme has changed to correctly load all required styles. + if ( + this.darkModeOnCreate !== this.darkModeSelected() || + this.schemeOnCreate !== this.getSelectedScheme() + ) { window.location.reload(); return; } diff --git a/app/assets/javascripts/profile/preferences/profile_preferences_diffs_colors.js b/app/assets/javascripts/profile/preferences/profile_preferences_diffs_colors.js new file mode 100644 index 0000000000000000000000000000000000000000..1b200187610bf5dea703aa73f7992bf6016ed8b3 --- /dev/null +++ b/app/assets/javascripts/profile/preferences/profile_preferences_diffs_colors.js @@ -0,0 +1,21 @@ +import Vue from 'vue'; +import DiffsColors from './components/diffs_colors.vue'; + +export default () => { + const el = document.querySelector('#js-profile-preferences-diffs-colors-app'); + + if (!el) return false; + + const { deletion, addition } = el.dataset; + + return new Vue({ + el, + provide: { + deletion, + addition, + }, + render(createElement) { + return createElement(DiffsColors); + }, + }); +}; diff --git a/app/assets/stylesheets/highlight/diff_custom_colors_addition.scss b/app/assets/stylesheets/highlight/diff_custom_colors_addition.scss new file mode 100644 index 0000000000000000000000000000000000000000..30895a557119a2f36c4369a2e7e6d2c4810c4575 --- /dev/null +++ b/app/assets/stylesheets/highlight/diff_custom_colors_addition.scss @@ -0,0 +1,36 @@ +/** +* CSS variables used below are declared in `app/views/layouts/_diffs_colors_css.haml` +*/ +.diff-custom-addition-color { + .code { + .line_holder { + .diff-line-num, + .line-coverage, + .line-codequality, + .line_content { + &.new { + &:not(.hll) { + background: var(--diff-addition-color); + } + + &.line_content span.idiff { + background: var(--diff-addition-color) !important; + } + + &::before, + a { + mix-blend-mode: luminosity; + } + } + } + } + + .gd { + background-color: var(--diff-addition-color); + } + } + + .idiff.addition { + background: var(--diff-addition-color) !important; + } +} diff --git a/app/assets/stylesheets/highlight/diff_custom_colors_deletion.scss b/app/assets/stylesheets/highlight/diff_custom_colors_deletion.scss new file mode 100644 index 0000000000000000000000000000000000000000..a8ab43909ebdb33eea3220e2aff423318618ee26 --- /dev/null +++ b/app/assets/stylesheets/highlight/diff_custom_colors_deletion.scss @@ -0,0 +1,36 @@ +/** +* CSS variables used below are declared in `app/views/layouts/_diffs_colors_css.haml` +*/ +.diff-custom-deletion-color { + .code { + .line_holder { + .diff-line-num, + .line-coverage, + .line-codequality, + .line_content { + &.old { + &:not(.hll) { + background: var(--diff-deletion-color); + } + + &.line_content span.idiff { + background: var(--diff-deletion-color) !important; + } + + &::before, + a { + mix-blend-mode: luminosity; + } + } + } + } + + .gd { + background-color: var(--diff-deletion-color); + } + } + + .idiff.deletion { + background: var(--diff-deletion-color) !important; + } +} diff --git a/app/assets/stylesheets/highlight/themes/dark.scss b/app/assets/stylesheets/highlight/themes/dark.scss index 09dcedff43c8f35b1d58bf8cb8dec62b3eb0272c..c51b1f04757baad86d6775403c52833f917f88f3 100644 --- a/app/assets/stylesheets/highlight/themes/dark.scss +++ b/app/assets/stylesheets/highlight/themes/dark.scss @@ -120,6 +120,8 @@ $dark-il: #de935f; --color-hljs-selector-id: #{$dark-nn}; --color-hljs-selector-attr: #{$dark-nt}; --color-hljs-selector-pseudo: #{$dark-nd}; + --default-diff-color-deletion: #ff3333; + --default-diff-color-addition: #288f2a; } .code.dark { diff --git a/app/assets/stylesheets/highlight/themes/monokai.scss b/app/assets/stylesheets/highlight/themes/monokai.scss index 6faf1cffdefc63daf0196c5826c449bf622f9c4a..226bb44f0e74c14c4e8133ebbc11010ae7b623db 100644 --- a/app/assets/stylesheets/highlight/themes/monokai.scss +++ b/app/assets/stylesheets/highlight/themes/monokai.scss @@ -89,6 +89,11 @@ $monokai-gd: #f92672; $monokai-gi: #a6e22e; $monokai-gh: #75715e; +:root { + --default-diff-color-deletion: #c87872; + --default-diff-color-addition: #678528; +} + .code.monokai { // Line numbers .file-line-num { diff --git a/app/assets/stylesheets/highlight/themes/none.scss b/app/assets/stylesheets/highlight/themes/none.scss index 9c28d9463dc451dc152fccfc71e71d759913f4af..7a36aba8be7a032c1c756836d7770df4c469dd17 100644 --- a/app/assets/stylesheets/highlight/themes/none.scss +++ b/app/assets/stylesheets/highlight/themes/none.scss @@ -9,6 +9,11 @@ background-color: $white-normal; } +:root { + --default-diff-color-deletion: #b4b4b4; + --default-diff-color-addition: #b4b4b4; +} + .code.none { // Line numbers .file-line-num { diff --git a/app/assets/stylesheets/highlight/themes/solarized-dark.scss b/app/assets/stylesheets/highlight/themes/solarized-dark.scss index c9f889c79fcf2bf5d72c0e21bce2ab3916344e1d..acd401e1694930df4baff17b75db9f599ffd0621 100644 --- a/app/assets/stylesheets/highlight/themes/solarized-dark.scss +++ b/app/assets/stylesheets/highlight/themes/solarized-dark.scss @@ -92,6 +92,11 @@ $solarized-dark-vg: #268bd2; $solarized-dark-vi: #268bd2; $solarized-dark-il: #2aa198; +:root { + --default-diff-color-deletion: #ff362c; + --default-diff-color-addition: #647e0e; +} + .code.solarized-dark { // Line numbers .file-line-num { diff --git a/app/assets/stylesheets/highlight/themes/solarized-light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss index 0108d7e496f00c6169638728e31b4ef3f5d3886b..ddcecc4cbcf0a6dcaa0d757bab7466955cee3b09 100644 --- a/app/assets/stylesheets/highlight/themes/solarized-light.scss +++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss @@ -94,6 +94,11 @@ $solarized-light-vg: #268bd2; $solarized-light-vi: #268bd2; $solarized-light-il: #2aa198; +:root { + --default-diff-color-deletion: #dc322f; + --default-diff-color-addition: #859900; +} + @mixin match-line { color: $black-transparent; background: $solarized-light-matchline-bg; diff --git a/app/assets/stylesheets/highlight/themes/white.scss b/app/assets/stylesheets/highlight/themes/white.scss index ed1d9c924c06d3e150fb7569611d08e5c63d7c87..8698e448c94b127d33646641b14983dbf52f0cff 100644 --- a/app/assets/stylesheets/highlight/themes/white.scss +++ b/app/assets/stylesheets/highlight/themes/white.scss @@ -3,3 +3,8 @@ @include conflict-colors('white'); } + +:root { + --default-diff-color-deletion: #eb919b; + --default-diff-color-addition: #a0f5b4; +} \ No newline at end of file diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss index 91d8f4a1ba55004ae83e67b27f789aac4f88f04a..20a36d2e8b1794d952d1c58390b8e014425864e7 100644 --- a/app/assets/stylesheets/highlight/white_base.scss +++ b/app/assets/stylesheets/highlight/white_base.scss @@ -149,7 +149,6 @@ pre.code, .diff-line-num { &.old { background-color: $line-number-old; - border-color: $line-removed-dark; a { color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%); @@ -158,7 +157,6 @@ pre.code, &.new { background-color: $line-number-new; - border-color: $line-added-dark; a { color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%); diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index adecb56ea38433c926e837ab4649aa57d2c55bcc..820b6520f6c58eab9ff223b2ce20cacb94ecda75 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -36,6 +36,8 @@ def preferences_params def preferences_param_names [ :color_scheme_id, + :diffs_deletion_color, + :diffs_addition_color, :layout, :dashboard, :project_view, diff --git a/app/helpers/colors_helper.rb b/app/helpers/colors_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..bc72122220a732dd31e4dee6e47d39cf43953e43 --- /dev/null +++ b/app/helpers/colors_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module ColorsHelper + HEX_COLOR_PATTERN = /\A\#(?:[0-9A-Fa-f]{3}){1,2}\Z/.freeze + + def hex_color_to_rgb_array(hex_color) + raise ArgumentError, "invalid hex color `#{hex_color}`" unless hex_color =~ HEX_COLOR_PATTERN + + hex_color.length == 7 ? hex_color[1, 7].scan(/.{2}/).map(&:hex) : hex_color[1, 4].scan(/./).map { |v| (v * 2).hex } + end + + def rgb_array_to_hex_color(rgb_array) + raise ArgumentError, "invalid RGB array `#{rgb_array}`" unless rgb_array_valid?(rgb_array) + + "##{rgb_array.map{ "%02x" % _1 }.join}" + end + + private + + def rgb_array_valid?(rgb_array) + rgb_array.is_a?(Array) && rgb_array.length == 3 && rgb_array.all?{ _1 >= 0 && _1 <= 255 } + end +end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 6a8c39b5b15153ff0cbe929864fe94ef526d06a4..39a57e786edcfecb7bd5186ae24a0e2bc6a08d21 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -82,6 +82,22 @@ def user_tab_width Gitlab::TabWidth.css_class_for_user(current_user) end + def user_diffs_colors + { + deletion: current_user&.diffs_deletion_color.presence, + addition: current_user&.diffs_addition_color.presence + }.compact + end + + def custom_diff_color_classes + return if request.path == profile_preferences_path + + classes = [] + classes << 'diff-custom-addition-color' if current_user&.diffs_addition_color.presence + classes << 'diff-custom-deletion-color' if current_user&.diffs_deletion_color.presence + classes + end + def language_choices options_for_select( selectable_locales_with_translation_level.sort, diff --git a/app/models/user.rb b/app/models/user.rb index 027ea70958c22878fcc2ff157b687eed336a3f60..743ba4d229c310446d681865e60038344051621b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -324,6 +324,8 @@ def update_tracked_fields!(request) :setup_for_company, :setup_for_company=, :render_whitespace_in_code, :render_whitespace_in_code=, :markdown_surround_selection, :markdown_surround_selection=, + :diffs_deletion_color, :diffs_deletion_color=, + :diffs_addition_color, :diffs_addition_color=, to: :user_preference delegate :path, to: :namespace, allow_nil: true, prefix: true diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 7687430cfd17d489178dd2d552acfb4cba6423ff..9b4c0a2527ad6cf97b3ef0318e6dcc2801c65f19 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -19,6 +19,9 @@ class UserPreference < ApplicationRecord greater_than_or_equal_to: Gitlab::TabWidth::MIN, less_than_or_equal_to: Gitlab::TabWidth::MAX } + validates :diffs_deletion_color, :diffs_addition_color, + format: { with: ColorsHelper::HEX_COLOR_PATTERN }, + allow_blank: true ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22' diff --git a/app/views/layouts/_diffs_colors_css.haml b/app/views/layouts/_diffs_colors_css.haml new file mode 100644 index 0000000000000000000000000000000000000000..d2efa392bd9447beb4746d2f76224c314678af08 --- /dev/null +++ b/app/views/layouts/_diffs_colors_css.haml @@ -0,0 +1,20 @@ +- deletion_color = local_assigns.fetch(:deletion, nil) +- addition_color = local_assigns.fetch(:addition, nil) + +- if deletion_color.present? || request.path == profile_preferences_path + = stylesheet_link_tag_defer "highlight/diff_custom_colors_deletion" +- if deletion_color.present? + - deletion_color_rgb = hex_color_to_rgb_array(deletion_color).join(',') + :css + :root { + --diff-deletion-color: rgba(#{deletion_color_rgb},0.2); + } + +- if addition_color.present? || request.path == profile_preferences_path + = stylesheet_link_tag_defer "highlight/diff_custom_colors_addition" +- if addition_color.present? + - addition_color_rgb = hex_color_to_rgb_array(addition_color).join(',') + :css + :root { + --diff-addition-color: rgba(#{addition_color_rgb},0.2); + } diff --git a/app/views/layouts/_startup_css.haml b/app/views/layouts/_startup_css.haml index 67c871b95f57985522ac4a11ac064656d7731b8c..64a86cf319ea651935fb288113d7f8111f24fe41 100644 --- a/app/views/layouts/_startup_css.haml +++ b/app/views/layouts/_startup_css.haml @@ -1,6 +1,9 @@ - startup_filename_default = user_application_theme == 'gl-dark' ? 'dark' : 'general' - startup_filename = local_assigns.fetch(:startup_filename, nil) || startup_filename_default +- diffs_colors = user_diffs_colors %style = Rails.application.assets_manifest.find_sources("themes/#{user_application_theme_css_filename}.css").first.to_s.html_safe if user_application_theme_css_filename = Rails.application.assets_manifest.find_sources("startup/startup-#{startup_filename}.css").first.to_s.html_safe + += render 'layouts/diffs_colors_css', diffs_colors if diffs_colors.present? || request.path == profile_preferences_path diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 26e3d9b3b9287b660d758439fbc24da85113537d..bdab5d7ea0763e564e1578da685443e87f310326 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,6 +1,6 @@ - page_classes = page_class << @html_class - page_classes = page_classes.flatten.compact -- body_classes = [user_application_theme, user_tab_width, @body_class, client_class_list] +- body_classes = [user_application_theme, user_tab_width, @body_class, client_class_list, *custom_diff_color_classes] !!! 5 %html{ lang: I18n.locale, class: page_classes } diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 48be2001c9c7436568af8c4d3e6a820c126b27bc..ddd3b4c2d1c35f62f3f490578bf3695e2886678d 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -44,6 +44,19 @@ .col-sm-12 %hr + .row.js-preferences-form.js-search-settings-section + .col-lg-4.profile-settings-sidebar#diffs-colors + %h4.gl-mt-0 + = s_('Preferences|Diff colors') + %p + = s_('Preferences|Customize the colors of removed and added lines in diffs.') + .col-lg-8 + .form-group + #js-profile-preferences-diffs-colors-app{ data: user_diffs_colors } + + .col-sm-12 + %hr + .row.js-preferences-form.js-search-settings-section .col-lg-4.profile-settings-sidebar#behavior %h4.gl-mt-0 diff --git a/config/application.rb b/config/application.rb index b07af18d9c9293e7aa5c9a29c8b4a0c0a3354760..0216c3e7b9f6b15a47241cbfa2d65ace53d18390 100644 --- a/config/application.rb +++ b/config/application.rb @@ -314,6 +314,8 @@ class Application < Rails::Application config.assets.precompile << "themes/*.css" config.assets.precompile << "highlight/themes/*.css" + config.assets.precompile << "highlight/diff_custom_colors_addition.css" + config.assets.precompile << "highlight/diff_custom_colors_deletion.css" # Import gitlab-svgs directly from vendored directory config.assets.paths << "#{config.root}/node_modules/@gitlab/svgs/dist" diff --git a/db/migrate/20220113164801_add_diffs_colors_to_user_preferences.rb b/db/migrate/20220113164801_add_diffs_colors_to_user_preferences.rb new file mode 100644 index 0000000000000000000000000000000000000000..00e8e574722b494ac8f64bcc5ed192d67a51bfe4 --- /dev/null +++ b/db/migrate/20220113164801_add_diffs_colors_to_user_preferences.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddDiffsColorsToUserPreferences < Gitlab::Database::Migration[1.0] + enable_lock_retries! + + # rubocop:disable Migration/AddLimitToTextColumns + # limit is added in 20220113164901_add_text_limit_to_user_preferences_diffs_colors.rb + def change + add_column :user_preferences, :diffs_deletion_color, :text + add_column :user_preferences, :diffs_addition_color, :text + end + # rubocop:enable Migration/AddLimitToTextColumns +end diff --git a/db/migrate/20220113164901_add_text_limit_to_user_preferences_diffs_colors.rb b/db/migrate/20220113164901_add_text_limit_to_user_preferences_diffs_colors.rb new file mode 100644 index 0000000000000000000000000000000000000000..9ae1c87519451d4936ed3a8c50dacf8c7c99d3f2 --- /dev/null +++ b/db/migrate/20220113164901_add_text_limit_to_user_preferences_diffs_colors.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddTextLimitToUserPreferencesDiffsColors < Gitlab::Database::Migration[1.0] + disable_ddl_transaction! + + def up + add_text_limit :user_preferences, :diffs_deletion_color, 7 + add_text_limit :user_preferences, :diffs_addition_color, 7 + end + + def down + remove_text_limit :user_preferences, :diffs_addition_color + remove_text_limit :user_preferences, :diffs_deletion_color + end +end diff --git a/db/schema_migrations/20220113164801 b/db/schema_migrations/20220113164801 new file mode 100644 index 0000000000000000000000000000000000000000..8354489ac3178b45a20bc12b63be755528a9c4ec --- /dev/null +++ b/db/schema_migrations/20220113164801 @@ -0,0 +1 @@ +71526ea198c64d23a35f06804f30068591e937df22d74c262fdec9ecf04bf7d4 \ No newline at end of file diff --git a/db/schema_migrations/20220113164901 b/db/schema_migrations/20220113164901 new file mode 100644 index 0000000000000000000000000000000000000000..977ec8bb51b30feebf0ee54356c5438d1c973297 --- /dev/null +++ b/db/schema_migrations/20220113164901 @@ -0,0 +1 @@ +b157cec5eab77665ae57f02647c39dc0fb167d78e1894b395c46f59d791ab3e0 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index a3d18a997dae219184562b0924cba591d4cd6fa1..781cd83d7435e138ccfcb298c4782c1fe720bf9c 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -21335,7 +21335,11 @@ CREATE TABLE user_preferences ( experience_level smallint, view_diffs_file_by_file boolean DEFAULT false NOT NULL, gitpod_enabled boolean DEFAULT false NOT NULL, - markdown_surround_selection boolean DEFAULT true NOT NULL + markdown_surround_selection boolean DEFAULT true NOT NULL, + diffs_deletion_color text, + diffs_addition_color text, + CONSTRAINT check_89bf269f41 CHECK ((char_length(diffs_deletion_color) <= 7)), + CONSTRAINT check_d07ccd35f7 CHECK ((char_length(diffs_addition_color) <= 7)) ); CREATE SEQUENCE user_preferences_id_seq diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md index 48160bb97ac224c505875dc5e9fbc5a89159be9d..ecd6e83efa183b2573a6402d6c7a5625f88190fa 100644 --- a/doc/user/profile/preferences.md +++ b/doc/user/profile/preferences.md @@ -89,6 +89,20 @@ The default syntax theme is White, and you can choose among 5 different themes: Introduced in GitLab 13.6, the themes [Solarized](https://gitlab.com/gitlab-org/gitlab/-/issues/221034) and [Monokai](https://gitlab.com/gitlab-org/gitlab/-/issues/221034) also apply to the [Web IDE](../project/web_ide/index.md) and [Snippets](../snippets.md). +## Diff colors + +A diff compares the old/removed content with the new/added content (e.g. when +[reviewing a merge request](../project/merge_requests/reviews/index.md#review-a-merge-request) or in a +[Markdown inline diff](../markdown.md#inline-diff)). +Typically, the colors red and green are used for removed and added lines in diffs. +The exact colors depend on the selected [syntax highlighting theme](#syntax-highlighting-theme). +The colors may lead to difficulties in case of red–green color blindness. + +For this reason, you can customize the following colors: + +- Color for removed lines +- Color for added lines + ## Behavior The following settings allow you to customize the behavior of the GitLab layout diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6c388b966fc234cbc6b8a194ea361267c2d5fb5c..160e0dcbd4917475de5ddac072b8b581f03e8ae2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -28363,6 +28363,12 @@ msgstr "" msgid "Preferences|Choose what content you want to see on your homepage." msgstr "" +msgid "Preferences|Color for added lines" +msgstr "" + +msgid "Preferences|Color for removed lines" +msgstr "" + msgid "Preferences|Configure how dates and times display for you." msgstr "" @@ -28372,6 +28378,12 @@ msgstr "" msgid "Preferences|Customize the appearance of the application header and navigation sidebar." msgstr "" +msgid "Preferences|Customize the colors of removed and added lines in diffs." +msgstr "" + +msgid "Preferences|Diff colors" +msgstr "" + msgid "Preferences|Display time in 24-hour format" msgstr "" @@ -28408,6 +28420,9 @@ msgstr "" msgid "Preferences|Navigation theme" msgstr "" +msgid "Preferences|Preview" +msgstr "" + msgid "Preferences|Project overview content" msgstr "" @@ -36507,6 +36522,12 @@ msgstr "" msgid "SuggestedColors|Crimson" msgstr "" +msgid "SuggestedColors|Current addition color" +msgstr "" + +msgid "SuggestedColors|Current removal color" +msgstr "" + msgid "SuggestedColors|Dark coral" msgstr "" @@ -36522,6 +36543,12 @@ msgstr "" msgid "SuggestedColors|Deep violet" msgstr "" +msgid "SuggestedColors|Default addition color" +msgstr "" + +msgid "SuggestedColors|Default removal color" +msgstr "" + msgid "SuggestedColors|Gray" msgstr "" @@ -36540,6 +36567,9 @@ msgstr "" msgid "SuggestedColors|Medium sea green" msgstr "" +msgid "SuggestedColors|Orange" +msgstr "" + msgid "SuggestedColors|Red" msgstr "" diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb index b7870a63f9dd54f2d5c467fca9d67065338fafeb..7add3a723377e8d4712c65a1564981cde5cac119 100644 --- a/spec/controllers/profiles/preferences_controller_spec.rb +++ b/spec/controllers/profiles/preferences_controller_spec.rb @@ -46,6 +46,8 @@ def go(params: {}, format: :json) it "changes the user's preferences" do prefs = { color_scheme_id: '1', + diffs_deletion_color: '#123456', + diffs_addition_color: '#abcdef', dashboard: 'stars', theme_id: '2', first_day_of_week: '1', @@ -84,5 +86,27 @@ def go(params: {}, format: :json) expect(response.parsed_body['type']).to eq('alert') end end + + context 'on invalid diffs colors setting' do + it 'responds with error for diffs_deletion_color' do + prefs = { diffs_deletion_color: '#1234567' } + + go params: prefs + + expect(response).to have_gitlab_http_status(:bad_request) + expect(response.parsed_body['message']).to eq _('Failed to save preferences.') + expect(response.parsed_body['type']).to eq('alert') + end + + it 'responds with error for diffs_addition_color' do + prefs = { diffs_addition_color: '#1234567' } + + go params: prefs + + expect(response).to have_gitlab_http_status(:bad_request) + expect(response.parsed_body['message']).to eq _('Failed to save preferences.') + expect(response.parsed_body['type']).to eq('alert') + end + end end end diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb index e19e29bf63a9cc17c0dba358264aac5d69129eae..4c61e8d45e43ced7648cb9fef4efa58db8dfdd6f 100644 --- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb +++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb @@ -18,14 +18,6 @@ end describe 'User changes their syntax highlighting theme', :js do - it 'creates a flash message' do - choose 'user_color_scheme_id_5' - - wait_for_requests - - expect_preferences_saved_message - end - it 'updates their preference' do choose 'user_color_scheme_id_5' diff --git a/spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap b/spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..3025a2f87ae2e17b4db2df9cbc57e1b743617a1c --- /dev/null +++ b/spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap @@ -0,0 +1,915 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DiffsColorsPreview component renders diff colors preview 1`] = ` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + # + + Removed + + content + + + + + + + + # + + Added + + content + + +
+ + + + + v + + + + = + + + + 1 + + + + + + + + v + + + + = + + + + 1 + + +
+ + + + + s + + + + = + + + + "string" + + + + + + + + s + + + + = + + + + "string" + + +
+ + + + + + + +
+ + + + + for + + + + i + + + + in + + + + range + + + ( + + + - + + + 10 + + + , + + + + 10 + + + ): + + + + + + + + for + + + + i + + + + in + + + + range + + + ( + + + - + + + 10 + + + , + + + + 10 + + + ): + + +
+ + + + + + + + + print + + + ( + + + i + + + + + + + + + 1 + + + ) + + + + + + + + + + + + print + + + ( + + + i + + + + + + + + + 1 + + + ) + + +
+ + + + + + + +
+ + + + + class + + + + LinkedList + + + ( + + + object + + + ): + + + + + + + + class + + + + LinkedList + + + ( + + + object + + + ): + + +
+ + + + + + + + + def + + + + __init__ + + + ( + + + self + + + , + + + + x + + + ): + + + + + + + + + + + + def + + + + __init__ + + + ( + + + self + + + , + + + + x + + + ): + + +
+ + + + + + + + + self + + + . + + + val + + + + = + + + + x + + + + + + + + + + + + self + + + . + + + val + + + + = + + + + x + + +
+ + + + + + + + + self + + + . + + + next + + + + = + + + + None + + + + + + + + + + + + self + + + . + + + next + + + + = + + + + None + + +
+
+`; diff --git a/spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js b/spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..e60602ab33607e7468ad0c919bf398bfad6abb7d --- /dev/null +++ b/spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js @@ -0,0 +1,23 @@ +import { shallowMount } from '@vue/test-utils'; +import DiffsColorsPreview from '~/profile/preferences/components/diffs_colors_preview.vue'; + +describe('DiffsColorsPreview component', () => { + let wrapper; + + function createComponent() { + wrapper = shallowMount(DiffsColorsPreview); + } + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('renders diff colors preview', () => { + expect(wrapper.element).toMatchSnapshot(); + }); +}); diff --git a/spec/frontend/profile/preferences/components/diffs_colors_spec.js b/spec/frontend/profile/preferences/components/diffs_colors_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..02f501a0b068294abe61301991626fa1b7ed9e63 --- /dev/null +++ b/spec/frontend/profile/preferences/components/diffs_colors_spec.js @@ -0,0 +1,153 @@ +import { shallowMount } from '@vue/test-utils'; +import { s__ } from '~/locale'; +import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue'; +import DiffsColors from '~/profile/preferences/components/diffs_colors.vue'; +import DiffsColorsPreview from '~/profile/preferences/components/diffs_colors_preview.vue'; +import * as CssUtils from '~/lib/utils/css_utils'; + +describe('DiffsColors component', () => { + let wrapper; + + const defaultInjectedProps = { + addition: '#00ff00', + deletion: '#ff0000', + }; + + const initialSuggestedColors = { + '#d99530': s__('SuggestedColors|Orange'), + '#1f75cb': s__('SuggestedColors|Blue'), + }; + + const findColorPickers = () => wrapper.findAllComponents(ColorPicker); + + function createComponent(provide = {}) { + wrapper = shallowMount(DiffsColors, { + provide: { + ...defaultInjectedProps, + ...provide, + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('mounts', () => { + createComponent(); + + expect(wrapper.exists()).toBe(true); + }); + + describe('preview', () => { + it('should render preview', () => { + createComponent(); + + expect(wrapper.findComponent(DiffsColorsPreview).exists()).toBe(true); + }); + + it('should set preview classes', () => { + createComponent(); + + expect(wrapper.attributes('class')).toBe( + 'diff-custom-addition-color diff-custom-deletion-color', + ); + }); + + it.each([ + [{ addition: null }, 'diff-custom-deletion-color'], + [{ deletion: null }, 'diff-custom-addition-color'], + ])('should not set preview class if color not set', (provide, expectedClass) => { + createComponent(provide); + + expect(wrapper.attributes('class')).toBe(expectedClass); + }); + + it.each([ + [{}, '--diff-deletion-color: rgba(255,0,0,0.2); --diff-addition-color: rgba(0,255,0,0.2);'], + [{ addition: null }, '--diff-deletion-color: rgba(255,0,0,0.2);'], + [{ deletion: null }, '--diff-addition-color: rgba(0,255,0,0.2);'], + ])('should set correct CSS variables', (provide, expectedStyle) => { + createComponent(provide); + + expect(wrapper.attributes('style')).toBe(expectedStyle); + }); + }); + + describe('color pickers', () => { + it('should render both color pickers', () => { + createComponent(); + + const colorPickers = findColorPickers(); + + expect(colorPickers.length).toBe(2); + expect(colorPickers.at(0).props()).toMatchObject({ + label: s__('Preferences|Color for removed lines'), + value: '#ff0000', + state: true, + }); + expect(colorPickers.at(1).props()).toMatchObject({ + label: s__('Preferences|Color for added lines'), + value: '#00ff00', + state: true, + }); + }); + + describe('suggested colors', () => { + const suggestedColors = () => findColorPickers().at(0).props('suggestedColors'); + + it('contains initial suggested colors', () => { + createComponent(); + + expect(suggestedColors()).toMatchObject(initialSuggestedColors); + }); + + it('contains default diff colors of theme', () => { + jest.spyOn(CssUtils, 'getCssVariable').mockImplementation((variable) => { + if (variable === '--default-diff-color-addition') return '#111111'; + if (variable === '--default-diff-color-deletion') return '#222222'; + return '#000000'; + }); + + createComponent(); + + expect(suggestedColors()).toMatchObject({ + '#111111': s__('SuggestedColors|Default addition color'), + '#222222': s__('SuggestedColors|Default removal color'), + }); + }); + + it('contains current diff colors if set', () => { + createComponent(); + + expect(suggestedColors()).toMatchObject({ + [defaultInjectedProps.addition]: s__('SuggestedColors|Current addition color'), + [defaultInjectedProps.deletion]: s__('SuggestedColors|Current removal color'), + }); + }); + + it.each([ + [ + { addition: null }, + s__('SuggestedColors|Current removal color'), + s__('SuggestedColors|Current addition color'), + ], + [ + { deletion: null }, + s__('SuggestedColors|Current addition color'), + s__('SuggestedColors|Current removal color'), + ], + ])( + 'does not contain current diff color if not set %p', + (provide, expectedToContain, expectNotToContain) => { + createComponent(provide); + + const suggestedColorsLabels = Object.values(suggestedColors()); + expect(suggestedColorsLabels).toContain(expectedToContain); + expect(suggestedColorsLabels).not.toContain(expectNotToContain); + }, + ); + }); + }); +}); diff --git a/spec/helpers/colors_helper_spec.rb b/spec/helpers/colors_helper_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ca5cafb7ebe088e8a831956c801d99e511434255 --- /dev/null +++ b/spec/helpers/colors_helper_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ColorsHelper do + using RSpec::Parameterized::TableSyntax + + describe '#hex_color_to_rgb_array' do + context 'valid hex color' do + where(:hex_color, :rgb_array) do + '#000000' | [0, 0, 0] + '#aaaaaa' | [170, 170, 170] + '#cCcCcC' | [204, 204, 204] + '#FFFFFF' | [255, 255, 255] + '#000abc' | [0, 10, 188] + '#123456' | [18, 52, 86] + '#a1b2c3' | [161, 178, 195] + '#000' | [0, 0, 0] + '#abc' | [170, 187, 204] + '#321' | [51, 34, 17] + '#7E2' | [119, 238, 34] + '#fFf' | [255, 255, 255] + end + + with_them do + it 'returns correct RGB array' do + expect(helper.hex_color_to_rgb_array(hex_color)).to eq(rgb_array) + end + end + end + + context 'invalid hex color' do + where(:hex_color) { ['', '0', '#00', '#ffff', '#1234567', 'invalid', [], 1, nil] } + + with_them do + it 'raise ArgumentError' do + expect { helper.hex_color_to_rgb_array(hex_color) }.to raise_error(ArgumentError) + end + end + end + end + + describe '#rgb_array_to_hex_color' do + context 'valid RGB array' do + where(:rgb_array, :hex_color) do + [0, 0, 0] | '#000000' + [0, 0, 255] | '#0000ff' + [0, 255, 0] | '#00ff00' + [255, 0, 0] | '#ff0000' + [12, 34, 56] | '#0c2238' + [222, 111, 88] | '#de6f58' + [255, 255, 255] | '#ffffff' + end + + with_them do + it 'returns correct hex color' do + expect(helper.rgb_array_to_hex_color(rgb_array)).to eq(hex_color) + end + end + end + + context 'invalid RGB array' do + where(:rgb_array) do + [ + '', + '#000000', + 0, + nil, + [], + [0], + [0, 0], + [0, 0, 0, 0], + [-1, 0, 0], + [0, -1, 0], + [0, 0, -1], + [256, 0, 0], + [0, 256, 0], + [0, 0, 256] + ] + end + + with_them do + it 'raise ArgumentError' do + expect { helper.rgb_array_to_hex_color(rgb_array) }.to raise_error(ArgumentError) + end + end + end + end +end diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index 8c13afc2b45c0533619784b5b41cef4ba86bb740..01235c7bb51d38ecaf3e95ec7960599aaba40597 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -145,6 +145,67 @@ end end + describe '#user_diffs_colors' do + context 'with a user' do + it "returns user's diffs colors" do + stub_user(diffs_addition_color: '#123456', diffs_deletion_color: '#abcdef') + + expect(helper.user_diffs_colors).to eq({ addition: '#123456', deletion: '#abcdef' }) + end + + it 'omits property if nil' do + stub_user(diffs_addition_color: '#123456', diffs_deletion_color: nil) + + expect(helper.user_diffs_colors).to eq({ addition: '#123456' }) + end + + it 'omits property if blank' do + stub_user(diffs_addition_color: '', diffs_deletion_color: '#abcdef') + + expect(helper.user_diffs_colors).to eq({ deletion: '#abcdef' }) + end + end + + context 'without a user' do + it 'returns no properties' do + stub_user + + expect(helper.user_diffs_colors).to eq({}) + end + end + end + + describe '#custom_diff_color_classes' do + context 'with a user' do + it 'returns color classes' do + stub_user(diffs_addition_color: '#123456', diffs_deletion_color: '#abcdef') + + expect(helper.custom_diff_color_classes) + .to match_array(%w[diff-custom-addition-color diff-custom-deletion-color]) + end + + it 'omits property if nil' do + stub_user(diffs_addition_color: '#123456', diffs_deletion_color: nil) + + expect(helper.custom_diff_color_classes).to match_array(['diff-custom-addition-color']) + end + + it 'omits property if blank' do + stub_user(diffs_addition_color: '', diffs_deletion_color: '#abcdef') + + expect(helper.custom_diff_color_classes).to match_array(['diff-custom-deletion-color']) + end + end + + context 'without a user' do + it 'returns no classes' do + stub_user + + expect(helper.custom_diff_color_classes).to match_array([]) + end + end + end + describe '#language_choices' do include StubLanguagesTranslationPercentage diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb index d4491aacd9fc3cf06eca759a2451d1902258a42d..2492521c6346db075ef23c5a2f5fc049db58b437 100644 --- a/spec/models/user_preference_spec.rb +++ b/spec/models/user_preference_spec.rb @@ -5,6 +5,48 @@ RSpec.describe UserPreference do let(:user_preference) { create(:user_preference) } + describe 'validations' do + describe 'diffs_deletion_color and diffs_addition_color' do + using RSpec::Parameterized::TableSyntax + + where(color: [ + '#000000', + '#123456', + '#abcdef', + '#AbCdEf', + '#ffffff', + '#fFfFfF', + '#000', + '#123', + '#abc', + '#AbC', + '#fff', + '#fFf', + '' + ]) + + with_them do + it { is_expected.to allow_value(color).for(:diffs_deletion_color) } + it { is_expected.to allow_value(color).for(:diffs_addition_color) } + end + + where(color: [ + '#1', + '#12', + '#1234', + '#12345', + '#1234567', + '123456', + '#12345x' + ]) + + with_them do + it { is_expected.not_to allow_value(color).for(:diffs_deletion_color) } + it { is_expected.not_to allow_value(color).for(:diffs_addition_color) } + end + end + end + describe 'notes filters global keys' do it 'contains expected values' do expect(UserPreference::NOTES_FILTERS.keys).to match_array([:all_notes, :only_comments, :only_activity]) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index d8267c7db994fff763b97dd47d110cfd81f860e7..05e38efea80f26610e357115bd369a24ccb5613b 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -69,6 +69,12 @@ it { is_expected.to delegate_method(:markdown_surround_selection).to(:user_preference) } it { is_expected.to delegate_method(:markdown_surround_selection=).to(:user_preference).with_arguments(:args) } + it { is_expected.to delegate_method(:diffs_deletion_color).to(:user_preference) } + it { is_expected.to delegate_method(:diffs_deletion_color=).to(:user_preference).with_arguments(:args) } + + it { is_expected.to delegate_method(:diffs_addition_color).to(:user_preference) } + it { is_expected.to delegate_method(:diffs_addition_color=).to(:user_preference).with_arguments(:args) } + it { is_expected.to delegate_method(:job_title).to(:user_detail).allow_nil } it { is_expected.to delegate_method(:job_title=).to(:user_detail).with_arguments(:args).allow_nil }