Skip to content
Open
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
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ gem 'jbuilder'
gem 'bcrypt', '~> 3.1.7'

# Pagination [https://ddnexus.github.io/pagy/]
gem 'pagy', '~> 9.3'
gem 'pagy', '~> 43.0'

# Charts [https://chartkick.com/]
gem 'chartkick'
Expand Down
10 changes: 7 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,9 @@ GEM
racc (~> 1.4)
noticed (3.0.0)
rails (>= 6.1.0)
pagy (9.4.0)
pagy (43.2.4)
json
yaml
parallel (1.27.0)
parser (3.3.10.0)
ast (~> 2.4.1)
Expand Down Expand Up @@ -371,6 +373,7 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
yaml (0.4.0)
zeitwerk (2.7.4)

PLATFORMS
Expand Down Expand Up @@ -400,7 +403,7 @@ DEPENDENCIES
jbuilder
minitest (~> 5.25)
noticed (~> 3.0)
pagy (~> 9.3)
pagy (~> 43.0)
pg (~> 1.1)
propshaft
puma (>= 5.0)
Expand Down Expand Up @@ -496,7 +499,7 @@ CHECKSUMS
nokogiri (1.19.0-x86_64-linux-gnu) sha256=f482b95c713d60031d48c44ce14562f8d2ce31e3a9e8dd0ccb131e9e5a68b58c
nokogiri (1.19.0-x86_64-linux-musl) sha256=1c4ca6b381622420073ce6043443af1d321e8ed93cc18b08e2666e5bd02ffae4
noticed (3.0.0) sha256=cbb28f1624b9034117fbe7e1d36facfe624e9ad53a24aa149e2a676336ff8f98
pagy (9.4.0) sha256=db3f2e043f684155f18f78be62a81e8d033e39b9f97b1e1a8d12ad38d7bce738
pagy (43.2.4) sha256=b0574f8105e5cc45ed37c4633789a7a64cb517b9e5af2bf697b22c5e880c5f0f
parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
parser (3.3.10.0) sha256=ce3587fa5cc55a88c4ba5b2b37621b3329aadf5728f9eafa36bbd121462aabd6
pg (1.6.3) sha256=1388d0563e13d2758c1089e35e973a3249e955c659592d10e5b77c468f628a99
Expand Down Expand Up @@ -569,6 +572,7 @@ CHECKSUMS
websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962
websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e
yaml (0.4.0) sha256=240e69d1e6ce3584d6085978719a0faa6218ae426e034d8f9b02fb54d3471942
zeitwerk (2.7.4) sha256=2bef90f356bdafe9a6c2bd32bcd804f83a4f9b8bc27f3600fff051eb3edcec8b

BUNDLED WITH
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class ApplicationController < ActionController::Base
include Authentication
include Breadcrumbs
include Pagy::Backend
include Pagy::Method
include Authorizable
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
allow_browser versions: :modern
Expand Down
6 changes: 3 additions & 3 deletions app/controllers/problems_controller.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class ProblemsController < ApplicationController
include Pagy::Backend
include Pagy::Method

rescue_from Pagy::OverflowError, Pagy::VariableError, with: :redirect_to_first_page
rescue_from Pagy::RangeError, Pagy::OptionError, with: :redirect_to_first_page

before_action :set_app
before_action :set_problem, only: [ :show, :resolve, :unresolve ]
Expand Down Expand Up @@ -57,7 +57,7 @@ def index
end

# Pagination with Pagy
@pagy, @problems = pagy(@problems)
@pagy, @problems = pagy(:offset, @problems)
end

def show
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class UsersController < ApplicationController

def index
@users = User.all.order(created_at: :desc)
@pagy, @users = pagy(@users)
@pagy, @users = pagy(:offset, @users)
end

def show
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module ApplicationHelper
include Pagy::Frontend
include PagyTailwind

def nav_link_classes(active:)
base = 'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold transition-colors'
Expand Down
26 changes: 11 additions & 15 deletions config/initializers/pagy.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
# frozen_string_literal: true

# Pagy initializer file
# See https://ddnexus.github.io/pagy/guides/quick-start/
# Pagy initializer file (v43+)
# See https://ddnexus.github.io/pagy/guides/upgrade-guide/

# Instance variables (defaults shown)
# Pagy::DEFAULT[:limit] = 20 # items per page
# Pagy::DEFAULT[:size] = 7 # nav pages shown
# Pagy::DEFAULT[:ends] = true # if false will show only inner pages

Pagy::DEFAULT[:limit] = 25
# Instance variables
Pagy.options[:limit] = 25
Pagy.options[:raise_range_error] = true # Raise errors for out-of-range pages (enables redirect behavior)

# Custom Tailwind-styled pagination helper
module PagyTailwind
def pagy_tailwind_nav(pagy, id: nil, aria_label: nil, **vars)
p_prev = pagy.prev
p_prev = pagy.previous
p_next = pagy.next

