Commit 3899f83e authored by Gabriel Mazetto's avatar Gabriel Mazetto Committed by Doug Stull

Refactor `_gl_toggle` partial to ViewComponent

- introduce ViewComponent
parent 94d3458f
...@@ -46,6 +46,10 @@ AllCops: ...@@ -46,6 +46,10 @@ AllCops:
CacheRootDirectory: <%= Dir.getwd %>/tmp CacheRootDirectory: <%= Dir.getwd %>/tmp
MaxFilesInCache: 30000 MaxFilesInCache: 30000
Metrics/ParameterLists:
Exclude:
- 'app/components/**/*'
Cop/AvoidKeywordArgumentsInSidekiqWorkers: Cop/AvoidKeywordArgumentsInSidekiqWorkers:
Enabled: true Enabled: true
Include: Include:
......
...@@ -11,6 +11,8 @@ gem 'responders', '~> 3.0' ...@@ -11,6 +11,8 @@ gem 'responders', '~> 3.0'
gem 'sprockets', '~> 3.7.0' gem 'sprockets', '~> 3.7.0'
gem 'view_component', '~> 2.50.0'
# Default values for AR models # Default values for AR models
gem 'default_value_for', '~> 3.4.0' gem 'default_value_for', '~> 3.4.0'
......
...@@ -645,7 +645,7 @@ GEM ...@@ -645,7 +645,7 @@ GEM
mime-types (~> 3.0) mime-types (~> 3.0)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.9.1) i18n (1.10.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
i18n_data (0.8.0) i18n_data (0.8.0)
icalendar (2.4.1) icalendar (2.4.1)
...@@ -1362,6 +1362,9 @@ GEM ...@@ -1362,6 +1362,9 @@ GEM
activerecord (>= 3.0) activerecord (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
version_sorter (2.2.4) version_sorter (2.2.4)
view_component (2.50.0)
activesupport (>= 5.0.0, < 8.0)
method_source (~> 1.0)
vmstat (2.3.0) vmstat (2.3.0)
warden (1.2.8) warden (1.2.8)
rack (>= 2.0.6) rack (>= 2.0.6)
...@@ -1682,6 +1685,7 @@ DEPENDENCIES ...@@ -1682,6 +1685,7 @@ DEPENDENCIES
valid_email (~> 0.1) valid_email (~> 0.1)
validates_hostname (~> 1.0.11) validates_hostname (~> 1.0.11)
version_sorter (~> 2.2.4) version_sorter (~> 2.2.4)
view_component (~> 2.50.0)
vmstat (~> 2.3.0) vmstat (~> 2.3.0)
warning (~> 1.2.0) warning (~> 1.2.0)
webauthn (~> 2.3) webauthn (~> 2.3)
......
# frozen_string_literal: true
module Pajamas
class Component < ViewComponent::Base
private
# :nocov:
# Filter a given a value against a list of allowed values
# If no value is given or value is not allowed return default one
#
# @param [Object] value
# @param [Enumerable] allowed_values
# @param [Object] default
def filter_attribute(value, allowed_values, default: nil)
return default unless value
return value if allowed_values.include?(value)
default
end
# :nocov:
end
end
%span{ class: @classes,
data: { name: @name,
id: @id,
is_checked: @is_checked.to_s,
disabled: @is_disabled.to_s,
is_loading: @is_loading.to_s,
label: @label,
help: @help,
label_position: @label_position,
**@data } }
-# Leverage this block to render a rich help text. To render a plain text help text,
-# prefer the `help` parameter.
- if content.present?
.gl-text-secondary.gl-mt-1
= content
# frozen_string_literal: true
# Renders a GlToggle root element
# To actually initialize the component, make sure to call the initToggle helper from ~/toggles.
class Pajamas::ToggleComponent < Pajamas::Component
LABEL_POSITION_OPTIONS = [:top, :left, :hidden].freeze
# @param [String] classes
# @param [String] label
# @param [Symbol] label_position :top, :left or :hidden
# @param [String] id
# @param [String] name
# @param [String] help
# @param [Hash] data
# @param [Boolean] is_disabled
# @param [Boolean] is_checked
# @param [Boolean] is_loading
def initialize(
classes:, label: nil, label_position: nil,
id: nil, name: nil, help: nil, data: {},
is_disabled: false, is_checked: false, is_loading: false)
@id = id
@name = name
@classes = classes
@label = label
@label_position = filter_attribute(label_position, LABEL_POSITION_OPTIONS)
@help = help
@data = data
@is_disabled = is_disabled
@is_checked = is_checked
@is_loading = is_loading
end
end
...@@ -14,12 +14,11 @@ ...@@ -14,12 +14,11 @@
- lets_encrypt_link_start = "<a href=\"%{lets_encrypt_link_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-nowrap\">".html_safe % { lets_encrypt_link_url: lets_encrypt_link_url } - lets_encrypt_link_start = "<a href=\"%{lets_encrypt_link_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-nowrap\">".html_safe % { lets_encrypt_link_url: lets_encrypt_link_url }
- lets_encrypt_link_end = "</a>".html_safe - lets_encrypt_link_end = "</a>".html_safe
= _("Automatic certificate management using %{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end}").html_safe % { lets_encrypt_link_start: lets_encrypt_link_start, lets_encrypt_link_end: lets_encrypt_link_end } = _("Automatic certificate management using %{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end}").html_safe % { lets_encrypt_link_start: lets_encrypt_link_start, lets_encrypt_link_end: lets_encrypt_link_end }
= render "shared/gl_toggle", = render Pajamas::ToggleComponent.new(id: 'pages_domain_auto_ssl_enabled_button',
id: "pages_domain_auto_ssl_enabled_button", classes: 'js-project-feature-toggle js-enable-ssl-gl-toggle mt-2',
is_checked: auto_ssl_available_and_enabled, is_checked: auto_ssl_available_and_enabled,
classes: "js-project-feature-toggle js-enable-ssl-gl-toggle mt-2", label: _("Automatic certificate management using Let's Encrypt"),
label: _("Automatic certificate management using Let's Encrypt"), label_position: :hidden)
label_position: 'hidden'
= f.hidden_field :auto_ssl_enabled?, class: "js-project-feature-toggle-input" = f.hidden_field :auto_ssl_enabled?, class: "js-project-feature-toggle-input"
%p.gl-text-secondary.gl-mt-1 %p.gl-text-secondary.gl-mt-1
- docs_link_url = help_page_path("user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md") - docs_link_url = help_page_path("user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md")
......
...@@ -24,10 +24,9 @@ ...@@ -24,10 +24,9 @@
.form-group.row .form-group.row
= f.label :allow_force_push, s_("ProtectedBranch|Allowed to force push:"), class: 'col-md-2 gl-text-left text-md-right' = f.label :allow_force_push, s_("ProtectedBranch|Allowed to force push:"), class: 'col-md-2 gl-text-left text-md-right'
.col-md-10 .col-md-10
= render "shared/gl_toggle", = render Pajamas::ToggleComponent.new(classes: 'js-force-push-toggle',
classes: 'js-force-push-toggle',
label: s_("ProtectedBranch|Allowed to force push"), label: s_("ProtectedBranch|Allowed to force push"),
label_position: 'hidden' do label_position: :hidden) do
- force_push_docs_url = help_page_url('topics/git/git_rebase', anchor: 'force-push') - force_push_docs_url = help_page_url('topics/git/git_rebase', anchor: 'force-push')
- force_push_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: force_push_docs_url } - force_push_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: force_push_docs_url }
= (s_("ProtectedBranch|Allow all users with push access to %{tag_start}force push%{tag_end}.") % { tag_start: force_push_link_start, tag_end: '</a>' }).html_safe = (s_("ProtectedBranch|Allow all users with push access to %{tag_start}force push%{tag_end}.") % { tag_start: force_push_link_start, tag_end: '</a>' }).html_safe
......
-# This partial renders a GlToggle root element.
-# To actually initialize the component, make sure to call the initToggle helper from ~/toggles.
- classes = local_assigns.fetch(:classes)
- name = local_assigns.fetch(:name, nil)
- id = local_assigns.fetch(:id, nil)
- is_checked = local_assigns.fetch(:is_checked, false).to_s
- disabled = local_assigns.fetch(:disabled, false).to_s
- is_loading = local_assigns.fetch(:is_loading, false).to_s
- label = local_assigns.fetch(:label, nil)
- help = local_assigns.fetch(:help, nil)
- label_position = local_assigns.fetch(:label_position, nil)
- data = local_assigns.fetch(:data, {})
%span{ class: classes,
data: { name: name,
id: id,
is_checked: is_checked,
disabled: disabled,
is_loading: is_loading,
label: label,
help: help,
label_position: label_position,
**data } }
-# Leverage this block to render a rich help text. To render a plain text help text,
-# prefer the `help` parameter.
- if yield.present?
.gl-text-secondary.gl-mt-1
= yield
...@@ -34,8 +34,7 @@ ...@@ -34,8 +34,7 @@
= _('Members of %{group} can also push to this branch: %{branch}') % { group: (group_push_access_levels.size > 1 ? 'these groups' : 'this group'), branch: group_push_access_levels.map(&:humanize).to_sentence } = _('Members of %{group} can also push to this branch: %{branch}') % { group: (group_push_access_levels.size > 1 ? 'these groups' : 'this group'), branch: group_push_access_levels.map(&:humanize).to_sentence }
%td %td
= render "shared/gl_toggle", = render Pajamas::ToggleComponent.new(classes: 'js-force-push-toggle',
classes: 'js-force-push-toggle',
label: s_("ProtectedBranch|Toggle allowed to force push"), label: s_("ProtectedBranch|Toggle allowed to force push"),
is_checked: protected_branch.allow_force_push, is_checked: protected_branch.allow_force_push,
label_position: 'hidden' label_position: :hidden)
...@@ -18,6 +18,8 @@ module Gitlab ...@@ -18,6 +18,8 @@ module Gitlab
class Application < Rails::Application class Application < Rails::Application
config.load_defaults 6.1 config.load_defaults 6.1
config.view_component.preview_route = "/-/view_component/previews"
# This section contains configuration from Rails upgrades to override the new defaults so that we # This section contains configuration from Rails upgrades to override the new defaults so that we
# keep existing behavior. # keep existing behavior.
# #
......
...@@ -2,9 +2,8 @@ ...@@ -2,9 +2,8 @@
.form-group.row .form-group.row
= f.label :code_owner_approval_required, s_("ProtectedBranch|Require approval from code owners:"), class: 'col-md-2 text-left text-md-right' = f.label :code_owner_approval_required, s_("ProtectedBranch|Require approval from code owners:"), class: 'col-md-2 text-left text-md-right'
.col-md-10 .col-md-10
= render "shared/gl_toggle", = render Pajamas::ToggleComponent.new(classes: 'js-code-owner-toggle',
classes: 'js-code-owner-toggle',
label: s_("ProtectedBranch|Toggle code owner approval"), label: s_("ProtectedBranch|Toggle code owner approval"),
is_checked: true, is_checked: true,
help: s_("ProtectedBranch|Reject code pushes that change files listed in the CODEOWNERS file."), help: s_("ProtectedBranch|Reject code pushes that change files listed in the CODEOWNERS file."),
label_position: 'hidden' label_position: :hidden)
- if @project.feature_available?(:code_owner_approval_required) - if @project.feature_available?(:code_owner_approval_required)
%td %td
= render "shared/gl_toggle", = render Pajamas::ToggleComponent.new(classes: 'js-code-owner-toggle gl-mr-5',
classes: 'js-code-owner-toggle gl-mr-5',
label: s_("ProtectedBranch|Toggle code owner approval"), label: s_("ProtectedBranch|Toggle code owner approval"),
is_checked: protected_branch.code_owner_approval_required, is_checked: protected_branch.code_owner_approval_required,
label_position: 'hidden', label_position: :hidden,
data: { qa_selector: 'code_owner_toggle_button', qa_branch_name: protected_branch.name } data: { qa_selector: 'code_owner_toggle_button', qa_branch_name: protected_branch.name })
...@@ -7,8 +7,7 @@ ...@@ -7,8 +7,7 @@
= render partial: 'projects/settings/ee/access_level_dropdown', locals: { protected_branch: protected_branch, access_levels: protected_branch.push_access_levels, level_frequencies: access_level_frequencies(protected_branch.push_access_levels), input_basic_name: 'push_access_levels', disabled: !can_unprotect, toggle_class: 'js-allowed-to-push' } = render partial: 'projects/settings/ee/access_level_dropdown', locals: { protected_branch: protected_branch, access_levels: protected_branch.push_access_levels, level_frequencies: access_level_frequencies(protected_branch.push_access_levels), input_basic_name: 'push_access_levels', disabled: !can_unprotect, toggle_class: 'js-allowed-to-push' }
%td %td
= render "shared/gl_toggle", = render Pajamas::ToggleComponent.new(classes: 'js-force-push-toggle',
classes: 'js-force-push-toggle',
label: s_("ProtectedBranch|Toggle allowed to force push"), label: s_("ProtectedBranch|Toggle allowed to force push"),
is_checked: protected_branch.allow_force_push, is_checked: protected_branch.allow_force_push,
label_position: 'hidden' label_position: :hidden)
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Pajamas::Component do
describe '#filter_attribute' do
let(:allowed) { %w[default something] }
it 'returns default value when no value is given' do
value = subject.send(:filter_attribute, nil, allowed, default: 'default')
expect(value).to eq('default')
end
it 'returns default value when invalid value is given' do
value = subject.send(:filter_attribute, 'invalid', allowed, default: 'default')
expect(value).to eq('default')
end
it 'returns given value when it is part of allowed list' do
value = subject.send(:filter_attribute, 'something', allowed, default: 'default')
expect(value).to eq('something')
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require "spec_helper"
RSpec.describe 'shared/_gl_toggle.html.haml' do RSpec.describe Pajamas::ToggleComponent, type: :component do
context 'defaults' do context 'with defaults' do
before do before do
render partial: 'shared/gl_toggle', locals: { render_inline described_class.new(classes: 'js-feature-toggle')
classes: '.js-gl-toggle' end
}
it 'renders a toggle container with provided class' do
expect(rendered_component).to have_selector "[class='js-feature-toggle']"
end end
it 'does not set a name' do it 'does not set a name' do
expect(rendered).not_to have_selector('[data-name]') expect(rendered_component).not_to have_selector('[data-name]')
end end
it 'sets default is-checked attributes' do it 'sets default is-checked attributes' do
expect(rendered).to have_selector('[data-is-checked="false"]') expect(rendered_component).to have_selector('[data-is-checked="false"]')
end end
it 'sets default disabled attributes' do it 'sets default disabled attributes' do
expect(rendered).to have_selector('[data-disabled="false"]') expect(rendered_component).to have_selector('[data-disabled="false"]')
end end
it 'sets default is-loading attributes' do it 'sets default is-loading attributes' do
expect(rendered).to have_selector('[data-is-loading="false"]') expect(rendered_component).to have_selector('[data-is-loading="false"]')
end end
it 'does not set a label' do it 'does not set a label' do
expect(rendered).not_to have_selector('[data-label]') expect(rendered_component).not_to have_selector('[data-label]')
end end
it 'does not set a label position' do it 'does not set a label position' do
expect(rendered).not_to have_selector('[data-label-position]') expect(rendered_component).not_to have_selector('[data-label-position]')
end end
end end
context 'with custom options' do context 'with custom options' do
before do before do
render partial: 'shared/gl_toggle', locals: { render_inline described_class.new(
classes: 'js-custom-gl-toggle', classes: 'js-custom-gl-toggle',
name: 'toggle-name', name: 'toggle-name',
is_checked: true, is_checked: true,
disabled: true, is_disabled: true,
is_loading: true, is_loading: true,
label: 'Custom label', label: 'Custom label',
label_position: 'top', label_position: :top,
data: { data: {
foo: 'bar' foo: 'bar'
} })
}
end end
it 'sets the custom class' do it 'sets the custom class' do
expect(rendered).to have_selector('.js-custom-gl-toggle') expect(rendered_component).to have_selector('.js-custom-gl-toggle')
end end
it 'sets the custom name' do it 'sets the custom name' do
expect(rendered).to have_selector('[data-name="toggle-name"]') expect(rendered_component).to have_selector('[data-name="toggle-name"]')
end end
it 'sets the custom is-checked attributes' do it 'sets the custom is-checked attributes' do
expect(rendered).to have_selector('[data-is-checked="true"]') expect(rendered_component).to have_selector('[data-is-checked="true"]')
end end
it 'sets the custom disabled attributes' do it 'sets the custom disabled attributes' do
expect(rendered).to have_selector('[data-disabled="true"]') expect(rendered_component).to have_selector('[data-disabled="true"]')
end end
it 'sets the custom is-loading attributes' do it 'sets the custom is-loading attributes' do
expect(rendered).to have_selector('[data-is-loading="true"]') expect(rendered_component).to have_selector('[data-is-loading="true"]')
end end
it 'sets the custom label' do it 'sets the custom label' do
expect(rendered).to have_selector('[data-label="Custom label"]') expect(rendered_component).to have_selector('[data-label="Custom label"]')
end
it 'sets the custom label position' do
expect(rendered_component).to have_selector('[data-label-position="top"]')
end end
it 'sets the cutom label position' do it 'sets custom data attributes' do
expect(rendered).to have_selector('[data-label-position="top"]') expect(rendered_component).to have_selector('[data-foo="bar"]')
end
end
context 'with setting label_position' do
using RSpec::Parameterized::TableSyntax
where(:position, :count) do
:top | 1
:left | 1
:hidden | 1
:bogus | 0
'bogus' | 0
nil | 0
end
before do
render_inline described_class.new(classes: '_class_', label_position: position)
end end
it 'sets cutom data attributes' do with_them do
expect(rendered).to have_selector('[data-foo="bar"]') it { expect(rendered_component).to have_selector("[data-label-position='#{position}']", count: count) }
end end
end end
end end
# frozen_string_literal: true
require 'view_component/test_helpers'
RSpec.configure do |config|
config.include ViewComponent::TestHelpers, type: :component
config.include Capybara::RSpecMatchers, type: :component
end
...@@ -28,7 +28,7 @@ RSpec.describe Quality::TestLevel do ...@@ -28,7 +28,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do context 'when level is unit' do
it 'returns a pattern' do it 'returns a pattern' do
expect(subject.pattern(:unit)) expect(subject.pattern(:unit))
.to eq("spec/{bin,channels,config,db,dependencies,elastic,elastic_integration,experiments,events,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,metrics_server,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,sidekiq_cluster,spam,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb") .to eq("spec/{bin,channels,config,db,dependencies,elastic,elastic_integration,experiments,events,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,metrics_server,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,sidekiq_cluster,spam,support_specs,tasks,uploaders,validators,views,workers,tooling,component}{,/**/}*_spec.rb")
end end
end end
...@@ -110,7 +110,7 @@ RSpec.describe Quality::TestLevel do ...@@ -110,7 +110,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do context 'when level is unit' do
it 'returns a regexp' do it 'returns a regexp' do
expect(subject.regexp(:unit)) expect(subject.regexp(:unit))
.to eq(%r{spec/(bin|channels|config|db|dependencies|elastic|elastic_integration|experiments|events|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|metrics_server|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|sidekiq_cluster|spam|support_specs|tasks|uploaders|validators|views|workers|tooling)}) .to eq(%r{spec/(bin|channels|config|db|dependencies|elastic|elastic_integration|experiments|events|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|metrics_server|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|sidekiq_cluster|spam|support_specs|tasks|uploaders|validators|views|workers|tooling|component)})
end end
end end
......
...@@ -55,6 +55,7 @@ module Quality ...@@ -55,6 +55,7 @@ module Quality
views views
workers workers
tooling tooling
component
], ],
integration: %w[ integration: %w[
commands commands
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment