From b98605362acb32a48821353fa969cbd0d52e8fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Djalma=20Ara=C3=BAjo?= Date: Wed, 29 Apr 2026 15:33:52 -0300 Subject: [PATCH 1/4] Fix build_query losing array param values on sort/paginate Array#to_s returns inspect output (e.g. '["uuid"]'), which Rails re-parses as a literal string inside the array. Use Array(v) to handle both scalar and array values correctly. --- lib/ruby_ui/data_table/data_table_pagination.rb | 4 +++- lib/ruby_ui/data_table/data_table_sort_head.rb | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/ruby_ui/data_table/data_table_pagination.rb b/lib/ruby_ui/data_table/data_table_pagination.rb index 02625c57..66027ccd 100644 --- a/lib/ruby_ui/data_table/data_table_pagination.rb +++ b/lib/ruby_ui/data_table/data_table_pagination.rb @@ -48,7 +48,9 @@ def page_href(p) end def build_query(hash) - hash.map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join("&") + hash.flat_map { |k, v| + Array(v).map { |val| "#{CGI.escape(k.to_s)}=#{CGI.escape(val.to_s)}" } + }.join("&") end def prev_item diff --git a/lib/ruby_ui/data_table/data_table_sort_head.rb b/lib/ruby_ui/data_table/data_table_sort_head.rb index d74ff0dd..7cd174c7 100644 --- a/lib/ruby_ui/data_table/data_table_sort_head.rb +++ b/lib/ruby_ui/data_table/data_table_sort_head.rb @@ -44,7 +44,9 @@ def sort_href end def build_query(hash) - hash.map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join("&") + hash.flat_map { |k, v| + Array(v).map { |val| "#{CGI.escape(k.to_s)}=#{CGI.escape(val.to_s)}" } + }.join("&") end def sort_icon From 59594646ac8a43597d51048c9cfae31ef785a8ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Djalma=20Ara=C3=BAjo?= Date: Wed, 29 Apr 2026 15:41:53 -0300 Subject: [PATCH 2/4] Move pagination adapters into data_table/ and rename to avoid conflicts Rename DataTablePaginationAdapters::{Pagy,Kaminari,Manual} to DataTable{Pagy,Kaminari,Manual}Adapter under RubyUI namespace. Fixes class name collision when host app includes RubyUI module (e.g. Pagy clashing with the pagy gem). Also fixes generator only copying data_table/ folder, missing the separate adapters dir. --- .../data_table/data_table_kaminari_adapter.rb | 17 +++++++++++++++++ .../data_table/data_table_manual_adapter.rb | 17 +++++++++++++++++ .../data_table/data_table_pagination.rb | 12 ++++++------ .../data_table/data_table_pagy_adapter.rb | 17 +++++++++++++++++ .../kaminari.rb | 19 ------------------- .../data_table_pagination_adapters/manual.rb | 19 ------------------- .../data_table_pagination_adapters/pagy.rb | 19 ------------------- ...rb => data_table_kaminari_adapter_test.rb} | 6 +++--- ...t.rb => data_table_manual_adapter_test.rb} | 10 +++++----- ...est.rb => data_table_pagy_adapter_test.rb} | 6 +++--- 10 files changed, 68 insertions(+), 74 deletions(-) create mode 100644 lib/ruby_ui/data_table/data_table_kaminari_adapter.rb create mode 100644 lib/ruby_ui/data_table/data_table_manual_adapter.rb create mode 100644 lib/ruby_ui/data_table/data_table_pagy_adapter.rb delete mode 100644 lib/ruby_ui/data_table_pagination_adapters/kaminari.rb delete mode 100644 lib/ruby_ui/data_table_pagination_adapters/manual.rb delete mode 100644 lib/ruby_ui/data_table_pagination_adapters/pagy.rb rename test/ruby_ui/{data_table_pagination_adapters/kaminari_test.rb => data_table_kaminari_adapter_test.rb} (70%) rename test/ruby_ui/{data_table_pagination_adapters/manual_test.rb => data_table_manual_adapter_test.rb} (53%) rename test/ruby_ui/{data_table_pagination_adapters/pagy_test.rb => data_table_pagy_adapter_test.rb} (67%) diff --git a/lib/ruby_ui/data_table/data_table_kaminari_adapter.rb b/lib/ruby_ui/data_table/data_table_kaminari_adapter.rb new file mode 100644 index 00000000..6dd83402 --- /dev/null +++ b/lib/ruby_ui/data_table/data_table_kaminari_adapter.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class DataTableKaminariAdapter + def initialize(collection) + @collection = collection + end + + def current_page = @collection.current_page + + def total_pages = @collection.total_pages + + def total_count = @collection.total_count + + def per_page = @collection.limit_value + end +end diff --git a/lib/ruby_ui/data_table/data_table_manual_adapter.rb b/lib/ruby_ui/data_table/data_table_manual_adapter.rb new file mode 100644 index 00000000..46b859e6 --- /dev/null +++ b/lib/ruby_ui/data_table/data_table_manual_adapter.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class DataTableManualAdapter + attr_reader :current_page, :per_page, :total_count + + def initialize(page:, per_page:, total_count:) + @current_page = page.to_i + @per_page = [per_page.to_i, 1].max + @total_count = total_count.to_i + end + + def total_pages + [(@total_count.to_f / @per_page).ceil, 1].max + end + end +end diff --git a/lib/ruby_ui/data_table/data_table_pagination.rb b/lib/ruby_ui/data_table/data_table_pagination.rb index 66027ccd..d6114b35 100644 --- a/lib/ruby_ui/data_table/data_table_pagination.rb +++ b/lib/ruby_ui/data_table/data_table_pagination.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true require "cgi" -require_relative "../data_table_pagination_adapters/manual" -require_relative "../data_table_pagination_adapters/pagy" -require_relative "../data_table_pagination_adapters/kaminari" +require_relative "data_table_manual_adapter" +require_relative "data_table_pagy_adapter" +require_relative "data_table_kaminari_adapter" module RubyUI class DataTablePagination < Base @@ -30,10 +30,10 @@ def view_template def resolve_adapter(with:, pagy:, kaminari:, page:, per_page:, total_count:) return with if with - return RubyUI::DataTablePaginationAdapters::Pagy.new(pagy) if pagy - return RubyUI::DataTablePaginationAdapters::Kaminari.new(kaminari) if kaminari + return RubyUI::DataTablePagyAdapter.new(pagy) if pagy + return RubyUI::DataTableKaminariAdapter.new(kaminari) if kaminari if page && per_page && total_count - return RubyUI::DataTablePaginationAdapters::Manual.new(page:, per_page:, total_count:) + return RubyUI::DataTableManualAdapter.new(page:, per_page:, total_count:) end raise ArgumentError, "DataTablePagination requires one of: with:, pagy:, kaminari:, or page:+per_page:+total_count:" end diff --git a/lib/ruby_ui/data_table/data_table_pagy_adapter.rb b/lib/ruby_ui/data_table/data_table_pagy_adapter.rb new file mode 100644 index 00000000..fad905d3 --- /dev/null +++ b/lib/ruby_ui/data_table/data_table_pagy_adapter.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class DataTablePagyAdapter + def initialize(pagy) + @pagy = pagy + end + + def current_page = @pagy.page + + def total_pages = @pagy.pages + + def total_count = @pagy.count + + def per_page = @pagy.items + end +end diff --git a/lib/ruby_ui/data_table_pagination_adapters/kaminari.rb b/lib/ruby_ui/data_table_pagination_adapters/kaminari.rb deleted file mode 100644 index 85cc2b14..00000000 --- a/lib/ruby_ui/data_table_pagination_adapters/kaminari.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module RubyUI - module DataTablePaginationAdapters - class Kaminari - def initialize(collection) - @collection = collection - end - - def current_page = @collection.current_page - - def total_pages = @collection.total_pages - - def total_count = @collection.total_count - - def per_page = @collection.limit_value - end - end -end diff --git a/lib/ruby_ui/data_table_pagination_adapters/manual.rb b/lib/ruby_ui/data_table_pagination_adapters/manual.rb deleted file mode 100644 index b038ff1c..00000000 --- a/lib/ruby_ui/data_table_pagination_adapters/manual.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module RubyUI - module DataTablePaginationAdapters - class Manual - attr_reader :current_page, :per_page, :total_count - - def initialize(page:, per_page:, total_count:) - @current_page = page.to_i - @per_page = [per_page.to_i, 1].max - @total_count = total_count.to_i - end - - def total_pages - [(@total_count.to_f / @per_page).ceil, 1].max - end - end - end -end diff --git a/lib/ruby_ui/data_table_pagination_adapters/pagy.rb b/lib/ruby_ui/data_table_pagination_adapters/pagy.rb deleted file mode 100644 index a6e0f0a7..00000000 --- a/lib/ruby_ui/data_table_pagination_adapters/pagy.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module RubyUI - module DataTablePaginationAdapters - class Pagy - def initialize(pagy) - @pagy = pagy - end - - def current_page = @pagy.page - - def total_pages = @pagy.pages - - def total_count = @pagy.count - - def per_page = @pagy.items - end - end -end diff --git a/test/ruby_ui/data_table_pagination_adapters/kaminari_test.rb b/test/ruby_ui/data_table_kaminari_adapter_test.rb similarity index 70% rename from test/ruby_ui/data_table_pagination_adapters/kaminari_test.rb rename to test/ruby_ui/data_table_kaminari_adapter_test.rb index 8ff7ac05..8b568e50 100644 --- a/test/ruby_ui/data_table_pagination_adapters/kaminari_test.rb +++ b/test/ruby_ui/data_table_kaminari_adapter_test.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true require "test_helper" -require "ruby_ui/data_table_pagination_adapters/kaminari" +require "ruby_ui/data_table/data_table_kaminari_adapter" -class RubyUI::DataTablePaginationAdapters::KaminariTest < ComponentTest +class RubyUI::DataTableKaminariAdapterTest < ComponentTest CollectionDouble = Data.define(:current_page, :total_pages, :total_count, :limit_value) def test_reads_current_page_total_pages_total_count_limit_value coll = CollectionDouble.new(current_page: 3, total_pages: 7, total_count: 61, limit_value: 10) - adapter = RubyUI::DataTablePaginationAdapters::Kaminari.new(coll) + adapter = RubyUI::DataTableKaminariAdapter.new(coll) assert_equal 3, adapter.current_page assert_equal 7, adapter.total_pages assert_equal 61, adapter.total_count diff --git a/test/ruby_ui/data_table_pagination_adapters/manual_test.rb b/test/ruby_ui/data_table_manual_adapter_test.rb similarity index 53% rename from test/ruby_ui/data_table_pagination_adapters/manual_test.rb rename to test/ruby_ui/data_table_manual_adapter_test.rb index 35d7f916..d90bf8ea 100644 --- a/test/ruby_ui/data_table_pagination_adapters/manual_test.rb +++ b/test/ruby_ui/data_table_manual_adapter_test.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true require "test_helper" -require "ruby_ui/data_table_pagination_adapters/manual" +require "ruby_ui/data_table/data_table_manual_adapter" -class RubyUI::DataTablePaginationAdapters::ManualTest < ComponentTest +class RubyUI::DataTableManualAdapterTest < ComponentTest def test_computes_total_pages_from_total_count_and_per_page - adapter = RubyUI::DataTablePaginationAdapters::Manual.new(page: 2, per_page: 10, total_count: 25) + adapter = RubyUI::DataTableManualAdapter.new(page: 2, per_page: 10, total_count: 25) assert_equal 2, adapter.current_page assert_equal 10, adapter.per_page assert_equal 25, adapter.total_count @@ -13,12 +13,12 @@ def test_computes_total_pages_from_total_count_and_per_page end def test_total_pages_is_at_least_1_for_empty_total - adapter = RubyUI::DataTablePaginationAdapters::Manual.new(page: 1, per_page: 10, total_count: 0) + adapter = RubyUI::DataTableManualAdapter.new(page: 1, per_page: 10, total_count: 0) assert_equal 1, adapter.total_pages end def test_coerces_integer_inputs - adapter = RubyUI::DataTablePaginationAdapters::Manual.new(page: "3", per_page: "5", total_count: "12") + adapter = RubyUI::DataTableManualAdapter.new(page: "3", per_page: "5", total_count: "12") assert_equal 3, adapter.current_page assert_equal 3, adapter.total_pages end diff --git a/test/ruby_ui/data_table_pagination_adapters/pagy_test.rb b/test/ruby_ui/data_table_pagy_adapter_test.rb similarity index 67% rename from test/ruby_ui/data_table_pagination_adapters/pagy_test.rb rename to test/ruby_ui/data_table_pagy_adapter_test.rb index 2c9c1682..833f0dde 100644 --- a/test/ruby_ui/data_table_pagination_adapters/pagy_test.rb +++ b/test/ruby_ui/data_table_pagy_adapter_test.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true require "test_helper" -require "ruby_ui/data_table_pagination_adapters/pagy" +require "ruby_ui/data_table/data_table_pagy_adapter" -class RubyUI::DataTablePaginationAdapters::PagyTest < ComponentTest +class RubyUI::DataTablePagyAdapterTest < ComponentTest PagyDouble = Data.define(:page, :pages, :count, :items) def test_reads_page_pages_count_items pagy = PagyDouble.new(page: 2, pages: 5, count: 47, items: 10) - adapter = RubyUI::DataTablePaginationAdapters::Pagy.new(pagy) + adapter = RubyUI::DataTablePagyAdapter.new(pagy) assert_equal 2, adapter.current_page assert_equal 5, adapter.total_pages assert_equal 47, adapter.total_count From a626a9dc899f7aab6f117fa3ac325eddad81d590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Djalma=20Ara=C3=BAjo?= Date: Wed, 29 Apr 2026 15:42:23 -0300 Subject: [PATCH 3/4] Add customizable prev/next labels and hide pagination for single page Add prev_label and next_label params (default "<" and ">"). Skip rendering when total pages <= 1. --- lib/ruby_ui/data_table/data_table_pagination.rb | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/ruby_ui/data_table/data_table_pagination.rb b/lib/ruby_ui/data_table/data_table_pagination.rb index d6114b35..0c585362 100644 --- a/lib/ruby_ui/data_table/data_table_pagination.rb +++ b/lib/ruby_ui/data_table/data_table_pagination.rb @@ -7,16 +7,20 @@ module RubyUI class DataTablePagination < Base - def initialize(with: nil, pagy: nil, kaminari: nil, page: nil, per_page: nil, total_count: nil, page_param: "page", path: "", query: {}, window: 1, **attrs) + def initialize(with: nil, pagy: nil, kaminari: nil, page: nil, per_page: nil, total_count: nil, page_param: "page", path: "", query: {}, window: 1, prev_label: "<", next_label: ">", **attrs) @adapter = resolve_adapter(with:, pagy:, kaminari:, page:, per_page:, total_count:) @page_param = page_param @path = path @query = query.to_h.transform_keys(&:to_s) @window = window + @prev_label = prev_label + @next_label = next_label super(**attrs) end def view_template + return if total <= 1 + render RubyUI::Pagination.new(class: "mx-0 w-auto justify-end", **attrs) do render RubyUI::PaginationContent.new do prev_item @@ -56,20 +60,20 @@ def build_query(hash) def prev_item if current <= 1 li do - span(class: "opacity-50 pointer-events-none px-3 h-9 inline-flex items-center text-sm") { plain "Previous" } + span(class: "opacity-50 pointer-events-none px-3 h-9 inline-flex items-center text-sm") { plain @prev_label } end else - render RubyUI::PaginationItem.new(href: page_href(current - 1)) { plain "Previous" } + render RubyUI::PaginationItem.new(href: page_href(current - 1)) { plain @prev_label } end end def next_item if current >= total li do - span(class: "opacity-50 pointer-events-none px-3 h-9 inline-flex items-center text-sm") { plain "Next" } + span(class: "opacity-50 pointer-events-none px-3 h-9 inline-flex items-center text-sm") { plain @next_label } end else - render RubyUI::PaginationItem.new(href: page_href(current + 1)) { plain "Next" } + render RubyUI::PaginationItem.new(href: page_href(current + 1)) { plain @next_label } end end From 4940ef2438a8e37c062b3f0a831a460adaa2a1aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Djalma=20Ara=C3=BAjo?= Date: Wed, 29 Apr 2026 16:07:29 -0300 Subject: [PATCH 4/4] Remove unnecessary plain calls for label ivars in pagination Phlex renders ivars directly in tag blocks without plain. --- lib/ruby_ui/data_table/data_table_pagination.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/ruby_ui/data_table/data_table_pagination.rb b/lib/ruby_ui/data_table/data_table_pagination.rb index 0c585362..af12d3f5 100644 --- a/lib/ruby_ui/data_table/data_table_pagination.rb +++ b/lib/ruby_ui/data_table/data_table_pagination.rb @@ -60,20 +60,20 @@ def build_query(hash) def prev_item if current <= 1 li do - span(class: "opacity-50 pointer-events-none px-3 h-9 inline-flex items-center text-sm") { plain @prev_label } + span(class: "opacity-50 pointer-events-none px-3 h-9 inline-flex items-center text-sm") { @prev_label } end else - render RubyUI::PaginationItem.new(href: page_href(current - 1)) { plain @prev_label } + render RubyUI::PaginationItem.new(href: page_href(current - 1)) { @prev_label } end end def next_item if current >= total li do - span(class: "opacity-50 pointer-events-none px-3 h-9 inline-flex items-center text-sm") { plain @next_label } + span(class: "opacity-50 pointer-events-none px-3 h-9 inline-flex items-center text-sm") { @next_label } end else - render RubyUI::PaginationItem.new(href: page_href(current + 1)) { plain @next_label } + render RubyUI::PaginationItem.new(href: page_href(current + 1)) { @next_label } end end