Skip to content

Commit

Permalink
Merge pull request #5376 from nebulab/rainerd/add-dropdown-filter-com…
Browse files Browse the repository at this point in the history
…ponent

[Admin] Add dynamic filters to `ui/table` component
  • Loading branch information
rainerdema authored Oct 10, 2023
2 parents c64e9fd + 6e78e0a commit dc8dad1
Show file tree
Hide file tree
Showing 13 changed files with 395 additions and 51 deletions.
62 changes: 59 additions & 3 deletions admin/app/components/solidus_admin/orders/index/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,65 @@ def batch_actions
def filters
[
{
name: 'q[completed_at_not_null]',
value: 1,
label: t('.filters.only_show_complete_orders'),
presentation: t('.filters.status'),
combinator: 'or',
attribute: "state",
predicate: "eq",
options: Spree::Order.state_machines[:state].states.map do |state|
[
state.value.titleize,
state.value
]
end
},

{
presentation: t('.filters.shipment_state'),
combinator: 'or',
attribute: "shipment_state",
predicate: "eq",
options: %i[backorder canceled partial pending ready shipped].map do |option|
[
option.to_s.capitalize,
option
]
end
},
{
presentation: t('.filters.payment_state'),
combinator: 'or',
attribute: "payment_state",
predicate: "eq",
options: %i[balance_due checkout completed credit_owed invalid paid pending processing void].map do |option|
[
option.to_s.titleize,
option
]
end
},
{
presentation: t('.filters.variants'),
combinator: 'or',
attribute: "line_items_variant_id",
predicate: "in",
options: Spree::Variant.all.map do |variant|
[
variant.descriptive_name,
variant.id
]
end
},
{
presentation: t('.filters.promotions'),
combinator: 'or',
attribute: "promotions_id",
predicate: "in",
options: Spree::Promotion.all.map do |promotion|
[
promotion.name,
promotion.id
]
end
},
]
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ en:
one: 1 Item
other: '%{count} Items'
filters:
only_show_complete_orders: Only show complete orders
status: Status
shipment_state: Shipment State
payment_state: Payment State
variants: Variants
promotions: Promotions
date:
formats:
short: '%d %b %y'
19 changes: 13 additions & 6 deletions admin/app/components/solidus_admin/products/index/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,20 @@ def batch_actions
end

def filters
[
Spree::OptionType.all.map do |option_type|
{
name: 'q[with_discarded]',
value: true,
label: t('.filters.with_deleted'),
},
]
presentation: option_type.presentation,
combinator: 'or',
attribute: "variants_option_values",
predicate: "in",
options: option_type.option_values.map do |option_value|
[
option_value.name,
option_value.id
]
end
}
end
end

def columns
Expand Down
43 changes: 21 additions & 22 deletions admin/app/components/solidus_admin/ui/table/component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@
rounded-lg
border
border-gray-100
overflow-hidden
"
data-controller="<%= stimulus_id %>"
data-<%= stimulus_id %>-selected-row-class="bg-gray-15"
data-action="
<%= component("ui/table/ransack_filter").stimulus_id %>:search-><%= stimulus_id %>#search
<%= component("ui/table/ransack_filter").stimulus_id %>:showSearch-><%= stimulus_id %>#showSearch
"
>
<% toolbar_classes = "h-14 p-2 bg-white border-b border-gray-100 justify-start items-center gap-2 visible:flex hidden:hidden" %>

<% toolbar_classes = "
h-14 p-2 bg-white border-b border-gray-100
justify-start items-center gap-2
visible:flex hidden:hidden
rounded-t-lg
" %>
<div role="search">
<div class="<%= toolbar_classes %>" data-<%= stimulus_id %>-target="searchToolbar">
<%= form_with(
Expand All @@ -21,19 +28,18 @@
"data-turbo-frame": table_frame_id,
"data-turbo-action": "replace",
"data-#{stimulus_id}-target": "searchForm",
"data-action": "reset->#{stimulus_id}#search",
"data-action": "input->#{stimulus_id}#search change->#{stimulus_id}#search",
},
) do |form| %>
<label class="items-center gap-1 p-0 inline-flex w-full justify-start relative">
<%= render component("ui/icon").new(name: 'search-line', class: "w-[1.4em] h-[1.4em] fill-gray-500 absolute ml-3") %>
<input
name="q[<%= @search_key %>]"
value="<%= params.dig(:q, @search_key) %>"
name="<%= "#{@search_param}[#{@search_key}]" %>"
value="<%= params.dig(@search_param, @search_key) %>"
type="search"
placeholder="<%= t('.search_placeholder', resources: resource_plural_name) %>"
class="peer w-full placeholder:text-gray-400 py-1.5 px-10 bg-white rounded border border-gray-300 search-cancel:appearance-none"
data-<%= stimulus_id %>-target="searchField"
data-action="<%= stimulus_id %>#search"
aria-label="<%= t('.search_placeholder', resources: resource_plural_name) %>"
>
<button
Expand All @@ -57,18 +63,8 @@

