From d30d82d2774ee3d3d2f2b22087be887a732e769c Mon Sep 17 00:00:00 2001 From: Ernesto Tagwerker Date: Thu, 16 Jul 2026 23:15:21 -0400 Subject: [PATCH 1/6] Upgrade madmin to v2.x --- Gemfile | 2 +- Gemfile.lock | 29 +++++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index fd18f501..09e1eb54 100644 --- a/Gemfile +++ b/Gemfile @@ -108,4 +108,4 @@ gem "coffee-script" gem "pundit", "~> 2.2" -gem "madmin", "~> 1.2" +gem "madmin", "~> 2.0" diff --git a/Gemfile.lock b/Gemfile.lock index afb692a8..1d4f684c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -166,6 +166,10 @@ GEM logger i18n (1.15.2) concurrent-ruby (~> 1.0) + importmap-rails (2.2.3) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) io-console (0.8.2) irb (1.18.0) pp (>= 0.6.0) @@ -195,9 +199,13 @@ GEM loofah (2.25.2) crass (~> 1.0.2) nokogiri (>= 1.12.0) - madmin (1.2.8) + madmin (2.3.3) + importmap-rails pagy (>= 3.5) - rails (>= 6.0.3) + propshaft + rails (>= 7.0.0) + stimulus-rails + turbo-rails mail (2.9.1) logger mini_mime (>= 0.1.1) @@ -273,7 +281,10 @@ GEM actionpack (>= 4.2) omniauth (~> 2.0) orm_adapter (0.5.0) - pagy (6.1.0) + pagy (43.6.0) + json + uri + yaml parallel (2.1.0) parser (3.3.12.0) ast (~> 2.4.1) @@ -283,6 +294,10 @@ GEM prettyprint prettyprint (0.2.0) prism (1.9.0) + propshaft (1.3.2) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack public_suffix (7.0.5) puma (7.2.1) nio4r (~> 2.0) @@ -456,12 +471,17 @@ GEM rubocop-performance (~> 1.26.0) standardrb (1.0.1) standard + stimulus-rails (1.3.4) + railties (>= 6.0.0) terser (1.1.18) execjs (>= 0.3.0, < 3) thor (1.5.0) tilt (2.3.0) timeout (0.6.1) tsort (0.2.0) + turbo-rails (2.0.23) + actionpack (>= 7.1.0) + railties (>= 7.1.0) turbolinks (5.2.1) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) @@ -492,6 +512,7 @@ GEM websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) + yaml (0.4.0) zeitwerk (2.8.2) PLATFORMS @@ -520,7 +541,7 @@ DEPENDENCIES jquery-rails jquery-ui-rails (~> 8.0) listen (~> 3.7) - madmin (~> 1.2) + madmin (~> 2.0) matrix mimemagic (~> 0.3.8) newrelic_rpm From 2a26ecd032c2d61e48e53350666d301ddbd690a8 Mon Sep 17 00:00:00 2001 From: Ernesto Tagwerker Date: Thu, 16 Jul 2026 23:22:08 -0400 Subject: [PATCH 2/6] Fix asset precompilation with madmin 2.3.3 Upgraded sass-rails from 5.0 to 6.0 (which uses sassc-rails 2.1) to support modern CSS features like rgb() syntax used in madmin 2.3.3. Updated SCSS files to properly escape calc() and min() expressions using unquote() to prevent SassC from trying to evaluate unit-mixed expressions at compile time. Added variables import to project.scss since each SCSS file is compiled independently. Co-Authored-By: Claude Haiku 4.5 --- Gemfile | 2 +- Gemfile.lock | 34 +++++++++---------- .../stylesheets/1-utilities/_basic.scss | 2 +- .../stylesheets/4-molecules/_floors.scss | 2 +- app/assets/stylesheets/project.scss | 2 ++ app/assets/stylesheets/stories.scss | 2 +- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Gemfile b/Gemfile index 09e1eb54..4235abf9 100644 --- a/Gemfile +++ b/Gemfile @@ -29,7 +29,7 @@ gem "bootstrap-sass", "3.4.1" # Use Puma as the app server gem "puma", "~> 7.2" # Use SCSS for stylesheets -gem "sass-rails", "~> 5.0" +gem "sass-rails", "~> 6.0" gem "bourbon" gem "matrix" diff --git a/Gemfile.lock b/Gemfile.lock index 1d4f684c..2634140d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -413,19 +413,16 @@ GEM rubocop-ast (>= 1.47.1, < 2.0) ruby-progressbar (1.13.0) rubyzip (2.3.2) - sass (3.7.4) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sass-rails (5.1.0) - railties (>= 5.2.0) - sass (~> 3.1) - sprockets (>= 2.8, < 4.0) - sprockets-rails (>= 2.0, < 4.0) - tilt (>= 1.1, < 3) + sass-rails (6.0.0) + sassc-rails (~> 2.1, >= 2.1.1) sassc (2.4.0) ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt securerandom (0.4.1) selenium-webdriver (4.10.0) rexml (~> 3.2, >= 3.2.5) @@ -449,12 +446,13 @@ GEM hashie (>= 0.1.0, < 6) version_gem (~> 1.1, >= 1.1.14) spring (3.0.0) - sprockets (3.7.2) + sprockets (4.2.2) concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) + logger + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) sprockets (>= 3.0.0) stackprof (0.2.26) standard (1.56.0) @@ -476,7 +474,7 @@ GEM terser (1.1.18) execjs (>= 0.3.0, < 3) thor (1.5.0) - tilt (2.3.0) + tilt (2.8.0) timeout (0.6.1) tsort (0.2.0) turbo-rails (2.0.23) @@ -557,7 +555,7 @@ DEPENDENCIES recursive-open-struct redcarpet (~> 3.5.1) rspec-rails (~> 6.1) - sass-rails (~> 5.0) + sass-rails (~> 6.0) sentry-rails sentry-ruby shoulda-matchers (~> 3.1) diff --git a/app/assets/stylesheets/1-utilities/_basic.scss b/app/assets/stylesheets/1-utilities/_basic.scss index ddedbd03..6dea1aae 100644 --- a/app/assets/stylesheets/1-utilities/_basic.scss +++ b/app/assets/stylesheets/1-utilities/_basic.scss @@ -7,7 +7,7 @@ box-sizing: border-box; } main { - min-height: calc(100vh - 160px); + min-height: unquote("calc(100vh - 160px)"); padding-bottom: 60px; } ::selection { diff --git a/app/assets/stylesheets/4-molecules/_floors.scss b/app/assets/stylesheets/4-molecules/_floors.scss index 7963bd85..b00ddbdf 100644 --- a/app/assets/stylesheets/4-molecules/_floors.scss +++ b/app/assets/stylesheets/4-molecules/_floors.scss @@ -13,7 +13,7 @@ justify-content: center; width: 100%; overflow: hidden; - height: calc(100vh - 160px); + height: unquote("calc(100vh - 160px)"); margin: -100px 0; } } diff --git a/app/assets/stylesheets/project.scss b/app/assets/stylesheets/project.scss index 1a128dd9..a99ec534 100644 --- a/app/assets/stylesheets/project.scss +++ b/app/assets/stylesheets/project.scss @@ -2,6 +2,8 @@ // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ +@import "1-utilities/variables"; + .projects-controller { &.index-action { .btn-group { diff --git a/app/assets/stylesheets/stories.scss b/app/assets/stylesheets/stories.scss index 3ace7f06..ce3bed27 100644 --- a/app/assets/stylesheets/stories.scss +++ b/app/assets/stylesheets/stories.scss @@ -113,7 +113,7 @@ .extra_info_preview .content, .story_preview .content { overflow: auto; - max-height: min(50vh, 700px); + max-height: unquote("min(50vh, 700px)"); // prevent long links from overflowing a { word-break: break-all; From 98bbed244dd1b36b283a4fded65e52238128bd28 Mon Sep 17 00:00:00 2001 From: Ernesto Tagwerker Date: Thu, 16 Jul 2026 23:26:17 -0400 Subject: [PATCH 3/6] Disable SassC CSS compressor to fix production asset precompilation The SassC/libsass CSS compressor re-parses already-compiled CSS and evaluates arithmetic inside calc()/min() (e.g. `100vh - 160px`), raising "Incompatible units: 'px' and 'vh'" on otherwise valid CSS. This only manifests during production precompilation, since the compressor does not run in development. Setting css_compressor to nil explicitly (sprockets-rails defaults to :sass when sassc is present) skips CSS compression while keeping terser for JS. Co-Authored-By: Claude Opus 4.8 (1M context) --- config/environments/production.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index 49f47c6c..ce3ffeb6 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -18,7 +18,11 @@ # Apache or NGINX already handles this. config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? - config.assets.css_compressor = :sass + # Do not use the SassC/libsass CSS compressor: it re-parses already-compiled + # CSS and evaluates arithmetic inside calc()/min() (e.g. `100vh - 160px`), + # raising "Incompatible units" on valid CSS. Set to nil explicitly since + # sprockets-rails otherwise defaults to :sass when sassc is available. + config.assets.css_compressor = nil config.assets.js_compressor = :terser # Do not fallback to assets pipeline if a precompiled asset is missed. From 75498f44074cd6c855963df4cc42ea9bc76c2583 Mon Sep 17 00:00:00 2001 From: Ernesto Tagwerker Date: Thu, 16 Jul 2026 23:38:55 -0400 Subject: [PATCH 4/6] Patch for madmin v2.x --- app/views/madmin/application/_javascript.html.erb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/views/madmin/application/_javascript.html.erb b/app/views/madmin/application/_javascript.html.erb index fb8d4fd5..127d1e37 100644 --- a/app/views/madmin/application/_javascript.html.erb +++ b/app/views/madmin/application/_javascript.html.erb @@ -1,3 +1,6 @@ +<%# madmin's npm_rails_version helper was removed in 2.x; reproduce it here. + Pins @rails/* unpkg packages to the matching Rails version (or "main" for prereleases). %> +<% npm_rails_version = Rails.version.match?(/\A\d+\.\d+\.\d+\z/) ? Rails.version : "main" %> <%= stylesheet_link_tag "https://unpkg.com/flatpickr/dist/flatpickr.min.css", "data-turbo-track": "reload" %> <%= stylesheet_link_tag "https://unpkg.com/trix/dist/trix.css", "data-turbo-track": "reload" %> From 20bd219ae942c7efc0bfb5fee0d7eda0ba2717e2 Mon Sep 17 00:00:00 2001 From: Ernesto Tagwerker Date: Thu, 16 Jul 2026 23:47:01 -0400 Subject: [PATCH 5/6] Add request specs for the madmin interface Cover every route under /madmin with integration (request) specs: - Authorization: unauthenticated and non-admin users are redirected to the root path; admins get access across all controllers. - Dashboard: GET /madmin renders successfully (guards against layout / partial regressions like the removed npm_rails_version helper). - Each resource (projects, stories, estimates, users, version_jumps): index/new/show, plus create/update/destroy where those routes exist. Co-Authored-By: Claude Opus 4.8 (1M context) --- spec/requests/madmin/authorization_spec.rb | 56 +++++++++++++++ spec/requests/madmin/dashboard_spec.rb | 20 ++++++ spec/requests/madmin/estimates_spec.rb | 44 ++++++++++++ spec/requests/madmin/projects_spec.rb | 45 ++++++++++++ spec/requests/madmin/stories_spec.rb | 83 ++++++++++++++++++++++ spec/requests/madmin/users_spec.rb | 44 ++++++++++++ spec/requests/madmin/version_jumps_spec.rb | 81 +++++++++++++++++++++ 7 files changed, 373 insertions(+) create mode 100644 spec/requests/madmin/authorization_spec.rb create mode 100644 spec/requests/madmin/dashboard_spec.rb create mode 100644 spec/requests/madmin/estimates_spec.rb create mode 100644 spec/requests/madmin/projects_spec.rb create mode 100644 spec/requests/madmin/stories_spec.rb create mode 100644 spec/requests/madmin/users_spec.rb create mode 100644 spec/requests/madmin/version_jumps_spec.rb diff --git a/spec/requests/madmin/authorization_spec.rb b/spec/requests/madmin/authorization_spec.rb new file mode 100644 index 00000000..bd5bc677 --- /dev/null +++ b/spec/requests/madmin/authorization_spec.rb @@ -0,0 +1,56 @@ +require "rails_helper" + +# The madmin interface is gated by Madmin::ApplicationController#ensure_admin, +# which redirects anyone who is not a signed-in admin back to the root path. +# These specs lock that behavior down across a representative set of routes. +RSpec.describe "Madmin authorization", type: :request do + after { Warden.test_reset! } + + # One route per controller under /madmin, enough to prove the shared + # before_action guards every resource and the dashboard. + let(:guarded_paths) do + [ + "/madmin", + "/madmin/projects", + "/madmin/stories", + "/madmin/estimates", + "/madmin/users", + "/madmin/version_jumps" + ] + end + + context "when not signed in" do + it "redirects every madmin path to the root path" do + guarded_paths.each do |path| + get path + expect(response).to redirect_to("/"), "expected #{path} to redirect to /" + end + end + end + + context "when signed in as a non-admin user" do + let(:user) { FactoryBot.create(:user, admin: false) } + + before { login_as(user, scope: :user) } + + it "redirects every madmin path to the root path" do + guarded_paths.each do |path| + get path + expect(response).to redirect_to("/"), "expected #{path} to redirect to /" + end + end + end + + context "when signed in as an admin user" do + let(:admin) { FactoryBot.create(:user, :admin) } + + before { login_as(admin, scope: :user) } + + it "grants access to every madmin path" do + guarded_paths.each do |path| + get path + expect(response).to have_http_status(:ok), "expected #{path} to return 200" + end + end + end +end diff --git a/spec/requests/madmin/dashboard_spec.rb b/spec/requests/madmin/dashboard_spec.rb new file mode 100644 index 00000000..9cd29bd3 --- /dev/null +++ b/spec/requests/madmin/dashboard_spec.rb @@ -0,0 +1,20 @@ +require "rails_helper" + +# The dashboard renders the madmin layout (including _javascript.html.erb), +# so this request spec also guards against layout/partial regressions such as +# the removed `npm_rails_version` helper. +RSpec.describe "Madmin dashboard", type: :request do + let(:admin) { FactoryBot.create(:user, :admin) } + + before { login_as(admin, scope: :user) } + after { Warden.test_reset! } + + describe "GET /madmin" do + it "renders the dashboard successfully" do + get "/madmin" + + expect(response).to have_http_status(:ok) + expect(response.body).to include("madmin") + end + end +end diff --git a/spec/requests/madmin/estimates_spec.rb b/spec/requests/madmin/estimates_spec.rb new file mode 100644 index 00000000..d1bb3fc8 --- /dev/null +++ b/spec/requests/madmin/estimates_spec.rb @@ -0,0 +1,44 @@ +require "rails_helper" + +# Estimates are registered as: resources :estimates, except: [:update, :edit, :create] +RSpec.describe "Madmin estimates", type: :request do + let(:admin) { FactoryBot.create(:user, :admin) } + let!(:estimate) { FactoryBot.create(:estimate) } + + before { login_as(admin, scope: :user) } + after { Warden.test_reset! } + + describe "GET /madmin/estimates" do + it "lists estimates" do + get "/madmin/estimates" + + expect(response).to have_http_status(:ok) + end + end + + describe "GET /madmin/estimates/new" do + it "renders the new form" do + get "/madmin/estimates/new" + + expect(response).to have_http_status(:ok) + end + end + + describe "GET /madmin/estimates/:id" do + it "shows the estimate" do + get "/madmin/estimates/#{estimate.id}" + + expect(response).to have_http_status(:ok) + end + end + + describe "DELETE /madmin/estimates/:id" do + it "destroys the estimate" do + expect { + delete "/madmin/estimates/#{estimate.id}" + }.to change(Estimate, :count).by(-1) + + expect(response).to have_http_status(:redirect) + end + end +end diff --git a/spec/requests/madmin/projects_spec.rb b/spec/requests/madmin/projects_spec.rb new file mode 100644 index 00000000..3b1b326d --- /dev/null +++ b/spec/requests/madmin/projects_spec.rb @@ -0,0 +1,45 @@ +require "rails_helper" + +# Projects are registered as: resources :projects, except: [:update, :edit, :create] +RSpec.describe "Madmin projects", type: :request do + let(:admin) { FactoryBot.create(:user, :admin) } + let!(:project) { FactoryBot.create(:project) } + + before { login_as(admin, scope: :user) } + after { Warden.test_reset! } + + describe "GET /madmin/projects" do + it "lists projects" do + get "/madmin/projects" + + expect(response).to have_http_status(:ok) + expect(response.body).to include(project.title.truncate(20)) + end + end + + describe "GET /madmin/projects/new" do + it "renders the new form" do + get "/madmin/projects/new" + + expect(response).to have_http_status(:ok) + end + end + + describe "GET /madmin/projects/:id" do + it "shows the project" do + get "/madmin/projects/#{project.id}" + + expect(response).to have_http_status(:ok) + end + end + + describe "DELETE /madmin/projects/:id" do + it "destroys the project" do + expect { + delete "/madmin/projects/#{project.id}" + }.to change(Project, :count).by(-1) + + expect(response).to have_http_status(:redirect) + end + end +end diff --git a/spec/requests/madmin/stories_spec.rb b/spec/requests/madmin/stories_spec.rb new file mode 100644 index 00000000..67a49875 --- /dev/null +++ b/spec/requests/madmin/stories_spec.rb @@ -0,0 +1,83 @@ +require "rails_helper" + +# Stories are registered with the full set of resource routes, so this exercises +# create/update in addition to the read-only actions. +RSpec.describe "Madmin stories", type: :request do + let(:admin) { FactoryBot.create(:user, :admin) } + let(:project) { FactoryBot.create(:project) } + let!(:story) { FactoryBot.create(:story, project: project) } + + before { login_as(admin, scope: :user) } + after { Warden.test_reset! } + + describe "GET /madmin/stories" do + it "lists stories" do + get "/madmin/stories" + + expect(response).to have_http_status(:ok) + end + end + + describe "GET /madmin/stories/new" do + it "renders the new form" do + get "/madmin/stories/new" + + expect(response).to have_http_status(:ok) + end + end + + describe "GET /madmin/stories/:id" do + it "shows the story" do + get "/madmin/stories/#{story.id}" + + expect(response).to have_http_status(:ok) + end + end + + describe "GET /madmin/stories/:id/edit" do + it "renders the edit form" do + get "/madmin/stories/#{story.id}/edit" + + expect(response).to have_http_status(:ok) + end + end + + describe "POST /madmin/stories" do + it "creates a story" do + expect { + post "/madmin/stories", params: { + story: { + title: "A brand new story", + description: "Some description", + real_score: 3, + project_id: project.id + } + } + }.to change(Story, :count).by(1) + + expect(response).to have_http_status(:redirect) + expect(Story.last.title).to eq("A brand new story") + end + end + + describe "PATCH /madmin/stories/:id" do + it "updates the story" do + patch "/madmin/stories/#{story.id}", params: { + story: {title: "Updated title"} + } + + expect(response).to have_http_status(:redirect) + expect(story.reload.title).to eq("Updated title") + end + end + + describe "DELETE /madmin/stories/:id" do + it "destroys the story" do + expect { + delete "/madmin/stories/#{story.id}" + }.to change(Story, :count).by(-1) + + expect(response).to have_http_status(:redirect) + end + end +end diff --git a/spec/requests/madmin/users_spec.rb b/spec/requests/madmin/users_spec.rb new file mode 100644 index 00000000..a59d6ee1 --- /dev/null +++ b/spec/requests/madmin/users_spec.rb @@ -0,0 +1,44 @@ +require "rails_helper" + +# Users are registered as: resources :users, except: [:update, :edit, :create] +RSpec.describe "Madmin users", type: :request do + let(:admin) { FactoryBot.create(:user, :admin) } + let!(:user) { FactoryBot.create(:user) } + + before { login_as(admin, scope: :user) } + after { Warden.test_reset! } + + describe "GET /madmin/users" do + it "lists users" do + get "/madmin/users" + + expect(response).to have_http_status(:ok) + end + end + + describe "GET /madmin/users/new" do + it "renders the new form" do + get "/madmin/users/new" + + expect(response).to have_http_status(:ok) + end + end + + describe "GET /madmin/users/:id" do + it "shows the user" do + get "/madmin/users/#{user.id}" + + expect(response).to have_http_status(:ok) + end + end + + describe "DELETE /madmin/users/:id" do + it "destroys the user" do + expect { + delete "/madmin/users/#{user.id}" + }.to change(User, :count).by(-1) + + expect(response).to have_http_status(:redirect) + end + end +end diff --git a/spec/requests/madmin/version_jumps_spec.rb b/spec/requests/madmin/version_jumps_spec.rb new file mode 100644 index 00000000..b12c967d --- /dev/null +++ b/spec/requests/madmin/version_jumps_spec.rb @@ -0,0 +1,81 @@ +require "rails_helper" + +# Version jumps are registered with the full set of resource routes, so this +# exercises create/update in addition to the read-only actions. +RSpec.describe "Madmin version jumps", type: :request do + let(:admin) { FactoryBot.create(:user, :admin) } + let!(:version_jump) { FactoryBot.create(:version_jump) } + + before { login_as(admin, scope: :user) } + after { Warden.test_reset! } + + describe "GET /madmin/version_jumps" do + it "lists version jumps" do + get "/madmin/version_jumps" + + expect(response).to have_http_status(:ok) + end + end + + describe "GET /madmin/version_jumps/new" do + it "renders the new form" do + get "/madmin/version_jumps/new" + + expect(response).to have_http_status(:ok) + end + end + + describe "GET /madmin/version_jumps/:id" do + it "shows the version jump" do + get "/madmin/version_jumps/#{version_jump.id}" + + expect(response).to have_http_status(:ok) + end + end + + describe "GET /madmin/version_jumps/:id/edit" do + it "renders the edit form" do + get "/madmin/version_jumps/#{version_jump.id}/edit" + + expect(response).to have_http_status(:ok) + end + end + + describe "POST /madmin/version_jumps" do + it "creates a version jump" do + expect { + post "/madmin/version_jumps", params: { + version_jump: { + technology: "Rails-new", + initial_version: "6.1", + target_version: "7.0" + } + } + }.to change(VersionJump, :count).by(1) + + expect(response).to have_http_status(:redirect) + expect(VersionJump.last.technology).to eq("Rails-new") + end + end + + describe "PATCH /madmin/version_jumps/:id" do + it "updates the version jump" do + patch "/madmin/version_jumps/#{version_jump.id}", params: { + version_jump: {target_version: "8.0"} + } + + expect(response).to have_http_status(:redirect) + expect(version_jump.reload.target_version).to eq("8.0") + end + end + + describe "DELETE /madmin/version_jumps/:id" do + it "destroys the version jump" do + expect { + delete "/madmin/version_jumps/#{version_jump.id}" + }.to change(VersionJump, :count).by(-1) + + expect(response).to have_http_status(:redirect) + end + end +end From c907c0b86162cef3dd61978ecdfa84638bf8d231 Mon Sep 17 00:00:00 2001 From: Ernesto Tagwerker Date: Fri, 17 Jul 2026 07:41:20 -0400 Subject: [PATCH 6/6] Fix madmin index pagination for madmin 2.x madmin 2.x dropped the madmin/pagy/_nav partial and renders pagination via pagy's own helpers. The app's overridden index.html.erb still rendered the old partial, raising ActionView::MissingTemplate for any resource list with more than one page (pagy's default limit is 20). Replace the stale partial render with pagy's helpers, mirroring the gem's own index template (series_nav/info_tag on pagy 43+, pagy_nav/pagy_info on older versions). Add a request spec that creates 21 records to exercise the multi-page path, which the single-record index specs never hit. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/views/madmin/application/index.html.erb | 13 +++++++++- spec/requests/madmin/pagination_spec.rb | 28 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 spec/requests/madmin/pagination_spec.rb diff --git a/app/views/madmin/application/index.html.erb b/app/views/madmin/application/index.html.erb index ea987e0d..8fbce9c0 100644 --- a/app/views/madmin/application/index.html.erb +++ b/app/views/madmin/application/index.html.erb @@ -63,4 +63,15 @@ -<%== render(partial: 'madmin/pagy/nav', locals: { pagy: @pagy }) if @pagy.pages > 1 %> +<%# madmin 2.x dropped the madmin/pagy/_nav partial in favor of pagy's own + helpers. series_nav/info_tag are the pagy 43+ API; pagy_nav/pagy_info the + older one. %> + diff --git a/spec/requests/madmin/pagination_spec.rb b/spec/requests/madmin/pagination_spec.rb new file mode 100644 index 00000000..1c171955 --- /dev/null +++ b/spec/requests/madmin/pagination_spec.rb @@ -0,0 +1,28 @@ +require "rails_helper" + +# Guards the shared madmin/application/index.html.erb pagination path. +# +# The index partial only renders the pagination nav when there is more than +# one page of results (pagy's default limit is 20). An older madmin override +# rendered a `madmin/pagy/_nav` partial that no longer ships with madmin 2.x, +# raising ActionView::MissingTemplate once a list grew past one page. The +# single-record index specs never hit this branch, so this spec creates enough +# records to paginate and exercise the nav rendering. +RSpec.describe "Madmin pagination", type: :request do + let(:admin) { FactoryBot.create(:user, :admin) } + + before { login_as(admin, scope: :user) } + after { Warden.test_reset! } + + describe "GET /madmin/version_jumps with more than one page of records" do + before { FactoryBot.create_list(:version_jump, 21) } + + it "renders the index with pagination navigation" do + get "/madmin/version_jumps" + + expect(response).to have_http_status(:ok) + # pagy builds links to subsequent pages using the `page` query param + expect(response.body).to include("page=2") + end + end +end