- <%= render component("ui/thumbnail").new(
- src: @image&.url(:small),
- alt: @variant.name
- ) %>
-
-
<%= @variant.name %>
-
- 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) %>
+
+ <%= render component("ui/thumbnail").new(
+ src: @image&.url(:small),
+ alt: @variant.name
+ ) %>
+
+
<%= @variant.name %>
+
+ SKU:
+ <%= @variant.sku %>
+ <%= @variant.options_text.presence&.prepend("- ") %>
+
-
-
- <%= render component("products/stock").from_variant(@variant) %>
- <%= @variant.display_price.to_html %>
-
+
+ <%= render component("products/stock").from_variant(@variant) %>
+ <%= @variant.display_price.to_html %>
+
+ <% end %>
<% end %>
diff --git a/admin/app/components/solidus_admin/orders/cart/result/component.js b/admin/app/components/solidus_admin/orders/cart/result/component.js
deleted file mode 100644
index 6778c98c6da..00000000000
--- a/admin/app/components/solidus_admin/orders/cart/result/component.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Controller } from '@hotwired/stimulus'
-
-export default class extends Controller {
- submit(event) {
- event.currentTarget.requestSubmit()
- }
-}
diff --git a/admin/app/components/solidus_admin/ui/search_panel/component.html.erb b/admin/app/components/solidus_admin/ui/search_panel/component.html.erb
new file mode 100644
index 00000000000..81dde83b460
--- /dev/null
+++ b/admin/app/components/solidus_admin/ui/search_panel/component.html.erb
@@ -0,0 +1,60 @@
+
-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(**@panel_args) do |panel| %>
+
+
+
+ <%= render component("ui/forms/search_field").new(
+ id: "#{stimulus_id}--search-field--#{@id}",
+ placeholder: @search_placeholder,
+ "data-action": "
+ #{stimulus_id}#search
+ #{stimulus_id}#showResults
+ ",
+ "data-#{stimulus_id}-target": "searchField",
+ ) %>
+
+
+ <%# results popover %>
+
+
+ -target="results"
+ >
+
+
+
+
+ <%# line items table %>
+
+ <%= content %>
+
+ <% end %>
+
diff --git a/admin/app/components/solidus_admin/ui/search_panel/component.js b/admin/app/components/solidus_admin/ui/search_panel/component.js
new file mode 100644
index 00000000000..1803f8cfa52
--- /dev/null
+++ b/admin/app/components/solidus_admin/ui/search_panel/component.js
@@ -0,0 +1,117 @@
+import { Controller } from "@hotwired/stimulus"
+import { useClickOutside, 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 = ["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]
+ }
+
+ connect() {
+ useClickOutside(this)
+ useDebounce(this)
+
+ this.selectedIndex = 0
+
+ if (this.query) {
+ this.showResults()
+ this.search()
+ }
+ }
+
+ selectResult(event) {
+ event.preventDefault()
+ this.dispatch("submit", { detail: { resultTarget: this.selectedResult } })
+ }
+
+ clickedResult(event) {
+ this.selectedIndex = this.resultTargets.indexOf(event.currentTarget)
+ this.render()
+ this.selectResult(event)
+ }
+
+ selectPrev(event) {
+ event.preventDefault()
+ this.selectedIndex -= 1
+ this.render()
+ }
+
+ selectNext(event) {
+ event.preventDefault()
+ this.selectedIndex += 1
+ this.render()
+ }
+
+ clickOutside() {
+ this.openResults = false
+ this.render()
+ }
+
+ async search() {
+ const query = this.query
+
+ if (query) {
+ this.resultsValue = this.loadingTextValue
+ this.render()
+ this.dispatch("search", { detail: { query, controller: this } })
+ } else {
+ this.resultsValue = this.initialTextValue
+ this.render()
+ }
+ }
+
+ resultsValueChanged() {
+ this.selectedIndex = 0
+ this.render()
+ }
+
+ showResults() {
+ this.openResults = true
+ this.render()
+ }
+
+ 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
+ }
+ }
+}
diff --git a/admin/app/components/solidus_admin/ui/search_panel/component.rb b/admin/app/components/solidus_admin/ui/search_panel/component.rb
new file mode 100644
index 00000000000..9424506a961
--- /dev/null
+++ b/admin/app/components/solidus_admin/ui/search_panel/component.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class SolidusAdmin::UI::SearchPanel::Component < SolidusAdmin::BaseComponent
+ def initialize(search_placeholder:, id:, **panel_args)
+ @search_placeholder = search_placeholder
+ @panel_args = panel_args
+ @id = id
+ end
+end
diff --git a/admin/app/components/solidus_admin/ui/search_panel/component.yml b/admin/app/components/solidus_admin/ui/search_panel/component.yml
new file mode 100644
index 00000000000..05ebdc5b93c
--- /dev/null
+++ b/admin/app/components/solidus_admin/ui/search_panel/component.yml
@@ -0,0 +1,4 @@
+en:
+ loading: "Loading..."
+ initial: "Type to search"
+ empty: "No results"
diff --git a/admin/app/components/solidus_admin/ui/search_panel/result/component.rb b/admin/app/components/solidus_admin/ui/search_panel/result/component.rb
new file mode 100644
index 00000000000..520bbbce5ed
--- /dev/null
+++ b/admin/app/components/solidus_admin/ui/search_panel/result/component.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class SolidusAdmin::UI::SearchPanel::Result::Component < SolidusAdmin::BaseComponent
+ def call
+ tag.div(
+ content,
+ class: "rounded p-2 hover:bg-gray-25 aria-selected:bg-gray-25 cursor-pointer",
+ "data-#{component('ui/search_panel').stimulus_id}-target": "result",
+ "data-action": "click->#{component('ui/search_panel').stimulus_id}#clickedResult",
+ )
+ end
+end
diff --git a/admin/spec/components/previews/solidus_admin/ui/search_panel/component_preview.rb b/admin/spec/components/previews/solidus_admin/ui/search_panel/component_preview.rb
new file mode 100644
index 00000000000..1e7fd03be1d
--- /dev/null
+++ b/admin/spec/components/previews/solidus_admin/ui/search_panel/component_preview.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+# @component "ui/search_panel"
+class SolidusAdmin::UI::SearchPanel::ComponentPreview < ViewComponent::Preview
+ include SolidusAdmin::Preview
+
+ def overview
+ render_with_template
+ end
+
+
+ def playground
+ render component("ui/search_panel").new
+ end
+end
diff --git a/admin/spec/components/previews/solidus_admin/ui/search_panel/component_preview/overview.html.erb b/admin/spec/components/previews/solidus_admin/ui/search_panel/component_preview/overview.html.erb
new file mode 100644
index 00000000000..3b38152767d
--- /dev/null
+++ b/admin/spec/components/previews/solidus_admin/ui/search_panel/component_preview/overview.html.erb
@@ -0,0 +1,7 @@
+
+
+ Scenario 1
+
+
+ <%= render current_component.new %>
+
diff --git a/admin/spec/components/solidus_admin/ui/search_panel/component_spec.rb b/admin/spec/components/solidus_admin/ui/search_panel/component_spec.rb
new file mode 100644
index 00000000000..c8debebe6a5
--- /dev/null
+++ b/admin/spec/components/solidus_admin/ui/search_panel/component_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe SolidusAdmin::UI::SearchPanel::Component, type: :component do
+ it "renders the overview preview" do
+ render_preview(:overview)
+ end
+
+ # it "renders something useful" do
+ # render_inline(described_class.new())
+ #
+ # expect(page).to have_text "Hello, components!"
+ # expect(page).to have_css '.value'
+ # end
+end