Skip to content

Commit

Permalink
Merge pull request #5467 from solidusio/elia/admin/search-panel
Browse files Browse the repository at this point in the history
[Admin] Extract a `ui/search_panel` component from `orders/cart`
  • Loading branch information
elia authored Oct 31, 2023
2 parents 8e9813a + 7c3c939 commit 94c455b
Show file tree
Hide file tree
Showing 16 changed files with 351 additions and 261 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
.body-title {
@apply font-sans font-semibold text-xl;
}

.body-link {
@apply text-blue hover:underline;
}
}

<%= SolidusAdmin::Config.tailwind_stylesheets.map { File.read(_1) }.join("\n") %>
173 changes: 67 additions & 106 deletions admin/app/components/solidus_admin/orders/cart/component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,116 +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 do |panel| %>
<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()
}
}
4 changes: 1 addition & 3 deletions admin/app/components/solidus_admin/orders/cart/component.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Add your component translations here.
# Use the translation in the example in your template with `t(".hello")`.
en:
title: "Cart"
search_placeholder: "Find a variant by name or SKU"
loading: "Loading..."
initial: "Type to search"
empty: "No results"
Loading

0 comments on commit 94c455b

Please sign in to comment.