<% if @filters.any? %>
<div class="<%= toolbar_classes %>" data-<%= stimulus_id %>-target="filterToolbar">
<div class="font-semibold text-gray-700 text-sm px-2"><%= t('.refine_search') %>:</div>
<% @filters.each do |filter| %>
<label class="flex gap-2 px-2">
<%= render component('ui/forms/checkbox').new(
name: filter[:name],
value: filter[:value],
size: :s,
form: search_form_id,
'data-action': "#{stimulus_id}#search",
) %>
<span class="text-gray-700 leading-none text-sm self-center"><%= filter[:label] %></span>
</label>
<% @filters.each_with_index do |filter, index| %>
<%= render_ransack_filter_dropdown(filter, index) %>
<% end %>
</div>
<% end %>
Expand Down Expand Up @@ -131,7 +127,10 @@

<tbody class="bg-white text-3.5 line-[150%] text-black">
<% @rows.each do |row| %>
<tr class="<%= row_class_for(row) %>">
<tr class="
border-b border-gray-100 last:border-0
<%= 'bg-gray-15 text-gray-700' if @fade_row_proc&.call(row) %>
">
<% @columns.each do |column| %>
<%= render_data_cell(column.data, row) %>
<% end %>
Expand All @@ -142,7 +141,7 @@
<tr>
<td
colspan="<%= @columns.size %>"
class="text-center py-4 text-3.5 line-[150%] text-black bg-white"
class="text-center py-4 text-3.5 line-[150%] text-black bg-white rounded-b-lg"
>
<%= t('.no_resources_found', resources: resource_plural_name) %>
</td>
Expand All @@ -153,7 +152,7 @@
<% if @prev_page_link || @next_page_link %>
<tfoot>
<tr>
<td colspan="<%= @columns.size %>" class="py-4 bg-white">
<td colspan="<%= @columns.size %>" class="py-4 bg-white rounded-b-lg border-t border-gray-100">
<div class="flex justify-center">
<%= render component('ui/table/pagination').new(
prev_link: @prev_page_link,
Expand Down
48 changes: 32 additions & 16 deletions admin/app/components/solidus_admin/ui/table/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class SolidusAdmin::UI::Table::Component < SolidusAdmin::BaseComponent
# @param model_class [ActiveModel::Translation] The model class used for translations.
# @param rows [Array] The collection of objects that will be passed to columns for display.
# @param fade_row_proc [Proc, nil] A proc determining if a row should have a faded appearance.
# @param search_param [Symbol] The param for searching.
# @param search_key [Symbol] The key for searching.
# @param search_url [String] The base URL for searching.
#
Expand All @@ -19,20 +20,22 @@ class SolidusAdmin::UI::Table::Component < SolidusAdmin::BaseComponent
# @option batch_actions [String] :action The batch action path.
# @option batch_actions [String] :method The batch action HTTP method for the provided path.
#
#
# @param filters [Array<Hash>] The array of filter definitions.
# @option filters [String] :name The filter's name.
# @option filters [Any] :value The filter's value.
# @option filters [String] :label The filter's label.
# @param filters [Array<Hash>] The list of filter configurations to render.
# @option filters [String] :presentation The display name of the filter dropdown.
# @option filters [String] :combinator The combining logic of the filter dropdown.
# @option filters [String] :attribute The database attribute this filter modifies.
# @option filters [String] :predicate The predicate used for this filter (e.g., "eq" for equals).
# @option filters [Array<Array>] :options An array of arrays, each containing two elements:
# 1. A human-readable presentation of the filter option (e.g., "Active").
# 2. The actual value used for filtering (e.g., "active").
#
# @param prev_page_link [String, nil] The link to the previous page.
# @param next_page_link [String, nil] The link to the next page.
def initialize(
id:,
model_class:,
rows:,
search_key:,
search_url:,
search_key:, search_url:, search_param: :q,
fade_row_proc: nil,
columns: [],
batch_actions: [],
Expand All @@ -47,6 +50,7 @@ def initialize(
@model_class = model_class
@rows = rows
@fade_row_proc = fade_row_proc
@search_param = search_param
@search_key = search_key
@search_url = search_url
@prev_page_link = prev_page_link
Expand Down Expand Up @@ -112,6 +116,19 @@ def render_batch_action_button(batch_action)
)
end

def render_ransack_filter_dropdown(filter, index)
render component("ui/table/ransack_filter").new(
presentation: filter.presentation,
search_param: @search_param,
combinator: filter.combinator,
attribute: filter.attribute,
predicate: filter.predicate,
options: filter.options,
form: search_form_id,
index: index,
)
end

def render_header_cell(cell, **attrs)
cell = cell.call if cell.respond_to?(:call)
cell = @model_class.human_attribute_name(cell) if cell.is_a?(Symbol)
Expand All @@ -133,18 +150,17 @@ def render_data_cell(cell, data)
cell = data.public_send(cell) if cell.is_a?(Symbol)
cell = cell.render_in(self) if cell.respond_to?(:render_in)

content_tag(:td, content_tag(:div, cell, class: "flex items-center gap-1.5"), class: "py-2 px-4 h-10 vertical-align-middle leading-none")
end

def row_class_for(row)
classes = ['border-b', 'border-gray-100']
classes << ['bg-gray-15', 'text-gray-700'] if @fade_row_proc&.call(row)

classes.join(' ')
tag.td(
tag.div(cell, class: "flex items-center gap-1.5"),
class: "
py-2 px-4 h-10 vertical-align-middle leading-none
[tr:last-child_&:first-child]:rounded-bl-lg [tr:last-child_&:last-child]:rounded-br-lg
",
)
end

Column = Struct.new(:header, :data, :class_name, keyword_init: true)
BatchAction = Struct.new(:display_name, :icon, :action, :method, keyword_init: true) # rubocop:disable Lint/StructNewOverride
Filter = Struct.new(:name, :value, :label, keyword_init: true)
Filter = Struct.new(:presentation, :combinator, :attribute, :predicate, :options, keyword_init: true)
private_constant :Column, :BatchAction, :Filter
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<div class="<%= stimulus_id %>" data-controller="<%= stimulus_id %>">
<input type="hidden" form="<%= @form %>"
name="<%= @combinator.name %>"
value="<%= @combinator.value %>">

<details class="relative inline-block text-left" data-<%= stimulus_id %>-target="details">
<summary class="
inline-flex justify-center
rounded-full border border-gray-300
shadow-sm
px-3 py-2
text-sm font-medium text-gray-700
hover:bg-gray-50
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-indigo-500
[&::marker]:hidden
[&::-webkit-details-marker]:hidden
cursor-default
" data-<%= stimulus_id %>-target="summary">
<%= @presentation %>
<%= render component("ui/icon").new(name: 'arrow-down-s-fill', class: "w-[1.4em] h-[1.4em]") %>
</summary>

<div class="
absolute
left-0 mt-2 w-56
rounded-md shadow-lg
bg-white
ring-1 ring-black ring-opacity-5
">
<div class="relative">
<% if @selections.size > 6 %>
<div class="px-4 py-2 sticky top-0 z-50">
<input type="text"
placeholder="<%= t('.search') %>"
class="w-full px-2 py-1 border border-gray-300 rounded focus:border-indigo-500 focus:ring-indigo-500"
data-action="input-><%= stimulus_id %>#filterOptions">
</div>
<% end %>
<div class="py-1 max-h-[240px] overflow-y-auto" role="menu" aria-orientation="vertical" aria-labelledby="options-menu" data-<%= stimulus_id %>-target="menu">
<% if @selections.any? %>
<% @selections.each do |selection| %>
<div class="px-4 py-2" data-<%= stimulus_id %>-target="option">
<input type="hidden" form="<%= @form %>"
name="<%= selection.attribute.name %>"
value="<%= selection.attribute.value %>">
<input type="hidden" form="<%= @form %>"
name="<%= selection.predicate.name %>"
value="<%= selection.predicate.value %>">

<%= render component('ui/forms/checkbox').new(
id: selection.id,
name: selection.option.name,
value: selection.option.value,
checked: selection.checked,
size: :s,
form: @form,
"data-action": "#{stimulus_id}#search #{stimulus_id}#sortCheckboxes",
"data-#{stimulus_id}-target": "checkbox"
) %>

<%= label_tag selection.id, selection.presentation, class: "ml-2 text-sm text-gray-700" %>
</div>
<% end %>
<% else %>
<div class="px-4 py-2 text-sm text-gray-700">
<%= t('.no_filter_options') %>
</div>
<% end %>
</div>
</div>
</div>
</details>
</div>
Loading

0 comments on commit dc8dad1

Please sign in to comment.