diff --git a/app/components/pajamas/banner_component.html.haml b/app/components/pajamas/banner_component.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..4fa2ed09cd359685e5ad0a653568de1c13180fb1 --- /dev/null +++ b/app/components/pajamas/banner_component.html.haml @@ -0,0 +1,23 @@ +%section.gl-banner{ @banner_options, class: banner_class } + - if illustration? + .gl-banner-illustration + = illustration + - elsif @svg_path.present? + .gl-banner-illustration + = image_tag @svg_path, alt: "" + + .gl-banner-content + %h1.gl-banner-title= title + + = content + + - if primary_action? + = primary_action + - else + = link_to @button_text, @button_link, { **@button_options, class: 'btn btn-md btn-confirm gl-button js-close-callout' } + + - actions.each do |action| + = action + + %button.gl-button.gl-banner-close.btn-sm.btn-icon.js-close{ @close_options, class: close_class, type: 'button' } + = sprite_icon('close', size: 16, css_class: 'dismiss-icon') diff --git a/app/components/pajamas/banner_component.rb b/app/components/pajamas/banner_component.rb new file mode 100644 index 0000000000000000000000000000000000000000..9b6343b47c90b4d9838fcd95be774914a39d7607 --- /dev/null +++ b/app/components/pajamas/banner_component.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Pajamas + class BannerComponent < Pajamas::Component + # @param [String] button_text + # @param [String] button_link + # @param [Boolean] embedded + # @param [Symbol] variant + # @param [String] svg_path + # @param [Hash] banner_options + # @param [Hash] button_options + # @param [Hash] close_options + def initialize( + button_text: 'OK', + button_link: '#', + embedded: false, + variant: :promotion, + svg_path: nil, + banner_options: {}, + button_options: {}, + close_options: {} + ) + @button_text = button_text + @button_link = button_link + @embedded = embedded + @variant = variant.to_sym + @svg_path = svg_path.to_s + @banner_options = banner_options + @button_options = button_options + @close_options = close_options + end + + private + + def banner_class + classes = [] + classes.push('gl-border-none') if @embedded + classes.push('gl-banner-introduction') if introduction? + classes.join(' ') + end + + def close_class + if introduction? + 'btn-confirm btn-confirm-tertiary' + else + 'btn-default btn-default-tertiary' + end + end + + delegate :sprite_icon, to: :helpers + + renders_one :title + renders_one :illustration + renders_one :primary_action + renders_many :actions + + def introduction? + @variant == :introduction + end + end +end diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml index d6d84b2181fccc4bf6b26c079745630a73413bb5..c2b941c6106d720a05a146eb3fb77437f8ce6538 100644 --- a/app/views/shared/_auto_devops_callout.html.haml +++ b/app/views/shared/_auto_devops_callout.html.haml @@ -1,15 +1,13 @@ -%section.js-autodevops-banner.gl-banner{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } } - .gl-banner-illustration - = image_tag('illustrations/autodevops.svg') += render Pajamas::BannerComponent.new(button_text: s_('AutoDevOps|Enable in settings'), + button_link: project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), + svg_path: 'illustrations/autodevops.svg', + banner_options: { class: 'js-autodevops-banner', data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }, + close_options: { 'aria-label' => s_('AutoDevOps|Dismiss Auto DevOps box'), class: 'js-close-callout' }) do |c| + - c.title do + = s_('AutoDevOps|Auto DevOps') - .gl-banner-content - %h1.gl-banner-title= s_('AutoDevOps|Auto DevOps') - %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') - %p - - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer') - = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link } - = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), class: 'btn btn-md btn-default gl-button js-close-callout' + %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') - %button.gl-banner-close.close.js-close-callout{ type: 'button', - 'aria-label' => s_('AutoDevOps|Dismiss Auto DevOps box') } - = sprite_icon('close', size: 16, css_class: 'dismiss-icon') + %p + - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer') + = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link } diff --git a/ee/app/views/shared/promotions/_promote_burndown_charts.html.haml b/ee/app/views/shared/promotions/_promote_burndown_charts.html.haml index 0cf10f20ad534a6d64a577f31cd153afc9ee6e12..9b7e67adf1b7b4965a68328ee4ee90ea05e88c89 100644 --- a/ee/app/views/shared/promotions/_promote_burndown_charts.html.haml +++ b/ee/app/views/shared/promotions/_promote_burndown_charts.html.haml @@ -1,20 +1,20 @@ - callout_id = 'promote_burndown_charts_dismissed' - if show_burndown_charts_promotion?(milestone) && show_callout?(callout_id) - .user-callout.promotion-callout#promote_burndown_charts{ data: { uid: callout_id } } - .bordered-box.content-block.promotion-burndown-charts-content - %button.gl-button.btn.btn-default.close.js-close-callout{ type: 'button', 'aria-label' => s_('Promotions|Dismiss burndown charts promotion') } - = sprite_icon('close', size: 16, css_class: 'dismiss-icon') - .svg-container - = custom_icon('icon_burndown_charts') - .user-callout-copy - %h4 - - if Gitlab::CurrentSettings.should_check_namespace_plan? - = s_('Promotions|Upgrade your plan to improve milestones with Burndown Charts.') - - else - = s_('Promotions|Improve milestones with Burndown Charts.') - %p - = s_('Promotions|Burndown Charts are visual representations of the progress of completing a milestone. At a glance, you see the current state for the completion a given milestone. Without them, you would have to organize the data from the milestone and plot it yourself to have the same sense of progress.') - = link_to _('Read more'), help_page_path('user/project/milestones/burndown_and_burnup_charts.md'), target: '_blank', rel: 'noopener noreferrer' + = render Pajamas::BannerComponent.new(banner_options: { id: 'promote_burndown_charts', class: 'user-callout', data: { uid: callout_id } }, + close_options: { 'aria-label' => s_('Promotions|Dismiss burndown charts promotion'), class: 'js-close-callout' }) do |c| + - c.title do + - if Gitlab::CurrentSettings.should_check_namespace_plan? + = s_('Promotions|Upgrade your plan to improve milestones with Burndown Charts.') + - else + = s_('Promotions|Improve milestones with Burndown Charts.') - = render 'shared/promotions/promotion_link_project', location: :burndown_charts + - c.illustration do + = custom_icon('icon_burndown_charts') + + %p + = s_('Promotions|Burndown Charts are visual representations of the progress of completing a milestone. At a glance, you see the current state for the completion a given milestone. Without them, you would have to organize the data from the milestone and plot it yourself to have the same sense of progress.') + = link_to _('Read more'), help_page_path('user/project/milestones/burndown_and_burnup_charts.md'), target: '_blank', rel: 'noopener noreferrer' + + - c.primary_action do + = render 'shared/promotions/promotion_link_project', location: :burndown_charts diff --git a/ee/spec/features/promotion_spec.rb b/ee/spec/features/promotion_spec.rb index eb37937e1a42427058e5bc7744c60411c81867e6..571f9f10cd606bfd66d14c697e0780731fe80495 100644 --- a/ee/spec/features/promotion_spec.rb +++ b/ee/spec/features/promotion_spec.rb @@ -89,7 +89,7 @@ visit project_milestone_path(project, milestone) within('#promote_burndown_charts') do - find('.close').click + find('.js-close').click end visit project_milestone_path(project, milestone) diff --git a/spec/components/pajamas/banner_component_spec.rb b/spec/components/pajamas/banner_component_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5969f06dbadad869fe4a6f4455a4a2833d5242fc --- /dev/null +++ b/spec/components/pajamas/banner_component_spec.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe Pajamas::BannerComponent, type: :component do + subject do + described_class.new(**options) + end + + let(:title) { "Banner title" } + let(:content) { "Banner content"} + let(:options) { {} } + + describe 'basic usage' do + before do + render_inline(subject) do |c| + c.title { title } + content + end + end + + it 'renders its content' do + expect(rendered_component).to have_text content + end + + it 'renders its title' do + expect(rendered_component).to have_css "h1[class='gl-banner-title']", text: title + end + + it 'renders a close button' do + expect(rendered_component).to have_css "button.gl-banner-close" + end + + describe 'button_text and button_link' do + let(:options) { { button_text: 'Learn more', button_link: '/learn-more' } } + + it 'define the primary action' do + expect(rendered_component).to have_css "a.btn-confirm.gl-button[href='/learn-more']", text: 'Learn more' + end + end + + describe 'banner_options' do + let(:options) { { banner_options: { class: "baz", data: { foo: "bar" } } } } + + it 'are on the banner' do + expect(rendered_component).to have_css ".gl-banner.baz[data-foo='bar']" + end + + context 'with custom classes' do + let(:options) { { variant: :introduction, banner_options: { class: 'extra special' } } } + + it 'don\'t conflict with internal banner_classes' do + expect(rendered_component).to have_css '.extra.special.gl-banner-introduction.gl-banner' + end + end + end + + describe 'close_options' do + let(:options) { { close_options: { class: "js-foo", data: { uid: "123" } } } } + + it 'are on the close button' do + expect(rendered_component).to have_css "button.gl-banner-close.js-foo[data-uid='123']" + end + end + + describe 'embedded' do + context 'by default (false)' do + it 'keeps the banner\'s borders' do + expect(rendered_component).not_to have_css ".gl-banner.gl-border-none" + end + end + + context 'when set to true' do + let(:options) { { embedded: true } } + + it 'removes the banner\'s borders' do + expect(rendered_component).to have_css ".gl-banner.gl-border-none" + end + end + end + + describe 'variant' do + context 'by default (promotion)' do + it 'applies no variant class' do + expect(rendered_component).to have_css "[class='gl-banner']" + end + end + + context 'when set to introduction' do + let(:options) { { variant: :introduction } } + + it "applies the introduction class to the banner" do + expect(rendered_component).to have_css ".gl-banner.gl-banner-introduction" + end + + it "applies the confirm class to the close button" do + expect(rendered_component).to have_css ".gl-banner-close.btn-confirm.btn-confirm-tertiary" + end + end + + context 'when set to unknown variant' do + let(:options) { { variant: :foobar } } + + it 'ignores the unknown variant' do + expect(rendered_component).to have_css "[class='gl-banner']" + end + end + end + + describe 'illustration' do + it 'has none by default' do + expect(rendered_component).not_to have_css ".gl-banner-illustration" + end + + context 'with svg_path' do + let(:options) { { svg_path: 'logo.svg' } } + + it 'renders an image as illustration' do + expect(rendered_component).to have_css ".gl-banner-illustration img" + end + end + end + end + + context 'with illustration slot' do + before do + render_inline(subject) do |c| + c.title { title } + c.illustration { "<svg></svg>".html_safe } + content + end + end + + it 'renders the slot content as illustration' do + expect(rendered_component).to have_css ".gl-banner-illustration svg" + end + + context 'and conflicting svg_path' do + let(:options) { { svg_path: 'logo.svg' } } + + it 'uses the slot content' do + expect(rendered_component).to have_css ".gl-banner-illustration svg" + expect(rendered_component).not_to have_css ".gl-banner-illustration img" + end + end + end + + context 'with primary_action slot' do + before do + render_inline(subject) do |c| + c.title { title } + c.primary_action { "<a class='special' href='#'>Special</a>".html_safe } + content + end + end + + it 'renders the slot content as the primary action' do + expect(rendered_component).to have_css "a.special", text: 'Special' + end + + context 'and conflicting button_text and button_link' do + let(:options) { { button_text: 'Not special', button_link: '/' } } + + it 'uses the slot content' do + expect(rendered_component).to have_css "a.special[href='#']", text: 'Special' + expect(rendered_component).not_to have_css "a.btn[href='/']" + end + end + end +end