Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Admin] Add dynamic filters to ui/table component #5376

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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