I'm having an issue with ActiveAdmin 3.3.0 in my Rails 8.0.2 application where DELETE actions are not working. When an admin user clicks the "Delete" link, the application performs a GET request to view the resource instead of a DELETE request to remove it. This happens across all ActiveAdmin resources.
- Create (POST), Read (GET), and Update (PATCH) actions all work properly
- When clicking the Delete link, the browser sends a GET request instead of DELETE
- The issue appears in all admin resources, not just one specific model
Environment
- Ruby on Rails 8.0.2
- ActiveAdmin 3.3.0
I'm a total noob so I have some gems but tbh I'm not sure which are even being used anymore pertaining to this issue:
- Propshaft (for asset pipeline)
- ImportMap (for JavaScript module handling)
- Turbo Rails (for page transitions)
What I've Tried
In my ActiveAdmin resource, I've attempted to explicitly set turbo_method: :delete for the Delete action:
# app/admin/service_templates.rb
ActiveAdmin.register ServiceTemplate do
# ...
index do
# ...
actions defaults: false do |template|
item "View", resource_path(template)
item "Edit", edit_resource_path(template)
item "Delete", resource_path(template), turbo_method: :delete, data: { confirm: "Are you sure?" }
end
end
# ...
end
...
def destroy
resource = ServiceTemplate.find(params[:id])
resource.destroy
flash[:notice] = "Service template was successfully destroyed."
redirect_to collection_path
end
However, this doesn't work - clicking the Delete link still sends a GET request.
Regular Devise sign-out links with data: { turbo_method: :delete } work correctly in my application although that might be overdid by Active_admin as well:
<%= link_to "Sign out", destroy_user_session_path, data: { turbo_method: :delete } %>
# Default:
config.logout_link_path = :destroy_admin_user_session_path
The list goes on...
- Added Rails UJS to the application
- Added Rails UJS via Yarn (yarn add @rails/ujs)
- Updated active_admin.js to include Rails UJS and initialize it
- Modified JavaScript imports and configuration
- Tried adding Rails UJS to importmap
- Attempted to add JavaScript import to ActiveAdmin head via config.head
- Changed how DELETE links are implemented
- Tried using form_tag with method: instead of links
- Added unique classes and IDs to forms to avoid batch action conflicts
- Used explicit absolute paths instead of resource path helpers
- Added data attributes to prevent batch action handling
- Attempted custom controller actions
- Tried creating a custom GET-based delete action as an alternative to DELETE requests
- Attempted to implement a custom controller hook to handle deletion
- Explored global modifications
- Considered monkey-patching ActiveAdmin classes to change how delete actions work
- Looked at modifying the ResourceController to handle alternative deletion approaches
The core issue remains that in Rails 8 with ActiveAdmin 3.3.0, the JavaScript handling for DELETE operations isn't working correctly, causing delete buttons to either not function or be incorrectly routed to batch actions.
Relevant Configuration Files
[config/initializers/active_admin.rb]
# frozen_string_literal: true
ActiveAdmin.setup do |config|
# == Site Title
#
# Set the title that is displayed on the main layout
# for each of the active admin pages.
#
config.site_title = "BizBlasts Admin"
# Set the link url for the title. For example, to take
# users to your main site. Defaults to no link.
#
config.site_title_link = "/"
# Set an optional image to be displayed for the header
# instead of a string (overrides :site_title)
#
# Note: Aim for an image that's 21px high so it fits in the header.
#
# config.site_title_image = "logo.png"
# == Load Paths
#
# By default Active Admin files go inside app/admin/.
# You can change this directory.
#
# eg:
# config.load_paths = [File.join(Rails.root, 'app', 'ui')]
#
# Or, you can also load more directories.
# Useful when setting namespaces with users that are not your main AdminUser entity.
#
# eg:
# config.load_paths = [
# File.join(Rails.root, 'app', 'admin'),
# File.join(Rails.root, 'app', 'cashier')
# ]
# == Default Namespace
#
# Set the default namespace each administration resource
# will be added to.
#
# eg:
# config.default_namespace = :hello_world
#
# This will create resources in the HelloWorld module and
# will namespace routes to /hello_world/*
#
# To set no namespace by default, use:
# config.default_namespace = false
#
# Default:
# config.default_namespace = :admin
#
# You can customize the settings for each namespace by using
# a namespace block. For example, to change the site title
# for a specific namespace:
#
# config.namespace :admin do |admin|
# admin.site_title = "Custom Admin Title"
# end
#
# This will ONLY change the title for the admin section. Other
# namespaces will continue to use the main "site_title" configuration.
# == User Authentication
#
# Active Admin will automatically call an authentication
# method in a before filter of all controller actions to
# ensure that there is a currently logged in admin user.
#
# This setting changes the method which Active Admin calls
# within the application controller.
config.authentication_method = :authenticate_admin_user!
# == User Authorization
#
# Active Admin will automatically call an authorization
# method in a before filter of all controller actions to
# ensure that there is a user with proper rights. You can use
# CanCanAdapter or make your own. Please refer to documentation.
config.authorization_adapter = ActiveAdmin::PunditAdapter
config.pundit_policy_namespace = :admin
# In case you prefer Pundit over other solutions you can here pass
# the name of default policy class. This policy will be used in every
# case when Pundit is unable to find suitable policy.
config.pundit_default_policy = "Admin::DefaultPolicy"
# You can customize your CanCan Ability class name here.
# config.cancan_ability_class = "Ability"
# You can specify a method to be called on unauthorized access.
# This is necessary in order to prevent a redirect loop which happens
# because, by default, user gets redirected to Dashboard. If user
# doesn't have access to Dashboard, he'll end up in a redirect loop.
# Method provided here should be defined in application_controller.rb.
# config.on_unauthorized_access = :access_denied
# == Current User
#
# Active Admin will associate actions with the current
# user performing them.
#
# This setting changes the method which Active Admin calls
# (within the application controller) to return the currently logged in user.
config.current_user_method = :current_admin_user
# == Logging Out
#
# Active Admin displays a logout link on each screen. These
# settings configure the location and method used for the link.
#
# This setting changes the path where the link points to. If it's
# a string, the strings is used as the path. If it's a Symbol, we
# will call the method to return the path.
#
# Default:
config.logout_link_path = :destroy_admin_user_session_path
# This setting changes the http method used when rendering the
# link. For example :get, :delete, :put, etc..
#
# Default:
# config.logout_link_method = :get
# == Root
#
# Set the action to call for the root path. You can set different
# roots for each namespace.
#
# Default:
# config.root_to = 'dashboard#index'
# == Admin Comments
#
# This allows your users to comment on any resource registered with Active Admin.
#
# You can completely disable comments:
config.comments = false
#
# You can change the name under which comments are registered:
# config.comments_registration_name = 'AdminComment'
#
# You can change the order for the comments and you can change the column
# to be used for ordering:
# config.comments_order = 'created_at ASC'
#
# You can disable the menu item for the comments index page:
# config.comments_menu = false
#
# You can customize the comment menu:
# config.comments_menu = { parent: 'Admin', priority: 1 }
# == Batch Actions
#
# Enable and disable Batch Actions
#
config.batch_actions = true
# == Controller Filters
#
# You can add before, after and around filters to all of your
# Active Admin resources and pages from here.
#
# Tenant handling for multi-tenancy - unscope tenancy for admin users
config.before_action do
ActsAsTenant.current_tenant = nil if request.path.include?('/admin')
end
# == Attribute Filters
#
# You can exclude possibly sensitive model attributes from being displayed,
# added to forms, or exported by default by ActiveAdmin
#
config.filter_attributes = [:encrypted_password, :password, :password_confirmation]
# == Localize Date/Time Format
#
# Set the localize format to display dates and times.
# To understand how to localize your app with I18n, read more at
# https://guides.rubyonrails.org/i18n.html
#
# You can run `bin/rails runner 'puts I18n.t("date.formats")'` to see the
# available formats in your application.
#
config.localize_format = :long
# == Setting a Favicon
#
# config.favicon = 'favicon.ico'
# == Meta Tags
#
# Add additional meta tags to the head element of active admin pages.
#
# Add tags to all pages logged in users see:
# config.meta_tags = { author: 'My Business' }
# By default, sign up/sign in/recover password pages are excluded
# from showing up in search engine results by adding a robots meta
# tag. You can reset the hash of meta tags included in logged out
# pages:
# config.meta_tags_for_logged_out_pages = {}
# == Removing Breadcrumbs
#
# Breadcrumbs are enabled by default. You can customize them for individual
# resources or you can disable them globally from here.
#
# config.breadcrumb = false
# == Create Another Checkbox
#
# Create another checkbox is disabled by default. You can customize it for individual
# resources or you can enable them globally from here.
#
# config.create_another = true
# == Register Stylesheets & Javascripts
#
# We recommend using the built in Active Admin layout and loading
# up your own stylesheets / javascripts to customize the look
# and feel.
#
# To load a stylesheet:
# config.register_stylesheet 'my_stylesheet.css'
#
# You can provide an options hash for more control, which is passed along to stylesheet_link_tag():
# config.register_stylesheet 'my_print_stylesheet.css', media: :print
#
# To load a javascript file:
# config.register_javascript 'my_javascript.js'
# == CSV options
#
# Set the CSV builder separator
# config.csv_options = { col_sep: ';' }
#
# Force the use of quotes
# config.csv_options = { force_quotes: true }
# == Menu System
#
# You can add a navigation menu to be used in your application, or configure a provided menu
#
# To change the default utility navigation to show a link to your website & a logout btn
#
# config.namespace :admin do |admin|
# admin.build_menu :utility_navigation do |menu|
# menu.add label: "My Great Website", url: "http://www.mygreatwebsite.com", html_options: { target: :blank }
# admin.add_logout_button_to_menu menu
# end
# end
#
# If you wanted to add a static menu item to the default menu provided:
#
config.namespace :admin do |admin|
admin.build_menu :default do |menu|
menu.add label: "Dashboard", url: "/admin/dashboard", priority: 1
menu.add label: "Client Websites", url: "/admin/client_websites", priority: 2
end
end
# == Download Links
#
# You can disable download links on resource listing pages,
# or customize the formats shown per namespace/globally
#
# To disable/customize for the :admin namespace:
#
# config.namespace :admin do |admin|
#
# # Disable the links entirely
# admin.download_links = false
#
# # Only show XML & PDF options
# admin.download_links = [:xml, :pdf]
#
# # Enable/disable the links based on block
# #
# # admin.download_links = proc { |admin| user.admin? }
#
# end
# == Pagination
#
# Pagination is enabled by default for all resources.
# You can control the default per page count for all resources here.
#
# config.default_per_page = 30
#
# You can control the max per page count too.
#
# config.max_per_page = 10_000
# == Filters
#
# By default the index screen includes a "Filters" sidebar on the right
# hand side with a filter for each attribute of the registered model.
# You can enable or disable them for all resources here.
#
# config.filters = true
#
# By default the filters include associations in a select, which means
# that every record will be loaded for each association (up
# to the value of config.maximum_association_filter_arity).
# You can enabled or disable the inclusion
# of those filters by default here.
#
# config.include_default_association_filters = true
# config.maximum_association_filter_arity = 256 # default value of :unlimited will change to 256 in a future version
# config.filter_columns_for_large_association = [
# :display_name,
# :full_name,
# :name,
# :username,
# :login,
# :title,
# :email,
# ]
# config.filter_method_for_large_association = '_starts_with'
# == Head
#
# You can add your own content to the site head like analytics. Make sure
# you only pass content you trust.
#
# config.head = ''.html_safe
# == Footer
#
# By default, the footer shows the current Active Admin version. You can
# override the content of the footer here.
#
config.footer = 'BizBlasts Admin'
# == Sorting
#
# By default ActiveAdmin::OrderClause is used for sorting logic
# You can inherit it with own class and inject it for all resources
#
# config.order_clause = MyOrderClause
# == Webpacker
#
# By default, Active Admin uses Propshaft for bootstrapping
# and other assets. If you would like to use Webpacker instead
# (i.e. you're using React, Vue, or other JS framework for
# your JS), you can set this flag to true.
#
# config.use_webpacker = true
end
[app/views/layouts/application.html.erb]
<!DOCTYPE html>
<html>
<head>
<title><%= content_for(:title) || "BizBlasts" %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= yield :head %>
<link rel="icon" href="/icon.png" type="image/png">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/icon.png">
<%# Use both Rails asset helper and direct link as fallback %>
<% begin %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<% rescue => e %>
<% Rails.logger.error "Failed to load stylesheet with helper: #{e.message}" %>
<link rel="stylesheet" href="/assets/application.css" data-turbo-track="reload">
<% end %>
<% begin %>
<%= javascript_importmap_tags %>
<% rescue => e %>
<% Rails.logger.error "Failed to load JavaScript with helper: #{e.message}" %>
<script src="/assets/application.js" data-turbo-track="reload"></script>
<% end %>
</head>
<body>
<%# Render flash messages %>
<% flash.each do |type, msg| %>
<div class="flash-message flash-<%= type %>">
<%= msg %>
</div>
<% end %>
<%= yield %>
<% if user_signed_in? %>
<%# Sign out link for regular Users (manager, staff, client) %>
<%= link_to "Sign out", destroy_user_session_path, data: { "turbo-method": :delete } %>
<% end %>
</body>
</html>
[app/assets/javascripts/active_admin.js]
//= require active_admin/base
[config/initializers/propshaft_config.rb]
# frozen_string_literal: true
# This initializer configures Propshaft to properly handle ActiveAdmin assets
# and ensures assets are served correctly in production
# Make sure paths are set up before the app loads
if defined?(Propshaft)
# Register ActiveAdmin asset paths with Propshaft immediately, rather than waiting for after_initialize
Rails.application.config.assets.paths ||= []
puts "Setting up Propshaft asset paths for ActiveAdmin..."
# Add app/assets/builds directory to load path
Rails.application.config.assets.paths << Rails.root.join('app', 'assets', 'builds').to_s
# Add public/assets directory to load path
Rails.application.config.assets.paths << Rails.root.join('public', 'assets').to_s
# Add the ActiveAdmin gem's asset paths
if defined?(ActiveAdmin) || Gem.loaded_specs.key?('activeadmin')
begin
activeadmin_path = Gem.loaded_specs['activeadmin'].full_gem_path
Rails.application.config.assets.paths << File.join(activeadmin_path, 'app', 'assets', 'stylesheets')
Rails.application.config.assets.paths << File.join(activeadmin_path, 'app', 'assets', 'javascripts')
puts "Added ActiveAdmin asset paths: #{activeadmin_path}"
rescue => e
puts "Failed to add ActiveAdmin asset paths: #{e.message}"
end
end
# Disable fingerprinting in production - we'll handle asset versioning manually
if Rails.env.production?
begin
if Rails.application.config.respond_to?(:assets)
Rails.application.config.assets.digest = false
puts "Disabled asset fingerprinting in production to avoid issues with ActiveAdmin"
end
rescue => e
puts "Failed to disable asset fingerprinting: #{e.message}"
end
end
# Register specific paths for ActiveAdmin CSS files
# This ensures they're findable regardless of precompilation
Rails.application.config.after_initialize do
# Print asset paths to logs for debugging
Rails.logger.info "Propshaft asset paths: #{Rails.application.config.assets.paths.inspect}"
# Special handling for ActiveAdmin assets in production
if Rails.env.production?
# Make a direct mapping for active_admin.css to active_admin.css in public/assets
# to bypass Propshaft lookups which might be failing
if Rails.application.respond_to?(:assets) && Rails.application.assets.respond_to?(:define_singleton_method)
begin
original_compute_path = Rails.application.assets.method(:compute_path)
Rails.application.assets.define_singleton_method(:compute_path) do |path, **options|
# Special case for active_admin.css
if path == "active_admin.css"
Rails.root.join('public', 'assets', 'active_admin.css').to_s
elsif path == "application.css"
Rails.root.join('public', 'assets', 'application.css').to_s
elsif path == "application.js"
Rails.root.join('public', 'assets', 'application.js').to_s
else
original_compute_path.call(path, **options)
end
end
Rails.logger.info "Added special handling for asset path computation"
rescue => e
Rails.logger.error "Failed to add special handling for assets: #{e.message}"
end
end
end
end
end
[config/importmap.rb]
# Pin npm packages by running ./bin/importmap
pin "application"
[Gemfile]
# frozen_string_literal: true
source "https://rubygems.org"
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 8.0.2"
# The modern asset pipeline for Rails [https://github.com/rails/propshaft]
gem "propshaft"
# Use postgresql as the database for Active Record
gem "pg", "~> 1.1"
# Use the Puma web server [https://github.com/puma/puma]
gem "puma", ">= 5.0"
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "importmap-rails"
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[windows jruby]
# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable
gem "solid_cache"
gem "solid_queue"
# Authentication
gem "devise"
gem "devise-jwt"
# Authorization
gem "pundit"
# Admin interface
gem "activeadmin"
# gem "sassc-rails" # REMOVED - Conflicts with Propshaft/Importmap
# Multitenancy
gem "acts_as_tenant"
# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false
# Deploy this application anywhere as a Docker container [https://kamal-deploy.org]
# gem "kamal", require: false # REMOVED
# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/]
gem "thruster", require: false
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"
# Use node based css bundling
gem "cssbundling-rails"
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri mingw x64_mingw ]
gem "dotenv-rails"
gem "factory_bot_rails"
gem "faker"
gem "rspec-rails"
gem "capybara"
gem "selenium-webdriver"
gem "cuprite"
gem "webdrivers"
# Static analysis for security vulnerabilities [https://brakemanscanner.org/]
gem "brakeman", require: false
# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
gem "rubocop-rails-omakase", require: false
# Testing framework
gem "shoulda-matchers", "~> 6.0"
gem "database_cleaner" # Main database_cleaner gem
gem "database_cleaner-active_record" # For cleaning the database between tests
# For test performance metrics
gem "simplecov", require: false
# For checking rendered templates in controller/request specs
gem "rails-controller-testing"
end
group :development do
# Use console on exceptions pages [https://github.com/rails/web-console]
gem "web-console"
# Annotate models
gem "annotate"
end
# For time-of-day calculations in availability logic
gem 'tod'
gem 'parallel_tests', group: [:development, :test]
Tests
I've created system tests that verify this issue:
# spec/system/admin/templates_management_spec.rb
context "Deleting a template" do
let!(:template) { create(:service_template, name: "Template 1") }
it "allows admin to delete a template" do
visit admin_service_templates_path
# Find the delete link in the actions column and click it
within("tr", text: template.name) do
# Use standard Capybara block for confirmation
accept_confirm do
click_link "Delete"
end
end
expect(page).to have_content("Service template was successfully destroyed.")
expect(page).not_to have_selector("tr", text: template.name)
end
end
This test fails because the DELETE request never happens.
How can I fix ActiveAdmin's DELETE functionality with Rails 8.0.2, and my mix of other components? Is there a compatibility issue between ActiveAdmin 3.3.0 and Rails 8.0.2 with Turbo? Do I need to configure something specific for UJS/Turbo to work with ActiveAdmin's link generation? I've tried to use buttons and they do not solve the problem either because I have batch actions. users/544825/max has mentioned this a common problem. I feel geniuely stuck after spending like a week on this one. And I don't think I can restart or create a small version of this due to my project already being half a million lines of code. Please let me know if you have any questions!
data: {turbo_method: :delete, turbo_confirm: "Are you sure?"}.