diff --git a/admin/app/assets/images/solidus_admin/arrow_down_s_fill_gray_700.svg b/admin/app/assets/images/solidus_admin/arrow_down_s_fill_gray_700.svg new file mode 100644 index 00000000000..78070f47c71 --- /dev/null +++ b/admin/app/assets/images/solidus_admin/arrow_down_s_fill_gray_700.svg @@ -0,0 +1,3 @@ + + + diff --git a/admin/app/assets/images/solidus_admin/arrow_down_s_fill_red_400.svg b/admin/app/assets/images/solidus_admin/arrow_down_s_fill_red_400.svg new file mode 100644 index 00000000000..de7bff77392 --- /dev/null +++ b/admin/app/assets/images/solidus_admin/arrow_down_s_fill_red_400.svg @@ -0,0 +1,3 @@ + + + diff --git a/admin/app/components/solidus_admin/products/show/component.html.erb b/admin/app/components/solidus_admin/products/show/component.html.erb index 0c500371254..edde127bff7 100644 --- a/admin/app/components/solidus_admin/products/show/component.html.erb +++ b/admin/app/components/solidus_admin/products/show/component.html.erb @@ -36,18 +36,15 @@
<%= render component('ui/panel').new do %> - <%= render component("ui/forms/text_field").new(field: :name, builder: f) %> - <%= render component("ui/forms/text_field").new(field: :slug, builder: f) %> - <%= render component("ui/forms/text_area").new(field: :description, builder: f) %> + <%= render component("ui/forms/field").text_field(f, :name) %> + <%= render component("ui/forms/field").text_field(f, :slug) %> + <%= render component("ui/forms/field").text_area(f, :description) %> <% end %> <%= render component('ui/panel').new(title: 'SEO', title_hint: 'Search Engine Optimization') do %> - <%= render component("ui/forms/text_field").new(field: :meta_title, builder: f) %> - <%= render component("ui/forms/text_field").new( - field: :meta_description, - builder: f - ) %> - <%= render component("ui/forms/text_area").new(field: :meta_keywords, builder: f) %> + <%= render component("ui/forms/field").text_field(f, :meta_title) %> + <%= render component("ui/forms/field").text_field(f, :meta_description) %> + <%= render component("ui/forms/field").text_area(f, :meta_keywords) %> <% end %> <%= render component('ui/panel').new(title: "Media") do |panel| %> @@ -58,15 +55,15 @@ <% end %> <%= render component('ui/panel').new(title: 'Pricing') do %> - <%= render component("ui/forms/text_field").new(field: :price, builder: f) %> -
- <%= render component("ui/forms/text_field").new(field: :cost_price, builder: f) %> - <%= render component("ui/forms/text_field").new(field: :cost_currency, builder: f) %> + <%= render component("ui/forms/field").text_field(f, :price) %> +
+ <%= render component("ui/forms/field").text_field(f, :cost_price) %> + <%= render component("ui/forms/field").text_field(f, :cost_currency) %>
<% end %> <%= render component('ui/panel').new(title: 'Stock') do |panel| %> - <%= render component("ui/forms/text_field").new(field: :sku, builder: f) %> + <%= render component("ui/forms/field").text_field(f, :sku) %> <% panel.with_action( name: t(".manage_stock"), @@ -75,38 +72,30 @@ <% end %> <%= render component('ui/panel').new(title: 'Shipping') do %> - <%= render component("ui/forms/select").new( - field: :shipping_category_id, - toggletip: t(".hints.shipping_category_html"), - choices: - [[t(".none"), nil]] + - Spree::ShippingCategory.order(:name).pluck(:name, :id), - builder: f + <%= render component("ui/forms/field").select( + f, + :shipping_category_id, + [[t(".none"), nil]] + Spree::ShippingCategory.order(:name).pluck(:name, :id), + tip: t(".hints.shipping_category_html"), ) %> - <%= render component("ui/forms/select").new( - field: :tax_category_id, - toggletip: - t( - ".hints.tax_category_html", - default_tax_category: Spree::TaxCategory.default&.name - ), - choices: - [[t(".none"), nil]] + Spree::TaxCategory.order(:name).pluck(:name, :id), - builder: f + <%= render component("ui/forms/field").select( + f, + :tax_category_id, + [[t(".none"), nil]] + Spree::TaxCategory.order(:name).pluck(:name, :id), + tip: t( + ".hints.tax_category_html", + default_tax_category: Spree::TaxCategory.default&.name + ), ) %> <% end %> <%= render component('ui/panel').new(title: "Options") do %> - <%= render component("ui/forms/label").new(field: :option_type_ids, builder: f) %> - <%= f.select( + <%= render component("ui/forms/field").select( + f, :option_type_ids, option_type_options, - { include_hidden: false }, - { - multiple: true, - size: option_type_options.size, - class: "w-full padding-2 border border-gray-200 rounded overflow-y-auto" - } + multiple: true, + "size" => option_type_options.size, ) %> <% end %> @@ -120,18 +109,8 @@
diff --git a/admin/app/components/solidus_admin/ui/forms/field/component.html.erb b/admin/app/components/solidus_admin/ui/forms/field/component.html.erb new file mode 100644 index 00000000000..28a3811f2fd --- /dev/null +++ b/admin/app/components/solidus_admin/ui/forms/field/component.html.erb @@ -0,0 +1,28 @@ + diff --git a/admin/app/components/solidus_admin/ui/forms/field/component.rb b/admin/app/components/solidus_admin/ui/forms/field/component.rb new file mode 100644 index 00000000000..b66c7cc1a97 --- /dev/null +++ b/admin/app/components/solidus_admin/ui/forms/field/component.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +class SolidusAdmin::UI::Forms::Field::Component < SolidusAdmin::BaseComponent + def initialize(label:, hint: nil, tip: nil, error: nil, input_attributes: nil, **attributes) + @label = label + @hint = hint + @tip = tip + @error = [error] if error.present? + @attributes = attributes + @input_attributes = input_attributes + + raise ArgumentError, "provide either a block or input_attributes" if content? && input_attributes + end + + def self.text_field(form, method, hint: nil, tip: nil, size: :m, **attributes) + errors = form.object.errors.messages_for(method).presence + + new( + label: form.object.class.human_attribute_name(method), + hint: hint, + tip: tip, + error: errors, + input_attributes: { + name: "#{form.object_name}[#{method}]", + tag: :input, + size: size, + value: form.object.public_send(method), + error: (errors.to_sentence.capitalize if errors), + **attributes, + } + ) + end + + def self.select(form, method, choices, hint: nil, tip: nil, size: :m, **attributes) + errors = form.object.errors.messages_for(method).presence + + new( + label: form.object.class.human_attribute_name(method), + hint: hint, + tip: tip, + error: errors, + input_attributes: { + name: "#{form.object_name}[#{method}]", + tag: :select, + choices: choices, + size: size, + value: form.object.public_send(method), + error: (errors.to_sentence.capitalize if errors), + **attributes, + } + ) + end + + def self.text_area(form, method, hint: nil, tip: nil, size: :m, **attributes) + errors = form.object.errors.messages_for(method).presence + + new( + label: form.object.class.human_attribute_name(method), + hint: hint, + tip: tip, + error: errors, + input_attributes: { + name: "#{form.object_name}[#{method}]", + size: size, + tag: :textarea, + value: form.object.public_send(method), + error: (errors.to_sentence.capitalize if errors), + **attributes, + } + ) + end +end diff --git a/admin/app/components/solidus_admin/ui/forms/fieldset/component.erb b/admin/app/components/solidus_admin/ui/forms/fieldset/component.erb deleted file mode 100644 index 7b05c129a8f..00000000000 --- a/admin/app/components/solidus_admin/ui/forms/fieldset/component.erb +++ /dev/null @@ -1,4 +0,0 @@ -<%= tag.fieldset(**fieldset_html_attributes) do %> - <%= legend_and_toggletip_tags %> - <%= content %> -<% end %> diff --git a/admin/app/components/solidus_admin/ui/forms/fieldset/component.rb b/admin/app/components/solidus_admin/ui/forms/fieldset/component.rb deleted file mode 100644 index 17a32b6710a..00000000000 --- a/admin/app/components/solidus_admin/ui/forms/fieldset/component.rb +++ /dev/null @@ -1,64 +0,0 @@ -# frozen_string_literal: true - -class SolidusAdmin::UI::Forms::Fieldset::Component < SolidusAdmin::BaseComponent - # @param legend [String, nil] The legend of the fieldset. - # @param fieldset_attributes [Hash] Attributes to pass to the fieldset tag. - # @param legend_attributes [Hash, nil] Attributes to pass to the legend tag. - # @param toggletip_attributes [Hash, nil] Attributes to pass to a toggletip - # component that will be rendered after the legend. - def initialize( - legend: nil, - attributes: {}, - legend_attributes: {}, - toggletip_attributes: {} - ) - @legend = legend - @attributes = attributes - @legend_attributes = legend_attributes - @toggletip_attributes = toggletip_attributes - end - - def fieldset_html_attributes - { - class: fieldset_classes, - **@attributes.except(:class) - } - end - - def fieldset_classes - %w[p-6 mb-6 border border-gray-100 rounded-lg] + Array(@attributes[:class]).compact - end - - def legend_and_toggletip_tags - return "" unless @legend || @toggletip_attributes.any? - - tag.div(class: "flex mb-4") do - legend_tag + toggletip_tag - end - end - - def legend_tag - return "".html_safe unless @legend - - tag.legend(@legend, **legend_html_attributes) - end - - def legend_html_attributes - { - class: legend_classes, - **@legend_attributes.except(:class) - } - end - - def legend_classes - %w[body-title mr-2] + Array(@legend_attributes[:class]).compact - end - - def toggletip_tag - return "" unless @toggletip_attributes.any? - - tag.div do - render component("ui/toggletip").new(**@toggletip_attributes) - end - end -end diff --git a/admin/app/components/solidus_admin/ui/forms/form/component.erb b/admin/app/components/solidus_admin/ui/forms/form/component.erb deleted file mode 100644 index 7ea4458e700..00000000000 --- a/admin/app/components/solidus_admin/ui/forms/form/component.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%= form_with(**@attributes) do |builder| %> - <%= render_elements(@elements, builder) %> -<% end %> diff --git a/admin/app/components/solidus_admin/ui/forms/form/component.rb b/admin/app/components/solidus_admin/ui/forms/form/component.rb deleted file mode 100644 index ca5e2b51742..00000000000 --- a/admin/app/components/solidus_admin/ui/forms/form/component.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -class SolidusAdmin::UI::Forms::Form::Component < SolidusAdmin::BaseComponent - # @param elements [Array<#call(form, builder)>] Builders of renderable - # elements within a form context. They need to implement `#call(form, - # builder)`, where the arguments are an instance of this class and an - # instance of `ActionView::Helpers::FormBuilder`. The method needs to return - # something responding to `#render_in(view_context)`. See the following - # classes for examples: - # - {SolidusAdmin::Form::Elements::Field} - # - {SolidusAdmin::Form::Elements::Fieldset} - # - {SolidusAdmin::Form::Elements::Component} - # - {SolidusAdmin::Form::Elements::HTML} - # @param attributes [Hash] Attributes to pass to the Rails `form_with` helper, - # which is used to render the form. - def initialize(elements:, **attributes) - @elements = elements - @attributes = attributes - end - - # @return [Hash{Symbol => SolidusAdmin::BaseComponent}] Hash of component - # classes dependencies given on initialization. - def dependencies - { - fieldset: component("ui/forms/fieldset"), - text_field: component("ui/forms/text_field"), - text_area: component("ui/forms/text_area") - } - end - - # @api private - def render_elements(elements, builder) - safe_join( - elements.map do |element| - render_element(element, builder) - end - ) - end - - # @api private - def render_element(element, builder) - render element.call(self, builder) - end -end diff --git a/admin/app/components/solidus_admin/ui/forms/guidance/component.rb b/admin/app/components/solidus_admin/ui/forms/guidance/component.rb deleted file mode 100644 index 67d88cb189a..00000000000 --- a/admin/app/components/solidus_admin/ui/forms/guidance/component.rb +++ /dev/null @@ -1,70 +0,0 @@ -# frozen_string_literal: true - -# @api private -class SolidusAdmin::UI::Forms::Guidance::Component < SolidusAdmin::BaseComponent - def initialize(field:, builder:, hint:, errors:, disabled: false) - @field = field - @builder = builder - @hint = hint - @disabled = disabled - @errors = errors || @builder.object&.errors || raise(ArgumentError, <<~MSG - When the form builder is not bound to a model instance, you must pass an - errors Hash (`{ field_name: [errors] }`) to the component. - MSG - ) - end - - def call - return "" unless needed? - - tag.div(class: "mt-2") do - hint_tag + error_tag - end - end - - def hint_tag - return "".html_safe unless @hint - - tag.p(id: hint_id, class: "body-tiny #{hint_text_color_class}") do - @hint - end - end - - def hint_text_color_class - @disabled ? "text-gray-300" : "text-gray-500" - end - - def hint_id - "#{prefix}_hint" - end - - def error_tag - return "".html_safe unless errors? - - tag.p(id: error_id, class: "body-tiny text-red-400") do - @errors[@field].map do |error| - tag.span(class: "block") { error.capitalize } - end.reduce(&:+) - end - end - - def errors? - @errors[@field].present? - end - - def error_id - "#{prefix}_error" - end - - def prefix - "#{@builder.object_name}_#{@field}" - end - - def aria_describedby - "#{hint_id if @hint} #{error_id if errors?}" - end - - def needed? - @hint || errors? - end -end diff --git a/admin/app/components/solidus_admin/ui/forms/input/component.js b/admin/app/components/solidus_admin/ui/forms/input/component.js new file mode 100644 index 00000000000..44ff79b03b0 --- /dev/null +++ b/admin/app/components/solidus_admin/ui/forms/input/component.js @@ -0,0 +1,16 @@ +import { Controller } from '@hotwired/stimulus' + +export default class extends Controller { + static values = { + customValidity: String, + } + + connect() { + if (this.customValidityValue) + this.element.setCustomValidity(this.customValidityValue) + } + + clearCustomValidity() { + this.element.setCustomValidity('') + } +} diff --git a/admin/app/components/solidus_admin/ui/forms/input/component.rb b/admin/app/components/solidus_admin/ui/forms/input/component.rb new file mode 100644 index 00000000000..e933cbb9409 --- /dev/null +++ b/admin/app/components/solidus_admin/ui/forms/input/component.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +class SolidusAdmin::UI::Forms::Input::Component < SolidusAdmin::BaseComponent + SIZES = { + s: "form-control-sm px-3 py-1.5 body-small", + m: "form-control-md px-3 py-1.5 body-small", + l: "form-control-lg px-3 py-1.5 body-text" + }.freeze + + HEIGHTS = { + s: "h-7", + m: "h-9", + l: "h-12" + }.freeze + + MULTILINE_HEIGHTS = { + s: %w[min-h-[84px]], + m: %w[min-h-[108px]], + l: %w[min-h-[144px]], + }.freeze + + TYPES = Set.new(%i[ + text + password + number + email + tel + url + search + color + date + datetime-local + month + week + time + ]).freeze + + def initialize(tag: :input, size: :m, error: nil, **attributes) + raise ArgumentError, "unsupported tag: #{tag}" unless %i[input textarea select].include?(tag) + + specialized_classes = [] + + case tag + when :input + specialized_classes << "form-input" + specialized_classes << HEIGHTS[size] + if attributes[:type] && !TYPES.include?(attributes[:type]) + raise ArgumentError, "unsupported type attribute: #{attributes[:type]}" + end + when :textarea + specialized_classes << "form-textarea" + specialized_classes << MULTILINE_HEIGHTS[size] + when :select + if attributes[:multiple] + specialized_classes << "form-multiselect" + specialized_classes << MULTILINE_HEIGHTS[size] + else + specialized_classes << "form-select" + specialized_classes << "bg-arrow-down-s-fill-gray-700 invalid:bg-arrow-down-s-fill-red-400 aria-invalid:bg-arrow-down-s-fill-red-400" + specialized_classes << HEIGHTS[size] + end + end + + attributes[:class] = [ + %w[ + w-full + text-black bg-white border border-gray-300 rounded-sm placeholder:text-gray-400 + hover:border-gray-500 + focus:ring focus:ring-gray-300 focus:ring-0.5 focus:bg-white focus:ring-offset-0 [&:focus-visible]:outline-none + disabled:bg-gray-50 disabled:text-gray-500 disabled:placeholder:text-gray-300 disabled:cursor-not-allowed + invalid:border-red-400 invalid:hover:border-red-400 invalid:text-red-400 + aria-invalid:border-red-400 aria-invalid:hover:border-red-400 aria-invalid:text-red-400 + ], + SIZES[size], + specialized_classes, + attributes[:class], + ].compact.join(" ") + + @tag = tag + @size = size + @error = error + @attributes = attributes + end + + def call + if @tag == :select && @attributes[:choices] + with_content options_for_select(@attributes.delete(:choices), @attributes.delete(:value)) + end + + tag.public_send( + @tag, + content, + "data-controller": stimulus_id, + "data-#{stimulus_id}-custom-validity-value": @error.presence, + "data-action": "#{stimulus_id}#clearCustomValidity", + **@attributes + ) + end +end diff --git a/admin/app/components/solidus_admin/ui/forms/label/component.rb b/admin/app/components/solidus_admin/ui/forms/label/component.rb deleted file mode 100644 index 072042cc811..00000000000 --- a/admin/app/components/solidus_admin/ui/forms/label/component.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -# @api private -class SolidusAdmin::UI::Forms::Label::Component < SolidusAdmin::BaseComponent - def initialize(field:, builder:) - @field = field - @builder = builder - end - - def call - @builder.label(@field, class: "block mb-0.5 body-tiny-bold") - end -end diff --git a/admin/app/components/solidus_admin/ui/forms/select/component.js b/admin/app/components/solidus_admin/ui/forms/select/component.js deleted file mode 100644 index b8e937cdfb5..00000000000 --- a/admin/app/components/solidus_admin/ui/forms/select/component.js +++ /dev/null @@ -1,33 +0,0 @@ -import { Controller } from '@hotwired/stimulus' - -export default class extends Controller { - static targets = ['select', 'arrow'] - static classes = ['regular', 'prompt', 'arrowPrompt'] - - connect () { - this.addClassToOptions() - this.refreshSelectClass() - } - - // Add class to all the options to avoid inheriting the select's styles - addClassToOptions () { - this.selectTarget.querySelectorAll('option').forEach((option) => { - if (option.value == '') { - option.classList.add(this.promptClass) - } else { - option.classList.add(this.regularClass) - } - }) - } - - // Make the select look like a placeholder when the prompt is selected - refreshSelectClass () { - if (this.selectTarget.options[this.selectTarget.selectedIndex].value == '') { - this.selectTarget.classList.add(this.promptClass) - this.arrowTarget.classList.add(this.arrowPromptClass) - } else { - this.selectTarget.classList.remove(this.promptClass) - this.arrowTarget.classList.remove(this.arrowPromptClass) - } - } -} diff --git a/admin/app/components/solidus_admin/ui/forms/select/component.rb b/admin/app/components/solidus_admin/ui/forms/select/component.rb deleted file mode 100644 index b52669184a3..00000000000 --- a/admin/app/components/solidus_admin/ui/forms/select/component.rb +++ /dev/null @@ -1,162 +0,0 @@ -# frozen_string_literal: true - -class SolidusAdmin::UI::Forms::Select::Component < SolidusAdmin::BaseComponent - SIZES = { - s: { - select: %w[leading-4 body-small], - arrow: %w[w-4 h-4] - }, - m: { - select: %w[leading-6 body-small], - arrow: %w[w-5 h-5] - }, - l: { - select: %w[leading-9 body-text], - arrow: %w[w-6 h-6] - } - }.freeze - - # @param field [Symbol] the name of the field. Usually a model attribute. - # @param builder [ActionView::Helpers::FormBuilder] the form builder instance. - # @param size [Symbol] the size of the field: `:s`, `:m` or `:l`. - # @param choices [Array] an array of choices for the select box. All the - # formats valid for Rails' `select` helper are supported. - # @param hint [String, null] helper text to display below the select box. - # @param errors [Hash, nil] a Hash of errors for the field. If `nil` and the - # builder is bound to a model instance, the component will automatically fetch - # the errors from the model. - # @param options [Hash] additional options to pass to Rails' `select` helper. - # @param attributes [Hash] additional HTML attributes to add to the select box. - # @raise [ArgumentError] when the form builder is not bound to a model - # instance and no `errors` Hash is passed to the component. - def initialize( - field:, - builder:, - size: :m, - choices: [], - hint: nil, - errors: nil, - toggletip: nil, - options: {}, - attributes: {} - ) - @field = field - @builder = builder - @size = size - @choices = choices - @hint = hint - @toggletip = toggletip - @options = options - @attributes = HashWithIndifferentAccess.new(attributes) - @errors = errors - end - - def call - guidance = component("ui/forms/guidance").new( - field: @field, - builder: @builder, - hint: @hint, - errors: @errors, - disabled: @attributes[:disabled] - ) - - toggletip_tag = @toggletip.present? ? render(component('ui/toggletip').new(text: @toggletip)) : '' - - tag.div(class: "mb-6") do - tag.div(label_tag + toggletip_tag, class: "flex gap-1") + field_tag(guidance) + guidance_tag(guidance) - end - end - - def field_wrapper_tag(guidance) - tag.div( - class: "relative", - "data-controller" => stimulus_id, - "data-#{stimulus_id}-regular-class" => "text-black", - "data-#{stimulus_id}-prompt-class" => "text-gray-400", - "data-#{stimulus_id}-arrow-prompt-class" => "!fill-gray-500" - ) do - field_tag(guidance) + arrow_tag(guidance) - end - end - - def field_tag(guidance) - @builder.select( - @field, - @choices, - @options, - class: field_classes(guidance), - **field_aria_describedby_attribute(guidance), - **field_error_attributes(guidance), - **@attributes.except(:class).merge( - "data-#{stimulus_id}-target" => "select", - "data-action" => "#{stimulus_id}#refreshSelectClass" - ) - ) - end - - def field_classes(guidance) - %w[ - block px-3 py-1.5 w-full - appearance-none - text-black - bg-white border border-gray-300 rounded-sm - hover:border-gray-500 - focus:border-gray-500 focus:shadow-[0_0_0_2px_#bbb] focus-visible:outline-none - disabled:bg-gray-50 disabled:text-gray-300 - ] + field_size_classes + field_error_classes(guidance) + Array(@attributes[:class]).compact - end - - def field_size_classes - SIZES.fetch(@size)[:select] - end - - def field_error_classes(guidance) - return [] unless guidance.errors? - - %w[border-red-400 text-red-400] - end - - def field_aria_describedby_attribute(guidance) - return {} unless guidance.needed? - - { - "aria-describedby": guidance.aria_describedby - } - end - - def field_error_attributes(guidance) - return {} unless guidance.errors? - - { - "aria-invalid": true - } - end - - def arrow_tag(guidance) - icon_tag( - "arrow-down-s-fill", - class: SIZES.fetch(@size)[:arrow] + [ - arrow_color_class(guidance) - ] + %w[absolute right-3 top-1/2 translate-y-[-50%] pointer-events-none], - "data-#{stimulus_id}-target" => "arrow" - ) - end - - def arrow_color_class(guidance) - if @attributes[:disabled] - "fill-gray-500" - elsif guidance.errors? - "fill-red-400" - else - "fill-gray-700" - end - end - - def label_tag - render component("ui/forms/label").new(field: @field, builder: @builder) - end - - def guidance_tag(guidance) - render guidance - end -end diff --git a/admin/app/components/solidus_admin/ui/forms/text_area/component.rb b/admin/app/components/solidus_admin/ui/forms/text_area/component.rb deleted file mode 100644 index 4921633b8ae..00000000000 --- a/admin/app/components/solidus_admin/ui/forms/text_area/component.rb +++ /dev/null @@ -1,105 +0,0 @@ -# frozen_string_literal: true - -class SolidusAdmin::UI::Forms::TextArea::Component < SolidusAdmin::BaseComponent - SIZES = { - s: %w[h-20 body-small], - m: %w[h-28 body-small], - l: %w[h-36 body-text] - }.freeze - - # @param field [Symbol] the name of the field. Usually a model attribute. - # @param builder [ActionView::Helpers::FormBuilder] the form builder instance. - # @param size [Symbol] the size of the field: `:s`, `:m` or `:l`. - # @param hint [String, null] helper text to display below the field. - # @param errors [Hash, nil] a Hash of errors for the field. If `nil` and the - # builder is bound to a model instance, the component will automatically fetch - # the errors from the model. - # @param attributes [Hash] additional HTML attributes to add to the field. - # @raise [ArgumentError] when the form builder is not bound to a model - # instance and no `errors` Hash is passed to the component. - def initialize( - field:, - builder:, - size: :m, - hint: nil, - errors: nil, - **attributes - ) - @field = field - @builder = builder - @size = size - @hint = hint - @attributes = HashWithIndifferentAccess.new(attributes) - @errors = errors - end - - def call - guidance = component("ui/forms/guidance").new( - field: @field, - builder: @builder, - hint: @hint, - errors: @errors, - disabled: @attributes[:disabled] - ) - - tag.div(class: "mb-6") do - label_tag + field_tag(guidance) + guidance_tag(guidance) - end - end - - def field_tag(guidance) - @builder.text_area( - @field, - class: field_classes(guidance), - **field_aria_describedby_attribute(guidance), - **field_error_attributes(guidance), - **@attributes.except(:class) - ) - end - - def field_classes(guidance) - %w[ - block px-3 py-4 w-full - text-black - bg-white border border-gray-300 rounded-sm - hover:border-gray-500 - placeholder:text-gray-400 - focus:border-gray-500 focus:shadow-[0_0_0_2px_#bbb] focus-visible:outline-none - disabled:bg-gray-50 disabled:text-gray-300 - ] + field_size_classes + field_error_classes(guidance) + Array(@attributes[:class]).compact - end - - def field_size_classes - SIZES.fetch(@size) - end - - def field_aria_describedby_attribute(guidance) - return {} unless guidance.needed? - - { - "aria-describedby": guidance.aria_describedby - } - end - - def field_error_classes(guidance) - return [] unless guidance.errors? - - %w[border-red-400 text-red-400] - end - - def field_error_attributes(guidance) - return {} unless guidance.errors? - - { - "aria-invalid": true - } - end - - def label_tag - render component("ui/forms/label").new(field: @field, builder: @builder) - end - - def guidance_tag(guidance) - render guidance - end -end diff --git a/admin/app/components/solidus_admin/ui/forms/text_field/component.rb b/admin/app/components/solidus_admin/ui/forms/text_field/component.rb deleted file mode 100644 index e33a31c13eb..00000000000 --- a/admin/app/components/solidus_admin/ui/forms/text_field/component.rb +++ /dev/null @@ -1,136 +0,0 @@ -# frozen_string_literal: true - -class SolidusAdmin::UI::Forms::TextField::Component < SolidusAdmin::BaseComponent - SIZES = { - s: %w[leading-4 body-small], - m: %w[leading-6 body-small], - l: %w[leading-9 body-text] - }.freeze - - TYPES = { - color: :color_field, - date: :date_field, - datetime: :datetime_field, - email: :email_field, - month: :month_field, - number: :number_field, - password: :password_field, - phone: :phone_field, - range: :range_field, - search: :search_field, - text: :text_field, - time: :time_field, - url: :url_field, - week: :week_field - }.freeze - - # @param field [Symbol] the name of the field. Usually a model attribute. - # @param builder [ActionView::Helpers::FormBuilder] the form builder instance. - # @param type [Symbol] the type of the field. Defaults to `:text`. - # @param size [Symbol] the size of the field: `:s`, `:m` or `:l`. - # @param hint [String, null] helper text to display below the field. - # @param errors [Hash, nil] a Hash of errors for the field. If `nil` and the - # builder is bound to a model instance, the component will automatically fetch - # the errors from the model. - # @param attributes [Hash] additional HTML attributes to add to the field. - # @raise [ArgumentError] when the form builder is not bound to a model - # instance and no `errors` Hash is passed to the component. - def initialize( - field:, - builder:, - type: :text, - size: :m, - hint: nil, - errors: nil, - toggletip: nil, - **attributes - ) - @field = field - @builder = builder - @type = type - @size = size - @hint = hint - @type = type - @toggletip = toggletip - @attributes = HashWithIndifferentAccess.new(attributes) - @errors = errors - end - - def call - guidance = component("ui/forms/guidance").new( - field: @field, - builder: @builder, - hint: @hint, - errors: @errors, - disabled: @attributes[:disabled] - ) - - toggletip_tag = @toggletip.present? ? render(component('ui/toggletip').new(text: @toggletip)) : '' - - tag.div(class: "w-full mb-6") do - tag.div(label_tag + toggletip_tag, class: "flex gap-1") + field_tag(guidance) + guidance_tag(guidance) - end - end - - def field_tag(guidance) - @builder.send( - field_helper, - @field, - class: field_classes(guidance), - **field_aria_describedby_attribute(guidance), - **field_error_attributes(guidance), - **@attributes.except(:class) - ) - end - - def field_classes(guidance) - %w[ - form-input - block px-3 py-1.5 w-full - text-black - bg-white border border-gray-300 rounded-sm - hover:border-gray-500 - placeholder:text-gray-400 - focus:border-gray-500 focus:shadow-[0_0_0_2px_#bbb] focus-visible:outline-none - disabled:bg-gray-50 disabled:text-gray-300 - ] + field_size_classes + field_error_classes(guidance) + Array(@attributes[:class]).compact - end - - def field_helper - TYPES.fetch(@type) - end - - def field_size_classes - SIZES.fetch(@size) - end - - def field_aria_describedby_attribute(guidance) - return {} unless guidance.needed? - - { - "aria-describedby": guidance.aria_describedby - } - end - - def field_error_classes(guidance) - return [] unless guidance.errors? - - %w[border-red-400 text-red-400] - end - - def field_error_attributes(guidance) - return {} unless guidance.errors? - - { - "aria-invalid": true - } - end - - def label_tag - render component("ui/forms/label").new(field: @field, builder: @builder) - end - - def guidance_tag(guidance) - render guidance - end -end diff --git a/admin/app/components/solidus_admin/ui/panel/component.html.erb b/admin/app/components/solidus_admin/ui/panel/component.html.erb index 065507a2655..17c3c2a4c0e 100644 --- a/admin/app/components/solidus_admin/ui/panel/component.html.erb +++ b/admin/app/components/solidus_admin/ui/panel/component.html.erb @@ -23,7 +23,7 @@ <% end %> <% if content&.present? %> -
+
<%= content %>
<% end %> diff --git a/admin/config/solidus_admin/tailwind.config.js.erb b/admin/config/solidus_admin/tailwind.config.js.erb index 8846b6f8ca7..5cf74942e64 100644 --- a/admin/config/solidus_admin/tailwind.config.js.erb +++ b/admin/config/solidus_admin/tailwind.config.js.erb @@ -74,6 +74,8 @@ module.exports = { }, backgroundImage: { 'arrow-right-up-line': "url('solidus_admin/arrow_right_up_line.svg')", + 'arrow-down-s-fill-gray-700': "url('solidus_admin/arrow_down_s_fill_gray_700.svg')", + 'arrow-down-s-fill-red-400': "url('solidus_admin/arrow_down_s_fill_red_400.svg')", }, boxShadow: { sm: '0px 1px 2px 0px rgba(0, 0, 0, 0.04)', diff --git a/admin/lib/solidus_admin/form/element/component.rb b/admin/lib/solidus_admin/form/element/component.rb deleted file mode 100644 index c46751808ab..00000000000 --- a/admin/lib/solidus_admin/form/element/component.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module SolidusAdmin - module Form - module Element - # Builds an arbitrary component in a form context. - # - # This class can be used to render an arbitrary components in a form. - # - # This is useful when there's the need to render a component that's not - # strictly related to a form definition, but still needs to be within the - # form tags. - class Component - # @param component [ViewComponent::Base] the component instance to - # render. - def initialize(component:) - @component = component - end - - # @api private - def call(_form, _builder) - @component - end - end - end - end -end diff --git a/admin/lib/solidus_admin/form/element/field.rb b/admin/lib/solidus_admin/form/element/field.rb deleted file mode 100644 index 9b54f22679e..00000000000 --- a/admin/lib/solidus_admin/form/element/field.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -module SolidusAdmin - module Form - module Element - # Builds a form field component. - # - # This class encapsulates a form field definition and its resolution to a - # component. - class Field - # @param component [Symbol, ViewComponent::Base] the component to be - # used when rendering. It can be a component class (which needs to - # accept the `builder:` parameter on initialization) or a Symbol. When - # the latter, it's used to infer the one configured in the form - # instance. For instance, for a `:text_field` type, the component used - # will be the one given to the form component as the - # `text_field_component` keyword argument on initialization. - # @param attributes [Hash] attributes to pass to the field component. - def initialize(component:, **attributes) - @component = component - @attributes = attributes - end - - # @api private - def call(form, builder) - component_class(form).new( - builder: builder, - **@attributes - ) - end - - private - - def component_class(form) - case @component - when Symbol - form.dependencies[@component] - else - @component - end - end - end - end - end -end diff --git a/admin/lib/solidus_admin/form/element/fieldset.rb b/admin/lib/solidus_admin/form/element/fieldset.rb deleted file mode 100644 index 08f01e4674d..00000000000 --- a/admin/lib/solidus_admin/form/element/fieldset.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -module SolidusAdmin - module Form - module Element - # Builds a form fieldset component. - # - # This class encapsulates a form fieldset definition and its resolution to - # a component. - class Fieldset - # @param elements [Array<#call(form, builder)>] See - # {SolidusAdmin::UI::Forms::Form::Component#initialize}. - # @param component [ViewComponent::Base, nil] the component to be - # used when rendering. When `nil`, the component configured in the form - # `fieldset_component` keyword argument on initialization is used. - # @param attributes [Hash] Attributes to pass to the fieldset - # component. - def initialize(elements:, component: nil, **attributes) - @elements = elements - @component = component - @attributes = attributes - end - - # @api private - def call(form, builder) - component_class(form).new( - **@attributes - ).with_content( - render_elements(form, builder) - ) - end - - private - - def component_class(form) - @component || form.dependencies[:fieldset] - end - - def render_elements(form, builder) - return "" if @elements.empty? - - form.render_elements(@elements, builder) - end - end - end - end -end diff --git a/admin/lib/solidus_admin/form/element/html.rb b/admin/lib/solidus_admin/form/element/html.rb deleted file mode 100644 index e9c681507b2..00000000000 --- a/admin/lib/solidus_admin/form/element/html.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -module SolidusAdmin - module Form - module Element - # Builds arbitrary HTML in a form. - # - # This class can be used to render arbitrary content in a form. - # - # This is useful when there's the need to render content that's not - # strictly related to a form definition, but still needs to be within the - # form tags. If the content is a component, it's better to use - # {SolidusAdmin::Form::Element::Component} instead. - class HTML - # @param html [String] the HTML to render. - def initialize(html:) - @html = html - end - - # @api private - def call(_form, _builder) - self - end - - # @api private - def render_in(_view_context) - @html - end - end - end - end -end diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/field/component_preview.rb b/admin/spec/components/previews/solidus_admin/ui/forms/field/component_preview.rb new file mode 100644 index 00000000000..38aa05b4e0c --- /dev/null +++ b/admin/spec/components/previews/solidus_admin/ui/forms/field/component_preview.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# @component "ui/forms/field" +class SolidusAdmin::UI::Forms::Field::ComponentPreview < ViewComponent::Preview + include SolidusAdmin::Preview + + def overview + render_with_template + end + + # @param hint text + # @param tip text + # @param error text + def playground(label: "My field", hint: "hint", tip: "tip", error: "error") + render component("ui/forms/field").new(label: label, hint: hint, tip: tip, error: error, input_attributes: { + tag: :input, value: "My value", error: error + }) + end +end diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/field/component_preview/overview.html.erb b/admin/spec/components/previews/solidus_admin/ui/forms/field/component_preview/overview.html.erb new file mode 100644 index 00000000000..b47ea5c14bc --- /dev/null +++ b/admin/spec/components/previews/solidus_admin/ui/forms/field/component_preview/overview.html.erb @@ -0,0 +1,26 @@ +
+
+ With just the label +
+ + <%= render current_component.new( + label: "Greeting", + ) do %> + <%= render component('ui/forms/input').new(value: "Hello world!") %> + <% end %> +
+
+
+ With hint, + tip and error +
+ + <%= render current_component.new( + label: "Greeting", + hint: "A brief explanation", + tip: "More in-detail information about this filed", + error: "The second letter should be lowecase", + ) do %> + <%= render component('ui/forms/input').new(value: "HEllo world!", error: "The second letter should be lowecase") %> + <% end %> +
diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/fieldset/component_preview.rb b/admin/spec/components/previews/solidus_admin/ui/forms/fieldset/component_preview.rb deleted file mode 100644 index 98ba84cb3f7..00000000000 --- a/admin/spec/components/previews/solidus_admin/ui/forms/fieldset/component_preview.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -# @component "ui/forms/fieldset" -class SolidusAdmin::UI::Forms::Fieldset::ComponentPreview < ViewComponent::Preview - include SolidusAdmin::Preview - - # The fieldset component is used to render a set of fields in a form. - # - # Most commonly, it'll be used indirectly through the definition given to a - # [form component](../form/overview). - # - # For standalone usage, it wraps the yielded content in a fieldset tag: - # - # ```erb - # <%= render components('ui/forms/fieldset').new do %> - # <%= # ... %> - # <% end %> - # ``` - # - # The legend of the fieldset can be set with the `legend` option: - # - # ```erb - # <%= render components('ui/forms/fieldset').new( - # legend: "My fieldset" - # ) do %> - # <%= # ... %> - # <% end %> - # ``` - # - # Lastly, a toggletip can be added to the legend with the - # `toggletip_attributes`, which will be passed to the [toggletip - # component](../../toggletip): - # - # ```erb - # <%= render components('ui/forms/fieldset').new( - # legend: "My fieldset", - # toggletip_attributes: { - # text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - # position: :right - # } - # ) do %> - # <%= # ... %> - # <% end %> - # ``` - def overview - end -end diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/fieldset/component_preview/overview.html.erb b/admin/spec/components/previews/solidus_admin/ui/forms/fieldset/component_preview/overview.html.erb deleted file mode 100644 index 8376955d631..00000000000 --- a/admin/spec/components/previews/solidus_admin/ui/forms/fieldset/component_preview/overview.html.erb +++ /dev/null @@ -1,48 +0,0 @@ -<%= form_with(url: "#", scope: :overview, method: :get, class: "w-full") do |form| %> -
-
- <%= render current_component.new do %> - <%= - render component('ui/forms/text_field').new( - field: :name, - builder: form, - errors: {} - ) - %> - <% end %> -
-
- <%= render current_component.new(legend: "Legend") do %> - <%= - render component('ui/forms/text_field').new( - field: :name, - builder: form, - errors: {} - ) - %> - <% end %> -
-
- <%= render current_component.new(toggletip_attributes: { text: "Lorem ipsum dolor est." }) do %> - <%= - render component('ui/forms/text_field').new( - field: :name, - builder: form, - errors: {} - ) - %> - <% end %> -
-
- <%= render current_component.new(legend: "Legend & tip", toggletip_attributes: { text: "Lorem ipsum dolor est.", position: :left, theme: :dark }) do %> - <%= - render component('ui/forms/text_field').new( - field: :name, - builder: form, - errors: {} - ) - %> - <% end %> -
-
-<% end %> diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/form/component_preview.rb b/admin/spec/components/previews/solidus_admin/ui/forms/form/component_preview.rb deleted file mode 100644 index 10786cf5a41..00000000000 --- a/admin/spec/components/previews/solidus_admin/ui/forms/form/component_preview.rb +++ /dev/null @@ -1,170 +0,0 @@ -# frozen_string_literal: true - -require "solidus_admin/form/element/field" -require "solidus_admin/form/element/fieldset" -require "solidus_admin/form/element/component" -require "solidus_admin/form/element/html" - -# @component "ui/forms/form" -class SolidusAdmin::UI::Forms::Form::ComponentPreview < ViewComponent::Preview - include SolidusAdmin::Preview - - # The form component is used to render a form tag along with its content, most - # commonly form fields. - # - # Internally, the - # [`form_with`](https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with) - # Rails helper is used to render the form tag, and the component will dispatch - # given arguments to it. - # - # The definition of the form is provided from the outside through the - # `elements` parameter. This parameter is an array of builders of renderable - # elements, and Solidus Admin provides all the necessary ones to build a form - # following its UI: - # - # ## SolidusAdmin::Form::Element::Field - # - # This element renders a form field: - # - # ```erb - # <%= - # render components('ui/forms/form', - # model: Spree::Product.new, - # elements: [ - # SolidusAdmin::Form::Element::Field.new( - # component: :text_field, - # field: :name - # ) - # ] - # ) - # %> - # ``` - # - # The previous example will use the [`text_field` - # component](../text_field/overview), but you can use any of the available - # field component. - # - # ## SolidusAdmin::Form::Element::Fieldset - # - # Wraps a set of fields in a fieldset. - # - # You need to provide the inner fields akin to how it's done with the form - # component. [The fieldet component](../fieldset/overview) is used under the - # hood, and you can pass any of its attributes through the `attributes` - # parameter. - # - # ```erb - # <%= - # render components('ui/forms/form', - # model: Spree::Product.new, - # elements: [ - # SolidusAdmin::Form::Element::Fieldset.new( - # elements: [ - # SolidusAdmin::Form::Element::Field.new( - # component: :text_field, - # field: :name - # ) - # ], - # legend: "Product details", - # toggletip_attributes: { text: "Minimal info", position: :right } - # ) - # ] - # ) - # %> - # ``` - # - # ## SolidusAdmin::Form::Element::Component - # - # This element allows you to render any component inside the form. - # - # ```erb - # <%= - # render components('ui/forms/form', - # model: Spree::Product.new, - # elements: [ - # SolidusAdmin::Form::Element::Component.new( - # component: MyCustomComponent.new - # ) - # ] - # ) - # %> - # ``` - # - # ## SolidusAdmin::Form::Element::HTML - # - # This element allows you to render any HTML inside the form. - # - # ```erb - # <%= - # render components('ui/forms/form', - # model: Spree::Product.new, - # elements: [ - # SolidusAdmin::Form::Element::HTML.new( - # html: "

Whatever HTML you want

".html_safe - # ) - # ] - # ) - # %> - # ``` - def overview - render_with_template( - locals: { - elements: elements - } - ) - end - - private - - def elements - [ - field_element, - fieldset_element, - component_element, - html_element - ] - end - - def field_element - SolidusAdmin::Form::Element::Field.new( - component: :text_field, - field: :name, - placeholder: "SolidusAdmin::Form::Element::Field", - errors: {} - ) - end - - def fieldset_element - SolidusAdmin::Form::Element::Fieldset.new( - elements: [ - SolidusAdmin::Form::Element::Field.new( - component: :text_field, - field: :name, - placeholder: "SolidusAdmin::Form::Element::Field", - errors: {} - ) - ], - legend: "SolidusAdmin::Form::Element::Fieldset" - ) - end - - def component_element - SolidusAdmin::Form::Element::Component.new( - component: Class.new(SolidusAdmin::BaseComponent) do - def self.name - "MyCustomComponent" - end - - def call - tag.p(class: "body-text-bold mb-2 italic") { "SolidusAdmin::Form::Element::Component" } - end - end.new - ) - end - - def html_element - SolidusAdmin::Form::Element::HTML.new( - html: "

SolidusAdmin::Form::Element::HTML

".html_safe - ) - end -end diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/form/component_preview/overview.html.erb b/admin/spec/components/previews/solidus_admin/ui/forms/form/component_preview/overview.html.erb deleted file mode 100644 index 40dee9a8220..00000000000 --- a/admin/spec/components/previews/solidus_admin/ui/forms/form/component_preview/overview.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -
- <%= - render current_component.new(url: "#", scope: :overview, method: :get, class: "m-auto", elements: elements) - %> -
diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/input/component_preview.rb b/admin/spec/components/previews/solidus_admin/ui/forms/input/component_preview.rb new file mode 100644 index 00000000000..fe4025467fc --- /dev/null +++ b/admin/spec/components/previews/solidus_admin/ui/forms/input/component_preview.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# @component "ui/forms/input" +class SolidusAdmin::UI::Forms::Input::ComponentPreview < ViewComponent::Preview + include SolidusAdmin::Preview + + def overview + render_with_template + end + + # @param error toggle + # @param size select { choices: [s, m, l] } + # @param value text + # @param type select :input_types + def input_playground(error: false, size: "m", value: "value", type: "text") + render component("ui/forms/input").new( + tag: :input, + type: type.to_sym, + error: error ? "There is an error" : nil, + size: size.to_sym, + value: value, + ) + end + + # @param error toggle + # @param size select { choices: [s, m, l] } + # @param multiple toggle + # @param rows number + # @param options number + # @param include_blank toggle + def select_playground(error: false, include_blank: true, options: 3, rows: 1, size: "m", multiple: false) + options = (1..options).map { |i| ["Option #{i}", i] } + options.unshift(["", ""]) if include_blank + options.map! { tag.option(_1, value: _2) } + + render component("ui/forms/input").new( + tag: :select, + "size" => rows > 1 ? rows : nil, + error: error ? "There is an error" : nil, + size: size.to_sym, + multiple: multiple, + ).with_content(options.reduce(:+)) + end + + # @param error toggle + # @param size select { choices: [s, m, l] } + # @param content textarea + def textarea_playground(error: false, size: "m", content: "value") + render component("ui/forms/input").new( + tag: :textarea, + size: size.to_sym, + error: error ? "There is an error" : nil, + ).with_content(content) + end + + private + + def input_types + current_component::TYPES.to_a + end +end diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/input/component_preview/overview.html.erb b/admin/spec/components/previews/solidus_admin/ui/forms/input/component_preview/overview.html.erb new file mode 100644 index 00000000000..33a55354b1d --- /dev/null +++ b/admin/spec/components/previews/solidus_admin/ui/forms/input/component_preview/overview.html.erb @@ -0,0 +1,28 @@ +
+

Input

+ +
+
Default
+ <%= render current_component.new(placeholder: "Placeholder") %> +
+ +
+
Filled
+ <%= render current_component.new(value: "My value") %> +
+ +
+
Error
+ <%= render current_component.new(value: "Bad value", error: "The value is wrong!") %> +
+ +
+
Disabled
+ <%= render current_component.new(placeholder: "Placeholder", disabled: true) %> +
+ +
+
Disabled filled
+ <%= render current_component.new(value: "My value", disabled: true) %> +
+
diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/select/component_preview.rb b/admin/spec/components/previews/solidus_admin/ui/forms/select/component_preview.rb deleted file mode 100644 index ce0fb2b1b1d..00000000000 --- a/admin/spec/components/previews/solidus_admin/ui/forms/select/component_preview.rb +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true - -# @component "ui/forms/select" -class SolidusAdmin::UI::Forms::Select::ComponentPreview < ViewComponent::Preview - include SolidusAdmin::Preview - - # The select component is used to render a select box in a form. - # - # See the [`ui/forms/text_field`](../text_field) component for usage - # instructions. - def overview - choices = [ - ["Option 1", "option_1"], - ["Option 2", "option_2"], - ["Option 3", "option_3"] - ] - hint = "Select one of the options" - render_with_template( - locals: { - sizes: current_component::SIZES.keys, - choices: choices, - variants: { - "with_prompt" => { - hint: nil, errors: {}, options: { prompt: "Select" }, attributes: {} - }, - "selected" => { - hint: nil, errors: {}, options: {}, attributes: {} - }, - "with_hint" => { - hint: hint, errors: {}, options: {}, attributes: {} - }, - "with_prompt_and_error" => { - hint: nil, errors: { "with_prompt_and_error" => ["can't be blank"] }, options: { prompt: "Select" }, attributes: {} - }, - "selected_with_error" => { - hint: nil, errors: { "selected_with_error" => ["is invalid"] }, options: {}, attributes: {} - }, - "with_hint_and_error" => { - hint: hint, errors: { "with_hint_and_error" => ["is invalid"] }, options: {}, attributes: {} - }, - "with_prompt_disabled" => { - hint: nil, errors: {}, options: { prompt: "Select" }, attributes: { disabled: true } - }, - "selected_disabled" => { - hint: nil, errors: {}, options: {}, attributes: { disabled: true } - }, - "with_hint_disabled" => { - hint: hint, errors: {}, options: {}, attributes: { disabled: true } - } - } - } - ) - end - - # @param size select { choices: [s, m, l] } - # @param choices text "Separate multiple choices with a comma" - # @param label text - # @param selected text - # @param hint text - # @param errors text "Separate multiple errors with a comma" - # @param prompt text - # @param disabled toggle - def playground( - size: :m, - choices: "Option 1, Option 2, Option 3", - label: "Choose:", - selected: "Option 1", - hint: nil, errors: "", - prompt: "Select", - disabled: false - ) - render_with_template( - locals: { - size: size.to_sym, - choices: choices.split(",").map(&:strip).map { [_1, _1.parameterize] }, - field: label, - selected: selected&.parameterize, - hint: hint, - errors: { label.dasherize => (errors.blank? ? [] : errors.split(",").map(&:strip)) }, - prompt: prompt, - disabled: disabled - } - ) - end -end diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/select/component_preview/overview.html.erb b/admin/spec/components/previews/solidus_admin/ui/forms/select/component_preview/overview.html.erb deleted file mode 100644 index 0742875b22d..00000000000 --- a/admin/spec/components/previews/solidus_admin/ui/forms/select/component_preview/overview.html.erb +++ /dev/null @@ -1,34 +0,0 @@ -<%= form_with(url: "#", scope: :overview, method: :get, class: "w-full") do |form| %> - - - - <% sizes.each do |size| %> - - <% end %> - - - - <% - variants.each_pair do |name, definition| %> - - <% sizes.each do |size| %> - - <% end %> - - <% end %> - -
<%= size.to_s.humanize %>
- <%= - render current_component.new( - builder: form, - field: name, - choices: choices, - size: size, - errors: definition[:errors], - hint: definition[:hint], - options: definition[:options], - attributes: definition[:attributes] - ) - %> -
-<% end %> diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/select/component_preview/playground.html.erb b/admin/spec/components/previews/solidus_admin/ui/forms/select/component_preview/playground.html.erb deleted file mode 100644 index cad37e0e02b..00000000000 --- a/admin/spec/components/previews/solidus_admin/ui/forms/select/component_preview/playground.html.erb +++ /dev/null @@ -1,19 +0,0 @@ -<%= form_with(url: "#", scope: :playground, method: :get, class: "w-60") do |form| %> - <%= - render current_component.new( - builder: form, - size: size, - choices: choices, - field: field, - hint: hint, - errors: errors, - options: { - prompt: prompt, - selected: selected - }, - attributes: { - disabled: disabled - } - ) - %> -<% end %> diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/text_area/component_preview.rb b/admin/spec/components/previews/solidus_admin/ui/forms/text_area/component_preview.rb deleted file mode 100644 index cb7ce0cc416..00000000000 --- a/admin/spec/components/previews/solidus_admin/ui/forms/text_area/component_preview.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -# @component "ui/forms/text_area" -class SolidusAdmin::UI::Forms::TextArea::ComponentPreview < ViewComponent::Preview - include SolidusAdmin::Preview - - # The text area component is used to render a textarea in a form. - # - # See the [`ui/forms/text_field`](../text_field) component for usage - # instructions. - def overview - dummy_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, urna eu aliquam ultricies, urna elit aliquam urna, eu aliquam urna elit euismod urna." - render_with_template( - locals: { - sizes: current_component::SIZES.keys, - variants: { - "empty" => { - value: nil, disabled: false, hint: nil, errors: {} - }, - "filled" => { - value: dummy_text, disabled: false, hint: nil, errors: {} - }, - "with_hint" => { - value: dummy_text, disabled: false, hint: "Max. 400 characters", errors: {} - }, - "empty_with_error" => { - value: nil, disabled: false, hint: nil, errors: { "empty_with_error" => ["can't be blank"] } - }, - "filled_with_error" => { - value: dummy_text, disabled: false, hint: nil, errors: { "filled_with_error" => ["is invalid"] } - }, - "with_hint_and_error" => { - value: dummy_text, disabled: false, hint: "Max. 400 characters", errors: { "with_hint_and_error" => ["is invalid"] } - }, - "empty_disabled" => { - value: nil, disabled: true, hint: nil, errors: {} - }, - "filled_disabled" => { - value: dummy_text, disabled: true, hint: nil, errors: {} - }, - "with_hint_disabled" => { - value: dummy_text, disabled: true, hint: "Max. 400 characters", errors: {} - } - } - } - ) - end - - # @param size select { choices: [s, m, l] } - # @param label text - # @param value text - # @param hint text - # @param errors text "Separate multiple errors with a comma" - # @param placeholder text - # @param disabled toggle - def playground(size: :m, label: "Description", value: nil, hint: nil, errors: "", placeholder: "Placeholder", disabled: false) - render_with_template( - locals: { - size: size.to_sym, - field: label, - value: value, - hint: hint, - errors: { label.dasherize => (errors.blank? ? [] : errors.split(",").map(&:strip)) }, - placeholder: placeholder, - disabled: disabled - } - ) - end -end diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/text_area/component_preview/overview.html.erb b/admin/spec/components/previews/solidus_admin/ui/forms/text_area/component_preview/overview.html.erb deleted file mode 100644 index 8ded42897d1..00000000000 --- a/admin/spec/components/previews/solidus_admin/ui/forms/text_area/component_preview/overview.html.erb +++ /dev/null @@ -1,34 +0,0 @@ -<%= form_with(url: "#", scope: :overview, method: :get, class: "w-full") do |form| %> - - - - <% sizes.each do |size| %> - - <% end %> - - - - <% - variants.each_pair do |name, definition| %> - - <% sizes.each do |size| %> - - <% end %> - - <% end %> - -
<%= size.to_s.humanize %>
- <%= - render current_component.new( - builder: form, - field: name, - size: size, - errors: definition[:errors], - hint: definition[:hint], - disabled: definition[:disabled], - placeholder: "Placeholder", - value: definition[:value] - ) - %> -
-<% end %> diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/text_area/component_preview/playground.html.erb b/admin/spec/components/previews/solidus_admin/ui/forms/text_area/component_preview/playground.html.erb deleted file mode 100644 index c12a2684850..00000000000 --- a/admin/spec/components/previews/solidus_admin/ui/forms/text_area/component_preview/playground.html.erb +++ /dev/null @@ -1,14 +0,0 @@ -<%= form_with(url: "#", scope: :playground, method: :get, class: "w-60") do |form| %> - <%= - render current_component.new( - builder: form, - size: size, - field: field, - value: value, - hint: hint, - errors: errors, - placeholder: placeholder, - disabled: disabled - ) - %> -<% end %> diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview.rb b/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview.rb deleted file mode 100644 index 72d46308dd8..00000000000 --- a/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview.rb +++ /dev/null @@ -1,105 +0,0 @@ -# frozen_string_literal: true - -# @component "ui/forms/text_field" -class SolidusAdmin::UI::Forms::TextField::ComponentPreview < ViewComponent::Preview - include SolidusAdmin::Preview - - # The text field component is used to render a text field in a form. - # - # Most commonly, it'll be used indirectly through the definition given to a - # [form component](../form/overview). - # - # For standalone usage, it must be used within the block context yielded in - # the [`form_with` - # ](https://api.rubyonrails.org/v5.1/classes/ActionView/Helpers/FormHelper.html#method-i-form_with) - # or - # [`form_for`](https://api.rubyonrails.org/v5.1/classes/ActionView/Helpers/FormHelper.html#method-i-form_for) - # helpers. - # - # When the form builder is not bound to a model instance, you must pass an - # errors Hash to the component. For example: - # - # ```erb - # <%= form_with(url: search_path, method: :get) do |form| %> - # <%= render components('ui/forms/text_field').new( - # builder: form, - # field: :q, - # errors: params[:q].present? ? {} : { - # q: ["can't be blank"] - # } - # ) %> - # <%= form.submit "Search" %> - # <% end %> - # ``` - # - # When the form builder is bound to a model instance, the component will - # automatically fetch the errors from the model. - # - # ```erb - # <%= form_with(model: @user) do |form| %> - # <%= render components('ui/forms/text_field').new( - # builder: form, - # field: :name - # ) %> - # <%= form.submit "Save" %> - # <% end %> - def overview - render_with_template( - locals: { - sizes: current_component::SIZES.keys, - variants: { - "empty" => { - value: nil, disabled: false, hint: nil, errors: {} - }, - "filled" => { - value: "Alice", disabled: false, hint: nil, errors: {} - }, - "with_hint" => { - value: "Alice", disabled: false, hint: "No special characters", errors: {} - }, - "empty_with_error" => { - value: nil, disabled: false, hint: nil, errors: { "empty_with_error" => ["can't be blank"] } - }, - "filled_with_error" => { - value: "Alice", disabled: false, hint: nil, errors: { "filled_with_error" => ["is invalid"] } - }, - "with_hint_and_error" => { - value: "Alice", disabled: false, hint: "No special characters", errors: { "with_hint_and_error" => ["is invalid"] } - }, - "empty_disabled" => { - value: nil, disabled: true, hint: nil, errors: {} - }, - "filled_disabled" => { - value: "Alice", disabled: true, hint: nil, errors: {} - }, - "with_hint_disabled" => { - value: "Alice", disabled: true, hint: "No special characters", errors: {} - } - } - } - ) - end - - # @param size select { choices: [s, m, l] } - # @param type select { choices: [color, date, datetime, email, month, number, password, phone, range, search, text, time, url, week] } - # @param label text - # @param value text - # @param hint text - # @param errors text "Separate multiple errors with a comma" - # @param placeholder text - # @param disabled toggle - def playground(size: :m, type: :text, label: "Name", value: nil, hint: nil, errors: "", placeholder: "Placeholder", disabled: false) - render_with_template( - locals: { - size: size.to_sym, - type: type.to_sym, - field: label, - value: value, - hint: hint, - errors: { label.dasherize => (errors.blank? ? [] : errors.split(",").map(&:strip)) }, - placeholder: placeholder, - disabled: disabled - } - ) - end -end diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview/overview.html.erb b/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview/overview.html.erb deleted file mode 100644 index 200f131e93b..00000000000 --- a/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview/overview.html.erb +++ /dev/null @@ -1,34 +0,0 @@ -<%= form_with(url: "#", scope: :overview, method: :get) do |form| %> - - - - <% sizes.each do |size| %> - - <% end %> - - - - <% - variants.each_pair do |name, definition| %> - - <% sizes.each do |size| %> - - <% end %> - - <% end %> - -
<%= size.to_s.humanize %>
- <%= - render current_component.new( - builder: form, - field: name, - size: size, - errors: definition[:errors], - hint: definition[:hint], - disabled: definition[:disabled], - placeholder: "Placeholder", - value: definition[:value] - ) - %> -
-<% end %> diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview/playground.html.erb b/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview/playground.html.erb deleted file mode 100644 index 76072bee203..00000000000 --- a/admin/spec/components/previews/solidus_admin/ui/forms/text_field/component_preview/playground.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -<%= form_with(url: "#", scope: :playground, method: :get, class: "w-56") do |form| %> - <%= - render current_component.new( - builder: form, - size: size, - type: type, - field: field, - value: value, - hint: hint, - errors: errors, - placeholder: placeholder, - disabled: disabled - ) - %> -<% end %> diff --git a/admin/spec/components/solidus_admin/ui/forms/form/component_spec.rb b/admin/spec/components/solidus_admin/ui/forms/field/component_spec.rb similarity index 63% rename from admin/spec/components/solidus_admin/ui/forms/form/component_spec.rb rename to admin/spec/components/solidus_admin/ui/forms/field/component_spec.rb index 87d5382ba0c..e62c3020a13 100644 --- a/admin/spec/components/solidus_admin/ui/forms/form/component_spec.rb +++ b/admin/spec/components/solidus_admin/ui/forms/field/component_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -RSpec.describe SolidusAdmin::UI::Forms::Form::Component, type: :component do +RSpec.describe SolidusAdmin::UI::Forms::Field::Component, type: :component do it "renders the overview preview" do render_preview(:overview) end diff --git a/admin/spec/components/solidus_admin/ui/forms/fieldset/component_spec.rb b/admin/spec/components/solidus_admin/ui/forms/fieldset/component_spec.rb deleted file mode 100644 index cfb51bf07e9..00000000000 --- a/admin/spec/components/solidus_admin/ui/forms/fieldset/component_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe SolidusAdmin::UI::Forms::Fieldset::Component, type: :component do - it "renders the overview preview" do - render_preview(:overview) - end -end diff --git a/admin/spec/components/solidus_admin/ui/forms/guidance/component_spec.rb b/admin/spec/components/solidus_admin/ui/forms/guidance/component_spec.rb deleted file mode 100644 index f5092e16c0d..00000000000 --- a/admin/spec/components/solidus_admin/ui/forms/guidance/component_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe SolidusAdmin::UI::Forms::Guidance::Component, type: :component do - describe "#initialize" do - it "uses given errors when form is bound to a model" do - form = double("form", object: double("model", errors: {})) - - component = described_class.new(builder: form, field: :name, hint: nil, errors: { name: ["can't be blank"] }) - - expect(component.errors?).to be(true) - end - - it "uses model errors when form is bound to a model and they are not given" do - form = double("form", object: double("model", errors: { name: ["can't be blank"] })) - - component = described_class.new(builder: form, field: :name, hint: nil, errors: nil) - - expect(component.errors?).to be(true) - end - - it "uses given errors when form is not bound to a model" do - form = double("form", object: nil) - - component = described_class.new(builder: form, field: :name, hint: nil, errors: { name: ["can't be blank"] }) - - expect(component.errors?).to be(true) - end - - it "raises an error when form is not bound to a model and errors are not given" do - form = double("form", object: nil) - - expect { described_class.new(builder: form, field: :name, errors: nil) }.to raise_error(ArgumentError) - end - end -end diff --git a/admin/spec/components/solidus_admin/ui/forms/input/component_spec.rb b/admin/spec/components/solidus_admin/ui/forms/input/component_spec.rb new file mode 100644 index 00000000000..cb00d6a65cb --- /dev/null +++ b/admin/spec/components/solidus_admin/ui/forms/input/component_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe SolidusAdmin::UI::Forms::Input::Component, type: :component do + it "renders the overview preview" do + render_preview(:overview) + render_preview(:input_playground) + render_preview(:select_playground) + render_preview(:textarea_playground) + end + + it "only accepts certain 'type' attributes for the input" do + expect { + render_inline(described_class.new(type: :button)) + }.to raise_error(ArgumentError, /unsupported type attribute: button/) + end + + describe "with `tag: input`" do + it "renders a text input" do + render_inline(described_class.new(type: :text, name: "name", value: "value")) + + expect(page).to have_css("input[type='text'][name='name'][value='value']") + end + + it "renders a password input" do + render_inline(described_class.new(type: :password, name: "name", value: "value")) + + expect(page).to have_css("input[type='password'][name='name'][value='value']") + end + + it "renders a number input" do + render_inline(described_class.new(type: :number, name: "name", value: "value")) + + expect(page).to have_css("input[type='number'][name='name'][value='value']") + end + end +end diff --git a/admin/spec/components/solidus_admin/ui/forms/select/component_spec.rb b/admin/spec/components/solidus_admin/ui/forms/select/component_spec.rb deleted file mode 100644 index 1cc630faec1..00000000000 --- a/admin/spec/components/solidus_admin/ui/forms/select/component_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe SolidusAdmin::UI::Forms::Select::Component, type: :component do - it "renders the overview preview" do - render_preview(:overview) - end - - it "renders the playground preview" do - render_preview(:playground) - end -end diff --git a/admin/spec/components/solidus_admin/ui/forms/text_area/component_spec.rb b/admin/spec/components/solidus_admin/ui/forms/text_area/component_spec.rb deleted file mode 100644 index af5e6f1bf09..00000000000 --- a/admin/spec/components/solidus_admin/ui/forms/text_area/component_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe SolidusAdmin::UI::Forms::TextArea::Component, type: :component do - it "renders the overview preview" do - render_preview(:overview) - end - - it "renders the playground preview" do - render_preview(:playground) - end -end diff --git a/admin/spec/components/solidus_admin/ui/forms/text_field/component_spec.rb b/admin/spec/components/solidus_admin/ui/forms/text_field/component_spec.rb deleted file mode 100644 index 2d89dabfe54..00000000000 --- a/admin/spec/components/solidus_admin/ui/forms/text_field/component_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe SolidusAdmin::UI::Forms::TextField::Component, type: :component do - it "renders the overview preview" do - render_preview(:overview) - end - - it "renders the playground preview" do - render_preview(:playground) - end -end diff --git a/admin/spec/solidus_admin/form/element/component_spec.rb b/admin/spec/solidus_admin/form/element/component_spec.rb deleted file mode 100644 index eda06035b50..00000000000 --- a/admin/spec/solidus_admin/form/element/component_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" -require "solidus_admin/form/element/component" - -RSpec.describe SolidusAdmin::Form::Element::Component do - describe "#call" do - it "returns the given instance component" do - element = described_class.new(component: :component) - - expect( - element.call(double("form"), double("builder")) - ).to be(:component) - end - end -end diff --git a/admin/spec/solidus_admin/form/element/field_spec.rb b/admin/spec/solidus_admin/form/element/field_spec.rb deleted file mode 100644 index 131ab0251cc..00000000000 --- a/admin/spec/solidus_admin/form/element/field_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" -require "solidus_admin/form/element/field" - -RSpec.describe SolidusAdmin::Form::Element::Field do - include SolidusAdmin::ComponentHelpers - - describe "#call" do - it "returns an instance of the given component" do - component = mock_component do - def initialize(builder:); end - end - builder = double("builder") - - element = described_class.new(component: component) - - expect( - element.call(double("form"), builder) - ).to be_a(component) - end - - it "initializes the component with the given attributes" do - component = mock_component do - attr_reader :builder, :attributes - - def initialize(builder:, **attributes) - @builder = builder - @attributes = attributes - end - end - attributes = { foo: :bar } - element = described_class.new(component: component, **attributes) - - result = element.call(double("form"), double("builder")) - - expect(result.attributes).to eq(attributes) - end - - it "initializes the component with the given builder" do - component = mock_component do - attr_reader :builder - - def initialize(builder:) - @builder = builder - end - end - builder = double("builder") - element = described_class.new(component: component) - - result = element.call(double("form"), builder) - - expect(result.builder).to be(builder) - end - - it "infers the component class from the form dependencies when given as a Symbol" do - text_field_component_class = mock_component do - def initialize(builder:); end - end - element = described_class.new(component: :text_field) - component = SolidusAdmin::UI::Forms::Form::Component.new(elements: [element]) - - allow(component).to receive(:component).and_call_original - allow(component).to receive(:component).with('ui/forms/text_field').and_return(text_field_component_class) - - result = element.call(component, double("builder")) - - expect(result).to be_a(text_field_component_class) - end - end -end diff --git a/admin/spec/solidus_admin/form/element/fieldset_spec.rb b/admin/spec/solidus_admin/form/element/fieldset_spec.rb deleted file mode 100644 index c7938db84aa..00000000000 --- a/admin/spec/solidus_admin/form/element/fieldset_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" -require "solidus_admin/form/element/fieldset" -require "solidus_admin/form/element/html" - -RSpec.describe SolidusAdmin::Form::Element::Fieldset do - include SolidusAdmin::ComponentHelpers - - describe "#call" do - it "returns an instance of the given component" do - component = mock_component - element = described_class.new(component: component, elements: []) - - expect( - element.call(double("form"), double("builder")) - ).to be_a(component) - end - - it "initializes the component with the given attributes" do - component = mock_component do - attr_reader :attributes - - def initialize(**attributes) - @attributes = attributes - end - end - attributes = { foo: :bar } - element = described_class.new(component: component, elements: [], **attributes) - - result = element.call(double("form"), double("builder")) - - expect(result.attributes).to eq(attributes) - end - - it "gives the concatenation of the rendered elements as the content of the component" do - component = mock_component - elements = [ - SolidusAdmin::Form::Element::HTML.new(html: "foo"), - SolidusAdmin::Form::Element::HTML.new(html: "bar") - ] - element = described_class.new(component: component, elements: elements) - form = SolidusAdmin::UI::Forms::Form::Component.new(elements: [element]) - - # Workaround for view_context not being available in specs - expect(form).to receive(:render_element).with(elements[0], any_args).and_return("foo") - expect(form).to receive(:render_element).with(elements[1], any_args).and_return("bar") - - result = element.call(form, double("builder")) - - expect(result.content).to eq("foobar") - end - end -end diff --git a/admin/spec/solidus_admin/form/element/html_spec.rb b/admin/spec/solidus_admin/form/element/html_spec.rb deleted file mode 100644 index e6c0a7de0e8..00000000000 --- a/admin/spec/solidus_admin/form/element/html_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" -require "solidus_admin/form/element/html" - -RSpec.describe SolidusAdmin::Form::Element::HTML do - describe "#call" do - it "returns itself" do - element = described_class.new(html: "foo") - - expect( - element.call(double("form"), double("builder")) - ).to be(element) - end - end - - describe "#render_in" do - it "returns the given HTML" do - element = described_class.new(html: "foo") - - expect( - element.render_in(double("view_context")) - ).to eq("foo") - end - end -end