html = +%(<nav#{id ? %( id="#{id}") : ''} class="flex items-center justify-center space-x-1" #{
Expand All @@ -27,13 +24,14 @@ def pagy_tailwind_nav(pagy, id: nil, aria_label: nil, **vars)
html << %(<span class="px-3 py-2 rounded-lg border border-gray-200 dark:border-zinc-700 text-gray-400 dark:text-zinc-600 cursor-not-allowed">&lsaquo; Prev</span>)
end

# Page links
pagy.series(**vars).each do |item|
# Page links (series is protected in pagy v43, use send to access it)
pagy.send(:series, **vars).each do |item|
html << case item
when Integer
pagy_tailwind_link(pagy, item, item.to_s, 'rounded-lg border border-gray-300 dark:border-zinc-600 text-gray-700 dark:text-zinc-300 hover:bg-gray-50 dark:hover:bg-zinc-700')
when String
%(<span class="px-3 py-2 rounded-lg bg-violet-600 text-white font-medium">#{pagy.label_for(item)}</span>)
# Current page - item is already the page number as a string
%(<span class="px-3 py-2 rounded-lg bg-violet-600 text-white font-medium">#{item}</span>)
when :gap
%(<span class="px-3 py-2 text-gray-500 dark:text-zinc-500">&hellip;</span>)
end
Expand All @@ -51,7 +49,7 @@ def pagy_tailwind_nav(pagy, id: nil, aria_label: nil, **vars)
end

def pagy_tailwind_link(pagy, page, text, classes)
%(<a href="#{pagy_url_for(pagy, page)}" class="px-3 py-2 text-sm transition-colors #{classes}">#{text}</a>)
%(<a href="#{pagy.page_url(page)}" class="px-3 py-2 text-sm transition-colors #{classes}">#{text}</a>)
end

def pagy_tailwind_info(pagy)
Expand All @@ -62,5 +60,3 @@ def pagy_tailwind_info(pagy)
%(<span class="text-sm text-gray-600 dark:text-zinc-400">Showing #{from} to #{to} of #{count} results</span>).html_safe
end
end

Pagy::Frontend.prepend(PagyTailwind)
5 changes: 3 additions & 2 deletions test/controllers/problems_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -355,9 +355,10 @@ class ProblemsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end

test 'index handles invalid page numbers by redirecting to first page' do
test 'index handles invalid page numbers by normalizing to first page' do
# Pagy 43+ normalizes negative page numbers to page 1 instead of raising errors
get app_problems_path(@app, page: -1)
assert_redirected_to app_problems_path(@app)
assert_response :success
end

test 'index handles overflow page numbers by redirecting to first page' do
Expand Down
113 changes: 113 additions & 0 deletions test/helpers/pagy_tailwind_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# frozen_string_literal: true

require 'test_helper'
require 'pagy/classes/request'
require 'pagy/toolbox/helpers/support/series'
require 'pagy/toolbox/helpers/support/a_lambda'

class PagyTailwindTest < ActionView::TestCase
include PagyTailwind

test 'pagy_tailwind_nav renders navigation with previous and next links' do
pagy = create_pagy(count: 100, page: 2, limit: 10)

html = pagy_tailwind_nav(pagy)

assert_includes html, 'aria-label="Pagination"'
assert_includes html, '&lsaquo; Prev'
assert_includes html, 'Next &rsaquo;'
assert_includes html, 'href='
assert html.html_safe?
end

test 'pagy_tailwind_nav disables previous link on first page' do
pagy = create_pagy(count: 100, page: 1, limit: 10)

html = pagy_tailwind_nav(pagy)

# Previous should be a span (disabled), not a link
assert_includes html, '<span class="px-3 py-2 rounded-lg border border-gray-200 dark:border-zinc-700 text-gray-400 dark:text-zinc-600 cursor-not-allowed">&lsaquo; Prev</span>'
# Next should be a link
assert_includes html, 'Next &rsaquo;</a>'
end

test 'pagy_tailwind_nav disables next link on last page' do
pagy = create_pagy(count: 30, page: 3, limit: 10)

html = pagy_tailwind_nav(pagy)

# Previous should be a link
assert_includes html, '&lsaquo; Prev</a>'
# Next should be a span (disabled)
assert_includes html, '<span class="px-3 py-2 rounded-lg border border-gray-200 dark:border-zinc-700 text-gray-400 dark:text-zinc-600 cursor-not-allowed">Next &rsaquo;</span>'
end

test 'pagy_tailwind_nav highlights current page' do
pagy = create_pagy(count: 100, page: 3, limit: 10)

html = pagy_tailwind_nav(pagy)

# Current page should be highlighted with violet background
assert_includes html, 'bg-violet-600 text-white font-medium'
end

test 'pagy_tailwind_nav renders gap for many pages' do
pagy = create_pagy(count: 500, page: 10, limit: 10)

html = pagy_tailwind_nav(pagy)

# Should include ellipsis for gaps
assert_includes html, '&hellip;'
end

test 'pagy_tailwind_nav accepts custom id' do
pagy = create_pagy(count: 100, page: 1, limit: 10)

html = pagy_tailwind_nav(pagy, id: 'custom-pagination')

assert_includes html, 'id="custom-pagination"'
end

test 'pagy_tailwind_nav accepts custom aria_label' do
pagy = create_pagy(count: 100, page: 1, limit: 10)

html = pagy_tailwind_nav(pagy, aria_label: 'Problem pages')

assert_includes html, 'aria-label="Problem pages"'
end

test 'pagy_tailwind_info shows correct range and total' do
pagy = create_pagy(count: 100, page: 2, limit: 10)

html = pagy_tailwind_info(pagy)

assert_includes html, 'Showing 11 to 20 of 100 results'
assert html.html_safe?
end

test 'pagy_tailwind_info shows correct info for first page' do
pagy = create_pagy(count: 50, page: 1, limit: 25)

html = pagy_tailwind_info(pagy)

assert_includes html, 'Showing 1 to 25 of 50 results'
end

test 'pagy_tailwind_info shows correct info for last partial page' do
pagy = create_pagy(count: 33, page: 2, limit: 25)

html = pagy_tailwind_info(pagy)

assert_includes html, 'Showing 26 to 33 of 33 results'
end

private

def create_pagy(count:, page:, limit:)
# Build options hash similar to how Pagy::Method does it
options = Pagy.options.merge(count:, page:, limit:)
options[:request] = { base_url: 'http://test.host', path: '/test', params: {} }
options[:request] = Pagy::Request.new(options)
Pagy::Offset.new(**options)
end
end