mirror of
https://github.com/we-promise/sure.git
synced 2026-09-06 07:11:14 +00:00
* feat: create merchant inline from transaction detail Add a searchable "create or select" merchant combobox (DS::MerchantSelect, mirroring the existing DS::TagSelect pattern) so a new FamilyMerchant can be created directly from the transaction detail and new-transaction forms, instead of requiring a trip to Settings > Merchants first. FamilyMerchantsController#create now also responds to JSON so the combobox can create-and-select a merchant without a full page reload. * fix: address PR review feedback on merchant inline creation - Handle Turbo validation failures in FamilyMerchantsController#create (missing format.turbo_stream branch raised ActionController::UnknownFormat) - Guard connect() so disabled merchant selectors (no menu target) don't throw - Select the exact match (or block form submission) on Enter when the create row is hidden - Catch network/parse failures in createMerchant and show an error - Localize the fallback "could not create merchant" error message - Move the create button/error text outside the listbox and add aria-controls for accessibility - Align create-option avatar size with the selected-value display Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: keep created merchant option inside the listbox, move logo URL logic to component - Add a dedicated listbox target and insert newly created merchant options inside it (previously landed outside role="listbox" after the create button was moved out in the prior review-fix commit) - Move selected-merchant logo URL transformation out of the template into DS::MerchantSelect#selected_merchant_logo_url Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: address maintainer review feedback on merchant inline creation - Drop the explicit format.turbo_stream branch in the merchant creation failure path: respond_to's turbo_stream matching forces the response Content-Type to text/vnd.turbo-stream.html before the block runs, so render :new, formats: [:html] only changed template lookup, not the Content-Type. Turbo's client then received a turbo-stream response with no <turbo-stream> tags and silently did nothing. Leaving turbo_stream undeclared lets Rails negotiate down to format.html, which renders :new with the correct text/html type. - Add truncate/shrink-0 classes to the merchant option partial's name and avatar spans so they match the selected-value display once updateSelectionDisplay clones them into the trigger button. - Add a regression test simulating a Turbo form submission (Accept: turbo-stream, text/html) against a duplicate merchant name. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Gerald <248542187+gfr-free@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
102 lines
4.9 KiB
ERB
102 lines
4.9 KiB
ERB
<div class="relative"
|
|
data-controller="merchant-select"
|
|
data-merchant-select-create-url-value="<%= helpers.family_merchants_path(format: :json) %>"
|
|
data-merchant-select-field-name-value="<%= field_name %>"
|
|
data-merchant-select-disabled-value="<%= disabled %>"
|
|
data-merchant-select-auto-submit-value="<%= auto_submit %>"
|
|
data-merchant-select-menu-placement-value="<%= menu_placement %>"
|
|
data-merchant-select-error-message-value="<%= helpers.t("transactions.form.create_merchant_error") %>"
|
|
data-action="click@window->merchant-select#handleOutsideClick keydown->merchant-select#handleKeydown">
|
|
<div class="form-field">
|
|
<div class="form-field__body">
|
|
<%= form.label method, label, class: "form-field__label" if label.present? %>
|
|
|
|
<button type="button"
|
|
class="form-field__input w-full flex items-center gap-2 <%= "cursor-not-allowed text-subdued" if disabled %>"
|
|
data-merchant-select-target="button"
|
|
data-action="click->merchant-select#toggle"
|
|
aria-haspopup="listbox"
|
|
aria-expanded="false"
|
|
aria-controls="<%= menu_id %>"
|
|
<%= "disabled" if disabled %>>
|
|
<span data-merchant-select-target="selectionContainer" class="flex items-center gap-2 truncate">
|
|
<% if selected_merchant %>
|
|
<% if selected_merchant_logo_url %>
|
|
<%= image_tag selected_merchant_logo_url, class: "w-5 h-5 rounded-full border border-secondary shrink-0", loading: "lazy" %>
|
|
<% else %>
|
|
<%= render DS::FilledIcon.new(variant: :text, text: selected_merchant.name, size: "sm", rounded: true) %>
|
|
<% end %>
|
|
<span class="truncate"><%= selected_merchant.name %></span>
|
|
<% else %>
|
|
<span class="text-secondary"><%= include_blank %></span>
|
|
<% end %>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<input type="hidden"
|
|
name="<%= field_name %>"
|
|
value="<%= selected_id %>"
|
|
data-merchant-select-target="hiddenInput"
|
|
<%= "disabled" if disabled %>>
|
|
|
|
<% unless disabled %>
|
|
<div class="absolute z-50 p-1.5 w-full min-w-32 rounded-lg shadow-lg shadow-border-xs bg-container mt-1.5 transition duration-150 ease-out -translate-y-1 opacity-0 hidden"
|
|
data-merchant-select-target="menu">
|
|
<div class="flex items-center bg-container border border-secondary rounded-lg mb-1 focus-ring-within transition-shadow">
|
|
<%= render DS::SearchInput.new(
|
|
variant: :embedded,
|
|
class: "flex-1 min-w-0",
|
|
placeholder: helpers.t("transactions.form.merchant_search_placeholder"),
|
|
data: {
|
|
merchant_select_target: "search",
|
|
action: "input->merchant-select#filter keydown->merchant-select#handleSearchKeydown"
|
|
}
|
|
) %>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-0.5 max-h-64 overflow-auto"
|
|
id="<%= menu_id %>"
|
|
role="listbox"
|
|
tabindex="-1"
|
|
data-merchant-select-target="listbox">
|
|
<% if include_blank.present? %>
|
|
<button type="button"
|
|
class="filterable-item text-primary text-sm cursor-pointer flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-container-inset-hover <%= "bg-container-inset" if selected_id.blank? %>"
|
|
role="option"
|
|
aria-selected="<%= selected_id.blank? %>"
|
|
tabindex="-1"
|
|
data-action="click->merchant-select#selectOption"
|
|
data-merchant-select-target="option"
|
|
data-merchant-id=""
|
|
data-merchant-name=""
|
|
data-filter-name="<%= include_blank %>">
|
|
<span class="check-icon w-5 shrink-0 <%= "hidden" unless selected_id.blank? %>">
|
|
<%= helpers.icon("check") %>
|
|
</span>
|
|
<span class="text-secondary"><%= include_blank %></span>
|
|
</button>
|
|
<% end %>
|
|
|
|
<% merchants.each do |merchant| %>
|
|
<% selected = selected_id.present? && merchant.id.to_s == selected_id %>
|
|
<%= render partial: "DS/merchant_select/option", locals: { merchant: merchant, selected: selected, view_helpers: helpers } %>
|
|
<% end %>
|
|
</div>
|
|
|
|
<button type="button"
|
|
class="hidden text-primary text-sm cursor-pointer items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-container-inset-hover"
|
|
data-merchant-select-target="createForm"
|
|
data-action="click->merchant-select#createMerchant">
|
|
<%= helpers.icon("plus") %>
|
|
<span><%= helpers.t("transactions.form.create_merchant") %> "<span data-merchant-select-create-name></span>"</span>
|
|
</button>
|
|
<p class="hidden px-3 py-1 text-xs text-destructive"
|
|
data-merchant-select-target="createError"
|
|
aria-live="assertive"
|
|
aria-atomic="true"></p>
|
|
</div>
|
|
<% end %>
|
|
</div>
|