Skip to content

Commit

Permalink
Extract a ui/search_panel component from orders/cart
Browse files Browse the repository at this point in the history
  • Loading branch information
elia committed Oct 31, 2023
1 parent b0f7533 commit 7c3c939
Show file tree
Hide file tree
Showing 14 changed files with 346 additions and 261 deletions.
175 changes: 67 additions & 108 deletions admin/app/components/solidus_admin/orders/cart/component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,118 +3,77 @@
data-controller="<%= stimulus_id %>"
data-<%= stimulus_id %>-products-url-value="<%= solidus_admin.variants_for_order_path(@order) %>"
data-action="
keydown-><%= stimulus_id %>#selectResult
<%= component('ui/search_panel').stimulus_id %>:search-><%= stimulus_id %>#search
<%= component('ui/search_panel').stimulus_id %>:submit-><%= stimulus_id %>#selectResult
"
data-<%= stimulus_id %>-loading-text-value="<%= t('.loading') %>"
data-<%= stimulus_id %>-initial-text-value="<%= t('.initial') %>"
data-<%= stimulus_id %>-empty-text-value="<%= t('.empty') %>"
>
<%= render component('ui/panel').new(title: t('.title')) do |panel| %>
<div class="border-gray-100 border-t -mx-6"></div>
<div>
<div class="peer">
<%= render component("ui/forms/search_field").new(
"data-action":
"#{stimulus_id}#search focus->#{stimulus_id}#showResults #{stimulus_id}#showResults",
placeholder: t(".search_placeholder"),
"data-#{stimulus_id}-target": "searchField"
) %>
</div>

<%# results popover %>
<details class="px-6 relative overflow-visible">
<summary class="hidden"></summary>
<div
class="
absolute
left-0
top-2
bg-white
z-30
w-full
rounded-lg
shadow
border
border-gray-100
p-2
flex-col
gap-1
max-h-screen
overflow-y-auto
"
data-<%= stimulus_id %>-target="results"
>
</div>
</details>

</div>

<%# line items table %>
<div class="rounded-b-lg -mx-6 -mb-6 overflow-hidden">
<table class="table-auto w-full">
<thead>
<%= render component('ui/search_panel').new(
title: t('.title'),
search_placeholder: t('.search_placeholder'),
id: :order_cart,
) do |panel| %>
<table class="table-auto w-full" <%= :hidden if @order.line_items.empty? %>>
<thead>
<tr class="border-gray-100 border-t">
<th class="text-left body-small-bold text-gray-800 bg-gray-15 px-6 py-3 leading-none">Product</th>
<th class="text-left body-small-bold text-gray-800 bg-gray-15 px-6 py-3 leading-none w-16">Quantity</th>
<th class="text-left body-small-bold text-gray-800 bg-gray-15 px-6 py-3 leading-none w-16">Price</th>
<th class="text-left body-small-bold text-gray-800 bg-gray-15 px-6 py-3 leading-none w-16"><span class="sr-only">Actions</span></th>
</tr>
</thead>
<tbody>
<% @order.line_items.each do |line_item| %>
<tr class="border-gray-100 border-t">
<th class="text-left body-small-bold text-gray-800 bg-gray-15 px-6 py-3 leading-none">Product</th>
<th class="text-left body-small-bold text-gray-800 bg-gray-15 px-6 py-3 leading-none w-16">Quantity</th>
<th class="text-left body-small-bold text-gray-800 bg-gray-15 px-6 py-3 leading-none w-16">Price</th>
<th class="text-left body-small-bold text-gray-800 bg-gray-15 px-6 py-3 leading-none w-16"><span class="sr-only">Actions</span></th>
</tr>
</thead>
<tbody>
<% @order.line_items.each do |line_item| %>
<tr class="border-gray-100 border-t">
<td class="px-6 py-4">
<div class="flex gap-2 grow">
<% variant = line_item.variant %>
<%= render component("ui/thumbnail").new(
src: (variant.images.first || variant.product.gallery.images.first)&.url(:small),
alt: variant.name
) %>
<div class="flex-col">
<div class="leading-5 text-black body-small-bold"><%= variant.name %></div>
<div class="leading-5 text-gray-500 body-small">
SKU: <%= variant.sku %>
<%= variant.options_text.presence&.prepend("- ") %>
</div>
<td class="px-6 py-4">
<div class="flex gap-2 grow">
<% variant = line_item.variant %>
<%= render component("ui/thumbnail").new(
src: (variant.images.first || variant.product.gallery.images.first)&.url(:small),
alt: variant.name
) %>
<div class="flex-col">
<div class="leading-5 text-black body-small-bold"><%= variant.name %></div>
<div class="leading-5 text-gray-500 body-small">
SKU: <%= variant.sku %>
<%= variant.options_text.presence&.prepend("- ") %>
</div>
</div>
</td>
<td class="px-6 py-4">
<%= form_for(line_item, url: solidus_admin.order_line_item_path(@order, line_item), html: {
"data-controller": "readonly-when-submitting"
}) do |f| %>
<%= render component("ui/forms/input").new(
name: "#{f.object_name}[quantity]",
type: :number,
value: line_item.quantity,
"aria-label": "Quantity",
min: 0,
class: "!w-16 inline-block",
"data-action": "input->#{stimulus_id}#updateLineItem",
) %>
<% render component("ui/button").new(type: :submit, text: "Update", class: "inline-block") %>
<% end %>
</td>
<td class="px-6 py-4">
<span class="text-gray-500 body-small"><%= line_item.single_money.to_html %></span>
</td>
<td class="px-6 py-4 text-right">
<%= form_for(line_item, url: solidus_admin.order_line_item_path(@order, line_item), method: :delete) do |f| %>
<%= render component('ui/button').new(
scheme: :ghost,
size: :s,
title: t("spree.delete"),
icon: 'close-line',
"data-controller": "confirm",
"data-confirm-text-value": t("spree.are_you_sure"),
) %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>

