diff --git a/admin/app/components/solidus_admin/layout/navigation/component.html.erb b/admin/app/components/solidus_admin/layout/navigation/component.html.erb index fe5aacf0b79..1d91983404a 100644 --- a/admin/app/components/solidus_admin/layout/navigation/component.html.erb +++ b/admin/app/components/solidus_admin/layout/navigation/component.html.erb @@ -4,14 +4,14 @@ p-4 w-full " data-controller="<%= stimulus_id %>" data-<%= stimulus_id %>-cookie-value="solidus_admin"> - <%= link_to @store.url, class: "py-3 px-2 text-left flex mb-4" do %> + <%= link_to spree.admin_path, class: "py-3 px-2 text-left flex mb-4" do %> <%= image_tag @logo_path, alt: t('.visit_store'), class: "max-h-7" %> <% end %> - <%= link_to @store.url, target: :_blank, class: "flex mb-4 px-2 py-1.5 border border-gray-100 rounded-sm shadow-sm" do %> + <%= link_to @store_url, target: :_blank, class: "flex mb-4 px-2 py-1.5 border border-gray-100 rounded-sm shadow-sm" do %>

<%= @store.name %>

-

<%= @store.url %>

+

<%= @store_url %>

<%= render component("ui/icon").new(name: 'arrow-right-up-line', class: 'w-4 h-4 fill-gray-400') %> <% end %> diff --git a/admin/app/components/solidus_admin/layout/navigation/component.rb b/admin/app/components/solidus_admin/layout/navigation/component.rb index ccddd86e5a0..d1945e48570 100644 --- a/admin/app/components/solidus_admin/layout/navigation/component.rb +++ b/admin/app/components/solidus_admin/layout/navigation/component.rb @@ -15,6 +15,12 @@ def initialize( @store = store end + def before_render + url = @store.url + url = "https://#{url}" unless url.start_with?("http") + @store_url = url + end + def items @items.sort_by(&:position) end diff --git a/admin/app/components/solidus_admin/orders/index/component.html.erb b/admin/app/components/solidus_admin/orders/index/component.html.erb index 344cce236b1..5a0440033ac 100644 --- a/admin/app/components/solidus_admin/orders/index/component.html.erb +++ b/admin/app/components/solidus_admin/orders/index/component.html.erb @@ -16,16 +16,23 @@ <%= render component('ui/table').new( id: 'orders-list', - model_class: Spree::Order, - rows: @page.records, - row_fade: row_fade, - row_url: ->(order) { spree.edit_admin_order_path(order) }, - search_key: SolidusAdmin::Config[:order_search_key], - search_url: solidus_admin.orders_path, - batch_actions: batch_actions, - filters: filters, - columns: columns, - prev_page_link: prev_page_link, - next_page_link: next_page_link, + data: { + class: Spree::Order, + rows: @page.records, + fade: row_fade, + url: ->(order) { spree.edit_admin_order_path(order) }, + batch_actions: batch_actions, + columns: columns, + prev: prev_page_link, + next: next_page_link, + }, + search: { + name: :q, + value: params[:q], + searchbar_key: SolidusAdmin::Config[:order_search_key], + url: solidus_admin.orders_path(scope: params[:scope]), + filters: filters, + scopes: scopes, + }, ) %> diff --git a/admin/app/components/solidus_admin/orders/index/component.rb b/admin/app/components/solidus_admin/orders/index/component.rb index 57df251d56e..407e5b710fa 100644 --- a/admin/app/components/solidus_admin/orders/index/component.rb +++ b/admin/app/components/solidus_admin/orders/index/component.rb @@ -23,6 +23,16 @@ def batch_actions [] end + def scopes + [ + { label: t('.scopes.complete'), name: 'completed', default: true }, + { label: t('.scopes.in_progress'), name: 'in_progress' }, + { label: t('.scopes.returned'), name: 'returned' }, + { label: t('.scopes.canceled'), name: 'canceled' }, + { label: t('.scopes.all_orders'), name: 'all' }, + ] + end + def filters [ { @@ -92,6 +102,7 @@ def filters def columns [ number_column, + state_column, date_column, customer_column, total_column, @@ -114,6 +125,21 @@ def number_column } end + def state_column + { + header: :state, + data: ->(order) do + color = { + 'complete' => :green, + 'returned' => :red, + 'canceled' => :blue, + 'cart' => :graphite_light, + }[order.state] || :yellow + component('ui/badge').new(name: order.state.humanize, color: color) + end + } + end + def date_column { header: :date, diff --git a/admin/app/components/solidus_admin/orders/index/component.yml b/admin/app/components/solidus_admin/orders/index/component.yml index a94a884aa5e..85613782601 100644 --- a/admin/app/components/solidus_admin/orders/index/component.yml +++ b/admin/app/components/solidus_admin/orders/index/component.yml @@ -15,3 +15,9 @@ en: date: formats: short: '%d %b %y' + scopes: + all_orders: All + canceled: Canceled + complete: Complete + returned: Returned + in_progress: In Progress diff --git a/admin/app/components/solidus_admin/products/index/component.html.erb b/admin/app/components/solidus_admin/products/index/component.html.erb index 5eee57b7229..f3033efaeb2 100644 --- a/admin/app/components/solidus_admin/products/index/component.html.erb +++ b/admin/app/components/solidus_admin/products/index/component.html.erb @@ -13,15 +13,22 @@ <%= render component('ui/table').new( id: 'products-list', - model_class: Spree::Product, - rows: @page.records, - row_url: ->(product) { solidus_admin.product_path(product) }, - search_key: SolidusAdmin::Config[:product_search_key], - search_url: solidus_admin.products_path, - batch_actions: batch_actions, - filters: filters, - columns: columns, - prev_page_link: prev_page_link, - next_page_link: next_page_link, + data: { + class: Spree::Product, + rows: @page.records, + url: ->(product) { solidus_admin.product_path(product) }, + prev: prev_page_link, + next: next_page_link, + columns: columns, + batch_actions: batch_actions, + }, + search: { + name: :q, + value: params[:q], + url: solidus_admin.products_path, + searchbar_key: SolidusAdmin::Config[:product_search_key], + filters: filters, + scopes: scopes, + }, ) %> <% end %> diff --git a/admin/app/components/solidus_admin/products/index/component.rb b/admin/app/components/solidus_admin/products/index/component.rb index eca8bee1509..a53adb8ce7e 100644 --- a/admin/app/components/solidus_admin/products/index/component.rb +++ b/admin/app/components/solidus_admin/products/index/component.rb @@ -59,6 +59,13 @@ def filters end end + def scopes + [ + { name: :all, label: t('.scopes.all'), default: true }, + { name: :deleted, label: t('.scopes.deleted') }, + ] + end + def columns [ image_column, diff --git a/admin/app/components/solidus_admin/products/index/component.yml b/admin/app/components/solidus_admin/products/index/component.yml index cc6fd827a10..4de6e6f3e48 100644 --- a/admin/app/components/solidus_admin/products/index/component.yml +++ b/admin/app/components/solidus_admin/products/index/component.yml @@ -7,3 +7,6 @@ en: activate: 'Activate' filters: with_deleted: Include deleted + scopes: + all: All + deleted: Deleted diff --git a/admin/app/components/solidus_admin/ui/button/component.rb b/admin/app/components/solidus_admin/ui/button/component.rb index 39dd4d0d493..dc3daae8097 100644 --- a/admin/app/components/solidus_admin/ui/button/component.rb +++ b/admin/app/components/solidus_admin/ui/button/component.rb @@ -34,32 +34,32 @@ class SolidusAdmin::UI::Button::Component < SolidusAdmin::BaseComponent hover:text-white hover:bg-gray-600 active:text-white active:bg-gray-800 focus:text-white focus:bg-gray-700 - disabled:text-gray-400 disabled:bg-gray-100 disabled:cursor-not-allowed - aria-disabled:text-gray-400 aria-disabled:bg-gray-100 aria-disabled:aria-disabled:cursor-not-allowed + disabled:text-gray-400 disabled:bg-gray-100 + aria-disabled:text-gray-400 aria-disabled:bg-gray-100 }, secondary: %{ text-gray-700 bg-white border border-1 border-gray-200 hover:bg-gray-50 active:bg-gray-100 focus:bg-gray-50 - disabled:text-gray-300 disabled:bg-white disabled:cursor-not-allowed - aria-disabled:text-gray-300 aria-disabled:bg-white aria-disabled:cursor-not-allowed + disabled:text-gray-300 disabled:bg-white + aria-disabled:text-gray-300 aria-disabled:bg-white }, danger: %{ text-red-500 bg-white border border-1 border-red-500 hover:bg-red-500 hover:border-red-600 hover:text-white active:bg-red-600 active:border-red-700 active:text-white focus:bg-red-50 focus:bg-red-500 focus:border-red-600 focus:text-white - disabled:text-red-300 disabled:bg-white disabled:border-red-200 disabled:cursor-not-allowed - aria-disabled:text-red-300 aria-disabled:bg-white aria-disabled:border-red-200 aria-disabled:cursor-not-allowed + disabled:text-red-300 disabled:bg-white disabled:border-red-200 + aria-disabled:text-red-300 aria-disabled:bg-white aria-disabled:border-red-200 }, ghost: %{ text-gray-700 bg-transparent hover:bg-gray-50 active:bg-gray-100 focus:bg-gray-50 focus:ring-gray-300 focus:ring-2 - disabled:text-gray-300 disabled:bg-transparent disabled:border-gray-300 disabled:cursor-not-allowed - aria-disabled:text-gray-300 aria-disabled:bg-transparent aria-disabled:border-gray-300 aria-disabled:cursor-not-allowed + disabled:text-gray-300 disabled:bg-transparent disabled:border-gray-300 + aria-disabled:text-gray-300 aria-disabled:bg-transparent aria-disabled:border-gray-300 }, } diff --git a/admin/app/components/solidus_admin/ui/tab/component.rb b/admin/app/components/solidus_admin/ui/tab/component.rb index fbd13487b1e..f9b74dd4bee 100644 --- a/admin/app/components/solidus_admin/ui/tab/component.rb +++ b/admin/app/components/solidus_admin/ui/tab/component.rb @@ -7,7 +7,8 @@ class SolidusAdmin::UI::Tab::Component < SolidusAdmin::BaseComponent l: %w[h-12 px-4 body-text-bold], } - def initialize(text:, size: :m, current: false, disabled: false, **attributes) + def initialize(text:, tag: :a, size: :m, current: false, disabled: false, **attributes) + @tag = tag @text = text @size = size @attributes = attributes @@ -22,11 +23,11 @@ def initialize(text:, size: :m, current: false, disabled: false, **attributes) hover:bg-gray-75 hover:text-gray-700 focus:bg-gray-25 focus:text-gray-700 - active:bg-gray-50 active:text-black + active:bg-gray-75 active:text-black aria-current:bg-gray-50 aria-current:text-black - disabled:bg-gray-100 disabled:text-gray-400 - aria-disabled:bg-gray-100 aria-disabled:text-gray-400 + disabled:bg-gray-75 disabled:text-gray-400 + aria-disabled:bg-gray-75 aria-disabled:text-gray-400 ], SIZES.fetch(@size.to_sym), @attributes.delete(:class), @@ -35,7 +36,7 @@ def initialize(text:, size: :m, current: false, disabled: false, **attributes) def call content_tag( - :a, + @tag, @text, **@attributes ) diff --git a/admin/app/components/solidus_admin/ui/table/component.html.erb b/admin/app/components/solidus_admin/ui/table/component.html.erb index bf456657e1d..9cc1c93d653 100644 --- a/admin/app/components/solidus_admin/ui/table/component.html.erb +++ b/admin/app/components/solidus_admin/ui/table/component.html.erb @@ -6,31 +6,34 @@ " data-controller="<%= stimulus_id %>" data-<%= stimulus_id %>-selected-row-class="bg-gray-15" + data-<%= stimulus_id %>-mode-value="<%= initial_mode %>" data-action=" <%= component("ui/table/ransack_filter").stimulus_id %>:search-><%= stimulus_id %>#search <%= component("ui/table/ransack_filter").stimulus_id %>:showSearch-><%= stimulus_id %>#showSearch " >
- <%= render component("ui/table/toolbar").new("data-#{stimulus_id}-target": "searchToolbar") do %> + <%= render component("ui/table/toolbar").new("data-#{stimulus_id}-target": "searchToolbar", hidden: initial_mode != "search") do %> <%= form_with( - url: @search_url, + url: @search.url, method: :get, html: { id: search_form_id, class: 'flex-grow', - "data-turbo-frame": table_frame_id, "data-turbo-action": "replace", "data-#{stimulus_id}-target": "searchForm", "data-action": "input->#{stimulus_id}#search change->#{stimulus_id}#search", }, ) do |form| %> + <%= hidden_field_tag @search.scope_param_name, @search.current_scope.name if @search.scopes.present? %> <%= render component('ui/forms/search_field').new( - name: "#{@search_param}[#{@search_key}]", - value: params.dig(@search_param, @search_key), - placeholder: t('.search_placeholder', resources: resource_plural_name), + name: @search.searchbar_param_name, + value: @search.value[@search.searchbar_key], + placeholder: t('.search_placeholder', resources: @data.plural_name), + "aria-label": t('.search_placeholder', resources: @data.plural_name), "data-#{stimulus_id}-target": "searchField", - "aria-label": t('.search_placeholder', resources: resource_plural_name) + "data-turbo-permanent": "true", + id: "#{stimulus_id}-search-field-#{@id}", ) %> <% end %> @@ -43,17 +46,28 @@
<% end %> - <% if @filters.any? %> - <%= render component("ui/table/toolbar").new("data-#{stimulus_id}-target": "filterToolbar") do %> - <% @filters.each_with_index do |filter, index| %> + <% if @search.filters.any? %> + <%= render component("ui/table/toolbar").new("data-#{stimulus_id}-target": "filterToolbar", hidden: initial_mode != "search") do %> + <% @search.filters.each_with_index do |filter, index| %> <%= render_ransack_filter_dropdown(filter, index) %> <% end %> <% end %> <% end %> - <%= render component("ui/table/toolbar").new("data-#{stimulus_id}-target": "scopesToolbar") do %> + <%= render component("ui/table/toolbar").new("data-#{stimulus_id}-target": "scopesToolbar", hidden: initial_mode != "scopes") do %>
- <%= render component("ui/tab").new(text: "All", current: true, href: "") %> + <%= form_with(url: @search.url, method: :get) do %> + <% @search.scopes.each do |scope| %> + <%= render component("ui/tab").new( + tag: :button, + type: :submit, + text: scope.label, + current: scope == @search.current_scope, + name: @search.scope_param_name, + value: scope.name, + ) %> + <% end %> + <% end %>
<%= render component("ui/button").new( @@ -65,90 +79,88 @@ <% end %> - <%= render component("ui/table/toolbar").new("data-#{stimulus_id}-target": "batchToolbar", role: "toolbar", "aria-label": t(".batch_actions")) do %> + <%= render component("ui/table/toolbar").new("data-#{stimulus_id}-target": "batchToolbar", role: "toolbar", "aria-label": t(".batch_actions"), hidden: true) do %> <%= form_tag '', id: batch_actions_form_id %> - <% @batch_actions.each do |batch_action| %> + <% @data.batch_actions.each do |batch_action| %> <%= render_batch_action_button(batch_action) %> <% end %> <% end %> - <%= turbo_frame_tag table_frame_id, target: "_top" do %> - - - <% @columns.each do |column| %> - "> +
+ + <% @data.columns.each do |column| %> + "> + <% end %> + + + -target="defaultHeader" + > + + <% @data.columns.each do |column| %> + <%= render_header_cell(column.header) %> <% end %> - + + + <% if @data.batch_actions %> -target="defaultHeader" + data-<%= stimulus_id %>-target="batchHeader" + class="bg-white color-black text-xs leading-none text-left" + hidden > - <% @columns.each do |column| %> - <%= render_header_cell(column.header) %> - <% end %> + <%= render_header_cell(selectable_column.header) %> + <%= render_header_cell(content_tag(:div, safe_join([ + content_tag(:span, "0", "data-#{stimulus_id}-target": "selectedRowsCount"), + " #{t('.rows_selected')}.", + ])), colspan: @data.columns.count - 1) %> + <% end %> - <% if @batch_actions %> - -target="batchHeader" - class="bg-white color-black text-xs leading-none text-left" - hidden + + <% @data.rows.each do |row| %> + + data-action="click-><%= stimulus_id %>#rowClicked" + data-<%= stimulus_id %>-url-param="<%= @data.url.call(row) %>" + <% end %> > - - <%= render_header_cell(selectable_column.header) %> - <%= render_header_cell(content_tag(:div, safe_join([ - content_tag(:span, "0", "data-#{stimulus_id}-target": "selectedRowsCount"), - " #{t('.rows_selected')}.", - ])), colspan: @columns.count - 1) %> - - + <% @data.columns.each do |column| %> + <%= render_data_cell(column, row) %> + <% end %> + <% end %> - - <% @rows.each do |row| %> - - data-action="click-><%= stimulus_id %>#rowClicked" - data-<%= stimulus_id %>-url-param="<%= @row_url.call(row) %>" - <% end %> + <% if @data.rows.empty? && @data.plural_name %> + + - <% end %> - - <% if @rows.empty? && @model_class %> - - - - <% end %> - - - <% if @prev_page_link || @next_page_link %> - - - - - + <%= t('.no_resources_found', resources: @data.plural_name) %> + + <% end %> + -
- <% @columns.each do |column| %> - <%= render_data_cell(column, row) %> - <% end %> -
- <%= t('.no_resources_found', resources: resource_plural_name) %> -
-
- <%= render component('ui/table/pagination').new( - prev_link: @prev_page_link, - next_link: @next_page_link - ) %> -
-
- <% end %> + <% if @data.prev || @data.next %> + + + +
+ <%= render component('ui/table/pagination').new( + prev_link: @data.prev, + next_link: @data.next + ) %> +
+ + + + <% end %> + + diff --git a/admin/app/components/solidus_admin/ui/table/component.js b/admin/app/components/solidus_admin/ui/table/component.js index d8e4ca7bf54..d6a5b196f9e 100644 --- a/admin/app/components/solidus_admin/ui/table/component.js +++ b/admin/app/components/solidus_admin/ui/table/component.js @@ -29,12 +29,6 @@ export default class extends Controller { this.search = debounce(this.search.bind(this), 200) } - connect() { - if (this.searchFieldTarget.value !== "") this.modeValue = "search" - - this.render() - } - showSearch(event) { this.modeValue = "search" this.render() @@ -51,10 +45,18 @@ export default class extends Controller { } cancelSearch() { - this.clearSearch() + this.resetFilters() + this.search() + } - this.modeValue = "scopes" - this.render() + resetFilters() { + if (!this.hasFilterToolbarTarget) return + + for (const fieldset of this.filterToolbarTarget.querySelectorAll('fieldset')) { + fieldset.setAttribute('disabled', true) + } + this.searchFieldTarget.setAttribute('disabled', true) + this.searchFormTarget.submit() } selectRow(event) { diff --git a/admin/app/components/solidus_admin/ui/table/component.rb b/admin/app/components/solidus_admin/ui/table/component.rb index 8008d4f38cd..fbc7edda4f9 100644 --- a/admin/app/components/solidus_admin/ui/table/component.rb +++ b/admin/app/components/solidus_admin/ui/table/component.rb @@ -1,69 +1,59 @@ # frozen_string_literal: true class SolidusAdmin::UI::Table::Component < SolidusAdmin::BaseComponent - # @param id [String] A unique identifier for the table component. - # @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 row_fade [Proc, nil] A proc determining if a row should have a faded appearance. - # @param row_url [Proc, nil] A proc that takes a row object as a parameter and returns the URL to navigate to when the row is clicked. - # @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. - # - # @param columns [Array] The array of column definitions. - # @option columns [Symbol|Proc|#to_s] :header The column header. - # @option columns [Symbol|Proc|#to_s] :data The data accessor for the column. - # @option columns [String] :class_name (optional) The class name for the column. - # - # @param batch_actions [Array] The array of batch action definitions. - # @option batch_actions [String] :display_name The batch action display name. - # @option batch_actions [String] :icon The batch action icon. - # @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] 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] :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_param: :q, - row_fade: nil, - row_url: nil, - columns: [], - batch_actions: [], - filters: [], - prev_page_link: nil, - next_page_link: nil - ) - @columns = columns.map { Column.new(wrap: true, **_1) } - @batch_actions = batch_actions.map { BatchAction.new(**_1) } - @filters = filters.map { Filter.new(**_1) } - @id = id - @model_class = model_class - @rows = rows - @row_fade = row_fade - @row_url = row_url - @search_param = search_param - @search_key = search_key - @search_url = search_url - @prev_page_link = prev_page_link - @next_page_link = next_page_link - - @columns.unshift selectable_column if batch_actions.present? + BatchAction = Struct.new(:display_name, :icon, :action, :method, keyword_init: true) # rubocop:disable Lint/StructNewOverride + Column = Struct.new(:header, :data, :col, :wrap, keyword_init: true) + Filter = Struct.new(:presentation, :combinator, :attribute, :predicate, :options, keyword_init: true) + Scope = Struct.new(:name, :label, :default, keyword_init: true) + private_constant :BatchAction, :Column, :Filter, :Scope + + class Data < Struct.new(:rows, :class, :url, :prev, :next, :columns, :fade, :batch_actions, keyword_init: true) # rubocop:disable Lint/StructNewOverride,Style/StructInheritance + def initialize(**args) + super + + self.columns = columns.map { |column| Column.new(wrap: false, **column) } + self.batch_actions = batch_actions.to_a.map { |action| BatchAction.new(**action) } + end + + def plural_name + self[:class].model_name.human.pluralize if self[:class] + end + end + + class Search < Struct.new(:name, :value, :url, :searchbar_key, :filters, :scopes, keyword_init: true) # rubocop:disable Style/StructInheritance + def initialize(**args) + super + + self.filters = filters.to_a.map { |filter| Filter.new(**filter) } + self.scopes = scopes.to_a.map { |scope| Scope.new(**scope) } + end + + def current_scope + scopes.find { |scope| scope.name.to_s == value[:scope].presence } || default_scope + end + + def default_scope + scopes.find(&:default) + end + + def scope_param_name + "#{name}[scope]" + end + + def searchbar_param_name + "#{name}[#{searchbar_key}]" + end + + def value + super || {} + end end - def resource_plural_name - @model_class.model_name.human.pluralize + def initialize(id:, data:, search: nil) + @id = id + @data = Data.new(**data) + @data.columns.unshift selectable_column if @data.batch_actions.present? + @search = Search.new(**search) end def selectable_column @@ -94,10 +84,6 @@ def batch_actions_form_id @batch_actions_form_id ||= "#{stimulus_id}--batch-actions-#{@id}" end - def table_frame_id - @table_frame_id ||= "#{stimulus_id}--table-frame-#{@id}" - end - def search_form_id @search_form_id ||= "#{stimulus_id}--search-form-#{@id}" end @@ -122,7 +108,7 @@ def render_batch_action_button(batch_action) def render_ransack_filter_dropdown(filter, index) render component("ui/table/ransack_filter").new( presentation: filter.presentation, - search_param: @search_param, + search_param: @search.name, combinator: filter.combinator, attribute: filter.attribute, predicate: filter.predicate, @@ -134,7 +120,7 @@ def render_ransack_filter_dropdown(filter, index) 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) + cell = @data[:class].human_attribute_name(cell) if cell.is_a?(Symbol) cell = cell.render_in(self) if cell.respond_to?(:render_in) content_tag(:th, cell, class: %{ @@ -153,7 +139,7 @@ def render_data_cell(column, data) cell = cell.call(data) if cell.respond_to?(:call) cell = data.public_send(cell) if cell.is_a?(Symbol) cell = cell.render_in(self) if cell.respond_to?(:render_in) - cell = tag.div(cell, class: "flex items-center gap-1.5 justify-start overflow-hidden") if column.wrap + cell = tag.div(cell, class: "flex items-center gap-1.5 justify-start overflow-x-hidden") if column.wrap tag.td(cell, class: " py-2 px-4 h-10 vertical-align-middle leading-none @@ -161,8 +147,11 @@ def render_data_cell(column, data) ") end - Column = Struct.new(:header, :data, :col, :wrap, keyword_init: true) - BatchAction = Struct.new(:display_name, :icon, :action, :method, keyword_init: true) # rubocop:disable Lint/StructNewOverride - Filter = Struct.new(:presentation, :combinator, :attribute, :predicate, :options, keyword_init: true) - private_constant :Column, :BatchAction, :Filter + def current_scope_name + @search.current_scope.name + end + + def initial_mode + @initial_mode ||= @search.value[@search.searchbar_key] ? "search" : "scopes" + end end diff --git a/admin/app/components/solidus_admin/ui/table/ransack_filter/component.html.erb b/admin/app/components/solidus_admin/ui/table/ransack_filter/component.html.erb index 9838a4d1f47..76d2a121392 100644 --- a/admin/app/components/solidus_admin/ui/table/ransack_filter/component.html.erb +++ b/admin/app/components/solidus_admin/ui/table/ransack_filter/component.html.erb @@ -1,4 +1,4 @@ -
+
@@ -52,7 +52,7 @@ checked: selection.checked, size: :s, form: @form, - "data-action": "#{stimulus_id}#search #{stimulus_id}#sortCheckboxes", + "data-action": "#{stimulus_id}#search #{stimulus_id}#sortCheckboxes #{stimulus_id}#updateHiddenInputs", "data-#{stimulus_id}-target": "checkbox" ) %> @@ -68,4 +68,4 @@
- + diff --git a/admin/app/components/solidus_admin/ui/table/ransack_filter/component.js b/admin/app/components/solidus_admin/ui/table/ransack_filter/component.js index ac9489cea99..106133d0292 100644 --- a/admin/app/components/solidus_admin/ui/table/ransack_filter/component.js +++ b/admin/app/components/solidus_admin/ui/table/ransack_filter/component.js @@ -11,6 +11,7 @@ export default class extends Controller { useDebounce(this, { wait: 50 }) useClickOutside(this) this.init() + this.updateHiddenInputs() } clickOutside(event) { @@ -44,6 +45,15 @@ export default class extends Controller { this.highlightFilter() } + updateHiddenInputs() { + this.checkboxTargets.forEach((checkbox) => { + const hiddenElements = checkbox.parentElement.querySelectorAll("input[type='hidden']") + checkbox.checked + ? hiddenElements.forEach(e => e.removeAttribute("disabled")) + : hiddenElements.forEach(e => e.setAttribute("disabled", true)) + }) + } + sortCheckboxes() { const checkboxes = this.checkboxTargets diff --git a/admin/app/controllers/solidus_admin/controller_helpers/search.rb b/admin/app/controllers/solidus_admin/controller_helpers/search.rb new file mode 100644 index 00000000000..3f40fb20fc5 --- /dev/null +++ b/admin/app/controllers/solidus_admin/controller_helpers/search.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module SolidusAdmin::ControllerHelpers::Search + extend ActiveSupport::Concern + + module ClassMethods + def search_scope(name, default: false, &block) + search_scopes << SearchScope.new( + name: name.to_s, + block: block, + default: default, + ) + end + + def search_scopes + @search_scopes ||= [] + end + end + + private + + def apply_search_to(relation, param:) + relation = apply_scopes_to(relation, param: param) + apply_ransack_search_to(relation, param: param) + end + + def apply_ransack_search_to(relation, param:) + relation + .ransack(params[param]&.except(:scope)) + .result(distinct: true) + end + + def apply_scopes_to(relation, param:) + current_scope_name = params.dig(param, :scope) + + search_block = ( + self.class.search_scopes.find { _1.name == current_scope_name } || + self.class.search_scopes.find { _1.default } + )&.block + + # Run the search if a block is present, fall back to the relation even if the + # block is present but returns nil. + (search_block && instance_exec(relation, &search_block)) || relation + end + + SearchScope = Struct.new(:name, :block, :default, keyword_init: true) + private_constant :SearchScope +end diff --git a/admin/app/controllers/solidus_admin/orders_controller.rb b/admin/app/controllers/solidus_admin/orders_controller.rb index 9b45c6bce3b..263a484fd7e 100644 --- a/admin/app/controllers/solidus_admin/orders_controller.rb +++ b/admin/app/controllers/solidus_admin/orders_controller.rb @@ -3,12 +3,19 @@ module SolidusAdmin class OrdersController < SolidusAdmin::BaseController include Spree::Core::ControllerHelpers::StrongParameters + include SolidusAdmin::ControllerHelpers::Search + + search_scope(:completed, default: true) { _1.complete } + search_scope(:canceled) { _1.canceled } + search_scope(:returned) { _1.with_state(:returned) } + search_scope(:in_progress) { _1.with_state([:cart] + _1.checkout_step_names) } + search_scope(:all) { _1 } def index - orders = Spree::Order - .order(created_at: :desc, id: :desc) - .ransack(params[:q]) - .result(distinct: true) + orders = apply_search_to( + Spree::Order.order(created_at: :desc, id: :desc), + param: :q, + ) set_page_and_extract_portion_from( orders, diff --git a/admin/app/controllers/solidus_admin/products_controller.rb b/admin/app/controllers/solidus_admin/products_controller.rb index d4a2d6550d8..c82a5ebd1a8 100644 --- a/admin/app/controllers/solidus_admin/products_controller.rb +++ b/admin/app/controllers/solidus_admin/products_controller.rb @@ -2,6 +2,27 @@ module SolidusAdmin class ProductsController < SolidusAdmin::BaseController + include SolidusAdmin::ControllerHelpers::Search + + search_scope(:all, default: true) + search_scope(:deleted) { _1.with_discarded.discarded } + + def index + products = apply_search_to( + Spree::Product.order(created_at: :desc, id: :desc), + param: :q, + ) + + set_page_and_extract_portion_from( + products, + per_page: SolidusAdmin::Config[:products_per_page] + ) + + respond_to do |format| + format.html { render component('products/index').new(page: @page) } + end + end + def edit redirect_to action: :show end @@ -33,22 +54,6 @@ def update end end - def index - products = Spree::Product - .order(created_at: :desc, id: :desc) - .ransack(params[:q]) - .result(distinct: true) - - set_page_and_extract_portion_from( - products, - per_page: SolidusAdmin::Config[:products_per_page] - ) - - respond_to do |format| - format.html { render component('products/index').new(page: @page) } - end - end - def destroy @products = Spree::Product.where(id: params[:id]) diff --git a/admin/config/routes.rb b/admin/config/routes.rb index 271e8b33b26..587c5599468 100644 --- a/admin/config/routes.rb +++ b/admin/config/routes.rb @@ -2,6 +2,7 @@ SolidusAdmin::Engine.routes.draw do resource :account, only: :show + resources( :products, only: [:index, :show, :edit, :update], diff --git a/admin/spec/components/previews/solidus_admin/ui/table/component_preview.rb b/admin/spec/components/previews/solidus_admin/ui/table/component_preview.rb index c781544b555..c5f357fa6b2 100644 --- a/admin/spec/components/previews/solidus_admin/ui/table/component_preview.rb +++ b/admin/spec/components/previews/solidus_admin/ui/table/component_preview.rb @@ -14,21 +14,25 @@ class SolidusAdmin::UI::Table::ComponentPreview < ViewComponent::Preview def simple render current_component.new( id: 'simple-list', - model_class: Spree::Product, - rows: Array.new(10) { |n| - Spree::Product.new(id: n, name: "Product #{n}", price: n * 10.0, available_on: n.days.ago) + data: { + class: Spree::Product, + rows: Array.new(10) { |n| + Spree::Product.new(id: n, name: "Product #{n}", price: n * 10.0, available_on: n.days.ago) + }, + columns: [ + { header: :id, data: -> { _1.id.to_s } }, + { header: :name, data: :name }, + { header: -> { "Availability at #{Time.current}" }, data: -> { "#{time_ago_in_words _1.available_on} ago" } }, + { header: -> { component("ui/badge").new(name: "$$$") }, data: -> { component("ui/badge").new(name: _1.display_price, color: :green) } }, + { header: "Generated at", data: Time.current.to_s }, + ], + prev: nil, + next: '#2', + }, + search: { + name: :no_key, + url: '#', }, - search_key: :no_key, - search_url: '#', - columns: [ - { header: :id, data: -> { _1.id.to_s } }, - { header: :name, data: :name }, - { header: -> { "Availability at #{Time.current}" }, data: -> { "#{time_ago_in_words _1.available_on} ago" } }, - { header: -> { component("ui/badge").new(name: "$$$") }, data: -> { component("ui/badge").new(name: _1.display_price, color: :green) } }, - { header: "Generated at", data: Time.current.to_s }, - ], - prev_page_link: nil, - next_page_link: '#2', ) end end diff --git a/admin/spec/features/orders_spec.rb b/admin/spec/features/orders_spec.rb index 4761d320585..ddc8f83f5ff 100644 --- a/admin/spec/features/orders_spec.rb +++ b/admin/spec/features/orders_spec.rb @@ -9,6 +9,7 @@ create(:order, number: "R123456789", total: 19.99) visit "/admin/orders" + click_on "In Progress" expect(page).to have_content("R123456789") expect(page).to have_content("$19.99")