Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,7 @@ export default class extends Controller {
option.innerText = state.name
stateSelect.appendChild(option)
})

stateSelect.setAttribute("synced", "false");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
<div class="
font-normal text-xs
[:disabled~&]:text-gray-300 text-gray-500
flex gap-1 flex-col
flex gap-1 flex-col [&>.error]:hidden [&>.error]:peer-invalid:block
">
<%= tag.span @hint if @hint.present? %>
<%= tag.span safe_join(@error, tag.br), class: "text-red-600" if @error.present? %>
<%= tag.span safe_join(@error, tag.br), class: "error text-red-600" if @error.present? %>
</div>
<% end %>
</label>
18 changes: 7 additions & 11 deletions admin/app/components/solidus_admin/ui/forms/field/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,16 @@ def self.text_field(form, method, object: nil, hint: nil, tip: nil, size: :m, **
def self.select(form, method, choices, object: nil, hint: nil, tip: nil, size: :m, **attributes)
object_name, object, label, errors = extract_form_details(form, object, method)

new(
component("ui/forms/select").new(
label:,
hint:,
tip:,
error: errors,
input_attributes: {
name: "#{object_name}[#{method}]",
tag: :select,
choices:,
size:,
value: (object.public_send(method) if object.respond_to?(method)),
error: (errors.to_sentence.capitalize if errors),
**attributes,
}
size: size,
name: "#{object_name}[#{method}]",
choices:,
value: (object.public_send(method) if object.respond_to?(method)),
error: (errors.to_sentence.capitalize if errors),
**attributes
)
end

Expand Down
15 changes: 0 additions & 15 deletions admin/app/components/solidus_admin/ui/forms/input/component.js

This file was deleted.

22 changes: 5 additions & 17 deletions admin/app/components/solidus_admin/ui/forms/input/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class SolidusAdmin::UI::Forms::Input::Component < SolidusAdmin::BaseComponent
]).freeze

def initialize(tag: :input, size: :m, error: nil, **attributes)
raise ArgumentError, "unsupported tag: #{tag}" unless %i[input textarea select].include?(tag)
raise ArgumentError, "unsupported tag: #{tag}" unless %i[input textarea].include?(tag)

specialized_classes = []
readonly_classes = "read-only:bg-gray-15 focus:read-only:bg-gray-15 focus:read-only:ring-0
Expand All @@ -56,20 +56,11 @@ def initialize(tag: :input, size: :m, error: nil, **attributes)
specialized_classes << "form-textarea"
specialized_classes << readonly_classes
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
peer 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
Expand All @@ -89,9 +80,7 @@ def initialize(tag: :input, size: :m, error: nil, **attributes)
end

def call
if @tag == :select && @attributes[:choices]
with_content options_for_select(@attributes.delete(:choices), @attributes.delete(:value))
elsif @tag == :textarea && @attributes[:value]
if @tag == :textarea && @attributes[:value]
with_content @attributes.delete(:value)
end

Expand All @@ -109,9 +98,8 @@ def build_tag

def tag_options
@tag_options ||= {
"data-controller": stimulus_id,
"data-#{stimulus_id}-custom-validity-value": @error.presence,
"data-action": "#{stimulus_id}#clearCustomValidity",
"data-controller": "custom-validity",
"data-custom-validity-error-message-value": @error.presence,
**@attributes
}
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<div class="flex flex-col gap-2 w-full">
<div class="flex gap-1 items-center">
<label class="text-gray-700 font-semibold text-xs" <%= tag.attributes for: @attributes[:id] %>>
<%= @label %>
</label>
<%= render component('ui/toggletip').new(text: @tip) if @tip.present? %>
</div>

<%= tag.select(options_for_select(@choices, @selected), **@attributes) %>

<% if @hint.present? || @error.present? %>
<div class="
font-normal text-xs
[:disabled~&]:text-gray-300 text-gray-500
flex gap-1 flex-col [&>.error]:hidden [&>.error]:peer-invalid:block
">
<%= tag.span @hint if @hint.present? %>
<%= tag.span safe_join(@error, tag.br), class: "error text-red-600" %>
</div>
<% end %>
</div>
98 changes: 98 additions & 0 deletions admin/app/components/solidus_admin/ui/forms/select/component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# frozen_string_literal: true