</div>
</td>
<td class="px-6 py-4">
<%= form_for(line_item, url: solidus_admin.order_line_item_path(@order, line_item), html: {
"data-controller": "readonly-when-submitting"
}) do |f| %>
<%= render component("ui/forms/input").new(
name: "#{f.object_name}[quantity]",
type: :number,
value: line_item.quantity,
"aria-label": "Quantity",
min: 0,
class: "!w-16 inline-block",
"data-action": "input->#{stimulus_id}#updateLineItem",
) %>
<% render component("ui/button").new(type: :submit, text: "Update", class: "inline-block") %>
<% end %>
</td>
<td class="px-6 py-4">
<span class="text-gray-500 body-small"><%= line_item.single_money.to_html %></span>
</td>
<td class="px-6 py-4 text-right">
<%= form_for(line_item, url: solidus_admin.order_line_item_path(@order, line_item), method: :delete) do |f| %>
<%= render component('ui/button').new(
scheme: :ghost,
size: :s,
title: t("spree.delete"),
icon: 'close-line',
"data-controller": "confirm",
"data-confirm-text-value": t("spree.are_you_sure"),
) %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
</div>
124 changes: 13 additions & 111 deletions admin/app/components/solidus_admin/orders/cart/component.js
Original file line number Diff line number Diff line change
@@ -1,135 +1,37 @@
import { Controller } from "@hotwired/stimulus"
import { useClickOutside, useDebounce } from "stimulus-use"

const QUERY_KEY = "q[name_or_variants_including_master_sku_cont]"
import { useDebounce } from "stimulus-use"

