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`] = `
+
+`;
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 }