class SolidusAdmin::UI::Forms::Select::Component < SolidusAdmin::BaseComponent
FONT_SIZES = {
s: "[&>.control]:text-xs [&_.dropdown]:text-xs",
m: "[&>.control]:text-sm [&_.dropdown]:text-sm",
l: "[&>.control]:text-base [&_.dropdown]:text-base",
}.freeze

HEIGHTS = {
control: {
s: "[&>.control]:min-h-7",
m: "[&>.control]:min-h-9",
l: "[&>.control]:min-h-12",
},
option: {
s: "[&_.option]:h-7",
m: "[&_.option]:h-9",
l: "[&_.option]:h-12",
},
item: {
s: "[&_.item]:h-5",
m: "[&_.item]:h-5.5",
l: "[&_.item]:h-8",
},
}.freeze

def initialize(label:, name:, choices:, size: :m, hint: nil, tip: nil, error: nil, **attributes)
@label = label
@name = name
@hint = hint
@tip = tip
@error = Array.wrap(error)

@choices = choices
@selected = attributes.delete(:value)

@attributes = attributes
@attributes[:name] = @name
@attributes[:is] = "solidus-select"
@attributes[:id] ||= "#{stimulus_id}_#{@name}"
@attributes[:"data-error-message"] = @error.presence

general_classes = ["w-full relative text-black font-normal #{FONT_SIZES[size]}"]
control_classes = ["[&>.control]:peer-invalid:border-red-600 [&>.control]:peer-invalid:hover:border-red-600
[&>.control]:peer-invalid:text-red-600 [&>.control]:flex [&>.control]:flex-wrap [&>.control]:items-center
[&>.control]:gap-1 [&>.control]:rounded-sm [&>.control]:w-full [&>.control]:rounded-sm [&>.control]:pl-3
[&>.control]:pr-10 [&>.control]:py-1.5 [&>.control]:bg-white [&>.control]:border [&>.control]:border-gray-300
[&>.control]:hover:border-gray-500 [&>.control]:has-[:disabled]:bg-gray-50 [&>.control]:has-[:disabled]:text-gray-500
[&>.control]:has-[:disabled]:cursor-not-allowed [&>.control]:has-[:disabled]:hover:border-gray-300
[&>.control]:has-[:focus]:ring [&>.control]:has-[:focus]:ring-gray-300 [&>.control]:has-[:focus]:ring-0.5
[&>.control]:has-[:focus]:bg-white [&>.control]:has-[:focus]:ring-offset-0 [&>.control]:has-[:focus]:outline-none
#{HEIGHTS[:control][size]}"]

unless @attributes[:multiple]
control_classes << "[&>.control]:peer-invalid:bg-arrow-down-s-fill-red-400 [&>.control]:form-select
[&>.control]:bg-arrow-down-s-fill-gray-700"
end

item_classes = []
if @attributes[:multiple]
item_classes << "[&_.item]:flex [&_.item]:gap-1 [&_.item]:items-center [&_.item]:rounded-full [&_.item]:whitespace-nowrap
[&_.item]:px-2 [&_.item]:py-0.5 [&_.item]:bg-graphite-light [&_.item]:peer-invalid:bg-red-100 #{HEIGHTS[:item][size]}
[&_.item_.remove-button]:text-xl [&_.item_.remove-button]:pb-0.5 [&_.item_.remove-button]:order-first
[&_.item_.remove-button]:has-[:disabled]:cursor-not-allowed"
end

input_classes = ["[&_input]:has-[.item]:placeholder:invisible [&_input:disabled]:cursor-not-allowed [&_input:disabled]:bg-gray-50
[&_input]:peer-invalid:placeholder:text-red-400"]

unless @attributes[:multiple]
input_classes << "[&_input]:has-[.item]:opacity-0 [&_input]:has-[.item]:cursor-default"
end

dropdown_classes = ["[&_.dropdown]:w-full [&_.dropdown]:absolute [&_.dropdown]:border [&_.dropdown]:border-gray-100
[&_.dropdown]:mt-0.5 [&_.dropdown]:min-w-[10rem] [&_.dropdown]:p-2 [&_.dropdown]:rounded-sm [&_.dropdown]:z-10
[&_.dropdown]:shadow-lg [&_.dropdown]:bg-white"]

dropdown_content_classes = ["[&_.dropdown-content]:flex [&_.dropdown-content]:flex-col [&_.dropdown-content]:max-h-[200px]
[&_.dropdown-content]:overflow-x-hidden [&_.dropdown-content]:overflow-y-auto [&_.dropdown-content]:scroll-smooth [&_.no-results]:text-gray-500"]

option_classes = ["[&_.option]:p-2 [&_.option]:rounded-sm [&_.option]:min-w-fit [&_.option.active]:bg-gray-50
[&_.option.active]:text-gray-700 [&_.option_.highlight]:bg-yellow [&_.option_.highlight]:rounded-[1px]
#{HEIGHTS[:option][size]}"]

@attributes[:class] = [
"peer",
general_classes,
control_classes,
item_classes,
input_classes,
dropdown_classes,
dropdown_content_classes,
option_classes,
@attributes[:class]
].compact.join(" ")
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
</form>
</header>

<div class="p-4 overflow-auto">
<div class="p-4">
<%= content %>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def ensure_store_credit_reason
@store_credit_reason = Spree::StoreCreditReason.find_by(id: permitted_resource_params[:store_credit_reason_id])

if @store_credit_reason.blank?
@store_credit.errors.add(:store_credit_reason_id, "Store Credit reason must be provided")
@store_credit.errors.add(:store_credit_reason_id, "Store credit reason must be provided")
yield if block_given? # Block is for error template rendering on a per-action basis so this can be re-used.
return false
end
Expand All @@ -213,7 +213,7 @@ def ensure_store_credit_category
@store_credit_category = Spree::StoreCreditCategory.find_by(id: permitted_resource_params[:category_id])

if @store_credit_category.blank?
@store_credit.errors.add(:category_id, "Store Credit category must be provided")
@store_credit.errors.add(:category_id, "Store credit category must be provided")
yield if block_given? # Block is for error template rendering on a per-action basis so this can be re-used.
return false
end
Expand Down
2 changes: 2 additions & 0 deletions admin/app/javascript/solidus_admin/application.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
import "@hotwired/turbo-rails"
import "vendor/custom_elements"
import "solidus_admin/controllers"
import "solidus_admin/web_components/solidus_select"
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Controller } from "@hotwired/stimulus"
import { setValidity } from "solidus_admin/utils";

export default class extends Controller {
static values = {
errorMessage: String,
}

connect() {
setValidity(this.element, this.errorMessageValue);
}
}
24 changes: 24 additions & 0 deletions admin/app/javascript/solidus_admin/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,27 @@ export const debounce = (func, wait) => {
timeout = setTimeout(func, wait)
}
}

export const setValidity = (element, error) => {
if (!error) return;

element.setCustomValidity(error);

const clearValidity = () => {
element.setCustomValidity("");
}

let clearOn;

switch (element.tagName) {
case "INPUT":
case "TEXTAREA":
clearOn = "input";
break;
case "SELECT":
clearOn = "change";
break;
}

element.addEventListener(clearOn, clearValidity, { once: true });
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import TomSelect from "tom-select";
import { setValidity } from "solidus_admin/utils";

class SolidusSelect extends HTMLSelectElement {
static observedAttributes = ["synced"];

connectedCallback() {
const originalSelect = this;

const tomselect = new TomSelect(originalSelect, {
controlClass: "control",
dropdownClass: "dropdown",
dropdownContentClass: "dropdown-content",
optionClass: "option",
wrapperClass: "wrapper",
maxOptions: null,
refreshThrottle: 0,
plugins: {
no_active_items: true,
remove_button: {
append: originalSelect.multiple,
className: "remove-button"
},
},
onItemAdd: function() {
this.setTextboxValue("");
if (originalSelect.multiple) this.refreshOptions();
},
onType: function() {
if (!originalSelect.multiple && !this.currentResults.items.length) {
this.setTextboxValue("");
this.refreshOptions();
}
},
});

originalSelect.setAttribute("synced", "true");

// set default style for inner input field
tomselect.control_input.style =
"flex: 1 1 auto;\n" +
"line-height: inherit !important;\n" +
"max-height: none !important;\n" +
"max-width: 100% !important;\n" +
"min-height: 0 !important;\n" +
"min-width: 7rem;\n" +
"outline: none !important;"

originalSelect.setAttribute("hidden", "true");
originalSelect.setAttribute("aria-hidden", "true");

setValidity(originalSelect, originalSelect.dataset.errorMessage);
}

attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case "synced":
if (newValue === "false") {
const keepNone = function() { return false; }
this.tomselect.clearOptions(keepNone);
this.tomselect.sync();
this.setAttribute("synced", "true");
}
break;
}
}
}

customElements.define("solidus-select", SolidusSelect, { extends: "select" });
Loading
Loading