export default class extends Controller {
static targets = ["result", "results", "searchField"]
static values = {
results: String,
productsUrl: String,
loadingText: String,
initialText: String,
emptyText: String,
}
static debounces = [
{
name: "requestSubmitForLineItems",
wait: 500,
},
"search",
]

get query() {
return this.searchFieldTarget.value
}

get selectedResult() {
// Keep the index within boundaries
if (this.selectedIndex < 0) this.selectedIndex = 0
if (this.selectedIndex >= this.resultTargets.length) this.selectedIndex = this.resultTargets.length - 1

return this.resultTargets[this.selectedIndex]
}
static values = { productsUrl: String }
static debounces = [{ name: "submitLineItems", wait: 500 }]

connect() {
useClickOutside(this)
useDebounce(this)

this.selectedIndex = 0
this.lineItemsToBeSubmitted = []

if (this.query) {
this.showResults()
this.search()
}
}

selectResult(event) {
switch (event.key) {
case "Enter":
event.preventDefault()
this.selectedResult?.click()
break
case "ArrowUp":
event.preventDefault()
this.selectedIndex -= 1
this.render()
break
case "ArrowDown":
event.preventDefault()
this.selectedIndex += 1
this.render()
break
}
}

clickOutside() {
this.openResults = false
this.render()
}

async search() {
const query = this.query

if (query) {
this.resultsValue = this.loadingTextValue
this.render()

this.resultsValue = (await (await fetch(`${this.productsUrlValue}?${QUERY_KEY}=${query}`)).text()) || this.emptyTextValue
this.render()
} else {
this.resultsValue = this.initialTextValue
this.render()
}
}

showResults() {
this.openResults = true
this.render()
async search({ detail: { query, controller } }) {
controller.resultsValue = await (
await fetch(`${this.productsUrlValue}?q[name_or_variants_including_master_sku_cont]=${query}`)
).text()
}

updateLineItem(event) {
if (!this.lineItemsToBeSubmitted.includes(event.currentTarget)) {
this.lineItemsToBeSubmitted.push(event.currentTarget)
}

this.requestSubmitForLineItems()
this.submitLineItems()
}

// This is a workaround to permit using debounce when needing to pass a parameter
requestSubmitForLineItems() {
this.lineItemsToBeSubmitted.forEach((lineItem) => {
lineItem.form.requestSubmit()
})
submitLineItems() {
this.lineItemsToBeSubmitted.forEach((lineItem) => lineItem.form.requestSubmit())
this.lineItemsToBeSubmitted = []
}

render() {
let resultsHtml = this.resultsValue

if (this.renderedHtml !== resultsHtml) {
this.renderedHtml = resultsHtml
this.resultsTarget.innerHTML = resultsHtml
}

if (this.openResults && resultsHtml && this.query) {
if (!this.resultsTarget.parentNode.open) this.selectedIndex = 0

for (const result of this.resultTargets) {
if (result === this.selectedResult) {
if (!result.hasAttribute("aria-selected") && result.scrollIntoViewIfNeeded) {
// This is a non-standard method, but it's supported by all major browsers
result.scrollIntoViewIfNeeded()
}
result.setAttribute("aria-selected", true)
} else {
result.removeAttribute("aria-selected")
}
}
this.resultsTarget.parentNode.open = true
} else {
this.resultsTarget.parentNode.open = false
}
selectResult(event) {
const form = event.detail.resultTarget.querySelector("form")
form.submit()
}
}
3 changes: 0 additions & 3 deletions admin/app/components/solidus_admin/orders/cart/component.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,3 @@
en:
title: "Cart"
search_placeholder: "Find a variant by name or SKU"
loading: "Loading..."
initial: "Type to search"
empty: "No results"
Original file line number Diff line number Diff line change
@@ -1,34 +1,26 @@
<%= form_for(@order.line_items.build(variant: @variant), url: solidus_admin.order_line_items_path(@order), method: :post, html: {
"data-controller": "readonly-when-submitting #{stimulus_id}",
"data-action": "click->#{stimulus_id}#submit",
"data-#{component('orders/cart').stimulus_id}-target": "result",
class: "
rounded
p-2
items-center
flex
hover:bg-gray-25
aria-selected:bg-gray-25
cursor-pointer
"
}) do |f| %>
<%= hidden_field_tag("#{f.object_name}[variant_id]", @variant.id) %>
<div class="flex gap-2 grow">
<%= render component("ui/thumbnail").new(
src: @image&.url(:small),
alt: @variant.name
) %>
<div class="flex-col">
<div class="leading-5 text-black body-small-bold"><%= @variant.name %></div>
<div class="leading-5 text-gray-500 body-small">
SKU:
<%= @variant.sku %>
<%= @variant.options_text.presence&.prepend("- ") %>
<%= render component('ui/search_panel/result').new do %>
<%= form_for(@order.line_items.build(variant: @variant), url: solidus_admin.order_line_items_path(@order), method: :post, html: {
"data-controller": "readonly-when-submitting",
class: "flex items-center",
}) do |f| %>
<%= hidden_field_tag("#{f.object_name}[variant_id]", @variant.id) %>
<div class="flex gap-2 grow">
<%= render component("ui/thumbnail").new(
src: @image&.url(:small),
alt: @variant.name
) %>
<div class="flex-col">
<div class="leading-5 text-black body-small-bold"><%= @variant.name %></div>
<div class="leading-5 text-gray-500 body-small">
SKU:
<%= @variant.sku %>
<%= @variant.options_text.presence&.prepend("- ") %>
</div>
</div>
</div>
</div>
<div class="flex gap-2 items-center">
<span class="text-gray-500 body-small"><%= render component("products/stock").from_variant(@variant) %></span>
<span class="text-black body-small"><%= @variant.display_price.to_html %></span>
</div>
<div class="flex gap-2 items-center">
<span class="text-gray-500 body-small"><%= render component("products/stock").from_variant(@variant) %></span>
<span class="text-black body-small"><%= @variant.display_price.to_html %></span>
</div>
<% end %>
<% end %>
Loading

0 comments on commit 7c3c939

Please sign in to comment.