diff --git a/app/components/avo/alert_component.rb b/app/components/avo/alert_component.rb
index 74e0ed21f0..064da16068 100644
--- a/app/components/avo/alert_component.rb
+++ b/app/components/avo/alert_component.rb
@@ -12,7 +12,7 @@ def initialize(type, message)
end
def icon
- return "heroicons/solid/x-circle" if is_error?
+ return "heroicons/solid/exclamation-circle" if is_error?
return "heroicons/solid/exclamation" if is_warning?
return "heroicons/solid/exclamation-circle" if is_info?
return "heroicons/solid/check-circle" if is_success?
diff --git a/app/components/avo/backtrace_alert_component.html.erb b/app/components/avo/backtrace_alert_component.html.erb
new file mode 100644
index 0000000000..debaf26be7
--- /dev/null
+++ b/app/components/avo/backtrace_alert_component.html.erb
@@ -0,0 +1,26 @@
+
+
+
+
+ <%= helpers.svg "heroicons/solid/exclamation-circle", class: "h-6" %>
+
+
+
+ Backtrace:
+
<%= @backtrace.join("\n") %>
+
+
+
+
+
+
diff --git a/app/components/avo/backtrace_alert_component.rb b/app/components/avo/backtrace_alert_component.rb
new file mode 100644
index 0000000000..f190e98b49
--- /dev/null
+++ b/app/components/avo/backtrace_alert_component.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class Avo::BacktraceAlertComponent < ViewComponent::Base
+ include Avo::ApplicationHelper
+
+ def initialize(backtrace: nil)
+ @backtrace = backtrace
+ end
+
+ def render?
+ @backtrace.present?
+ end
+end
diff --git a/app/controllers/avo/base_controller.rb b/app/controllers/avo/base_controller.rb
index e675fe8bc6..8b7da8fea2 100644
--- a/app/controllers/avo/base_controller.rb
+++ b/app/controllers/avo/base_controller.rb
@@ -219,32 +219,18 @@ def destroy_record_action
def perform_action_and_record_errors(&block)
begin
succeeded = block.call
+ rescue ActiveRecord::RecordInvalid => e
+ # Do nothing as the record errors are already being displayed
rescue => exception
# In case there's an error somewhere else than the record
# Example: When you save a license that should create a user for it and creating that user throws and error.
# Example: When you Try to delete a record and has a foreign key constraint.
- exception_message = exception.message
+ @record.errors.add(:base, exception.message)
+ @backtrace = exception.backtrace
end
- # Add the errors from the record
- @errors = @record.errors.full_messages
-
- # Remove duplicated errors
- if exception_message.present?
- @errors = @errors.reject { |error| exception_message.include? error }
- end
-
- # Figure out if we have to output the exception_message
- # Usually it means that it's not a validation error but something else
- if exception_message.present?
- exception_is_validation = @errors.select { |error| exception_message.include? error }.present?
- end
-
- if exception_is_validation || (@errors.blank? && exception_message.present?)
- @errors << exception_message
- end
-
- @errors.any? ? false : succeeded
+ # This method only needs to return true or false to indicate if the action was successful
+ @record.errors.any? ? false : succeeded
end
def model_params
@@ -533,7 +519,8 @@ def destroy_success_message
end
def destroy_fail_message
- @errors.present? ? @errors.join(". ") : t("avo.failed")
+ errors = @record.errors.full_messages
+ errors.present? ? errors.join(". ") : t("avo.failed")
end
def after_destroy_path
diff --git a/app/views/avo/base/create_fail_action.turbo_stream.erb b/app/views/avo/base/create_fail_action.turbo_stream.erb
index 735b172e29..bdc0dbb3a9 100644
--- a/app/views/avo/base/create_fail_action.turbo_stream.erb
+++ b/app/views/avo/base/create_fail_action.turbo_stream.erb
@@ -1,21 +1,5 @@
<%= turbo_stream.replace(frame_id(@resource), template: "avo/base/new") %>
<%= turbo_stream.append "alerts" do %>
- <%= render Avo::FlashAlertsComponent.new flashes: flash %>
-<% end %>
-
-<% if @errors.any? %>
- <%= turbo_stream.append("alerts") do %>
- <% @errors.each do |message| %>
- <%= render Avo::AlertComponent.new :error, message %>
- <% end %>
- <% end %>
-<% end %>
-
-<% if @record.errors.any? %>
- <%= turbo_stream.append("alerts") do %>
- <% @record.errors.full_messages.each do |message| %>
- <%= render Avo::AlertComponent.new :error, message %>
- <% end %>
- <% end %>
+ <%= render partial: "avo/partials/all_alerts" %>
<% end %>
diff --git a/app/views/avo/base/update_fail_action.turbo_stream.erb b/app/views/avo/base/update_fail_action.turbo_stream.erb
index 034047be81..40f6b6bb1a 100644
--- a/app/views/avo/base/update_fail_action.turbo_stream.erb
+++ b/app/views/avo/base/update_fail_action.turbo_stream.erb
@@ -1,21 +1,5 @@
<%= turbo_stream.replace(frame_id(@resource), template: "avo/base/edit") %>
<%= turbo_stream.append "alerts" do %>
- <%= render Avo::FlashAlertsComponent.new flashes: flash %>
-<% end %>
-
-<% if @errors.any? %>
- <%= turbo_stream.append("alerts") do %>
- <% @errors.each do |message| %>
- <%= render Avo::AlertComponent.new :error, message %>
- <% end %>
- <% end %>
-<% end %>
-
-<% if @record.errors.any? %>
- <%= turbo_stream.append("alerts") do %>
- <% @record.errors.full_messages.each do |message| %>
- <%= render Avo::AlertComponent.new :error, message %>
- <% end %>
- <% end %>
+ <%= render partial: "avo/partials/all_alerts" %>
<% end %>
diff --git a/app/views/avo/partials/_alerts.html.erb b/app/views/avo/partials/_alerts.html.erb
index 8be38d1c28..d562c4e10d 100644
--- a/app/views/avo/partials/_alerts.html.erb
+++ b/app/views/avo/partials/_alerts.html.erb
@@ -1,9 +1,3 @@
<%= turbo_frame_tag :alerts, class: "fixed inset-0 bottom-0 flex flex-col space-y-4 items-end justify-right px-4 py-6 sm:p-6 justify-end z-[100] pointer-events-none" do %>
- <%= render Avo::FlashAlertsComponent.new flashes: flash %>
- <% # In case we have other general error messages %>
- <% if @errors.present? %>
- <% @errors.each do |message| %>
- <%= render Avo::AlertComponent.new :error, message %>
- <% end %>
- <% end %>
+ <%= render partial: "avo/partials/all_alerts" %>
<% end %>
diff --git a/app/views/avo/partials/_all_alerts.html.erb b/app/views/avo/partials/_all_alerts.html.erb
new file mode 100644
index 0000000000..fb3abe9ca6
--- /dev/null
+++ b/app/views/avo/partials/_all_alerts.html.erb
@@ -0,0 +1,11 @@
+<%= render Avo::FlashAlertsComponent.new flashes: flash %>
+
+<% if @record&.errors&.any? %>
+ <% @record.errors.full_messages.each do |message| %>
+ <%= render Avo::AlertComponent.new :error, message %>
+ <% end %>
+<% end %>
+
+<% if @backtrace&.present? && Avo::Current.user_is_developer? %>
+ <%= render Avo::BacktraceAlertComponent.new backtrace: @backtrace %>
+<% end %>
diff --git a/lib/avo/configuration.rb b/lib/avo/configuration.rb
index 7c81a1d7c6..416a67cfce 100644
--- a/lib/avo/configuration.rb
+++ b/lib/avo/configuration.rb
@@ -51,6 +51,8 @@ class Configuration
attr_accessor :default_url_options
attr_accessor :click_row_to_view_record
attr_accessor :alert_dismiss_time
+ attr_accessor :is_admin_method
+ attr_accessor :is_developer_method
attr_accessor :search_results_count
def initialize
@@ -110,6 +112,8 @@ def initialize
@pagination = {}
@click_row_to_view_record = false
@alert_dismiss_time = 5000
+ @is_admin_method = :is_admin?
+ @is_developer_method = :is_developer?
@search_results_count = 8
end
diff --git a/lib/avo/current.rb b/lib/avo/current.rb
index 4ecd87b56f..9baf2da9ac 100644
--- a/lib/avo/current.rb
+++ b/lib/avo/current.rb
@@ -24,4 +24,16 @@ def params
def request
view_context&.request || ActionDispatch::Request.empty
end
+
+ def user_is_admin?
+ return false unless user&.respond_to?(Avo.configuration.is_admin_method)
+
+ user.send(Avo.configuration.is_admin_method)
+ end
+
+ def user_is_developer?
+ return false unless user&.respond_to?(Avo.configuration.is_developer_method)
+
+ user.send(Avo.configuration.is_developer_method)
+ end
end
diff --git a/lib/generators/avo/templates/initializer/avo.tt b/lib/generators/avo/templates/initializer/avo.tt
index afb7f38840..6ede49e65b 100644
--- a/lib/generators/avo/templates/initializer/avo.tt
+++ b/lib/generators/avo/templates/initializer/avo.tt
@@ -23,6 +23,8 @@ Avo.configure do |config|
## == Authentication ==
# config.current_user_method = {}
+ # config.is_admin_method = :is_admin
+ # config.is_developer_method = :is_developer
# config.authenticate_with do
# end
diff --git a/spec/dummy/app/avo/resources/post.rb b/spec/dummy/app/avo/resources/post.rb
index c1cdc62ef4..b74a70e5ae 100644
--- a/spec/dummy/app/avo/resources/post.rb
+++ b/spec/dummy/app/avo/resources/post.rb
@@ -70,7 +70,7 @@ def fields
{
cover_url:
if record.cover_photo.attached?
- main_app.url_for(record.cover_photo.url)
+ main_app.url_for(record.cover_photo)
end,
title: record.name,
body: helpers.extract_excerpt(record.body)
diff --git a/spec/dummy/app/models/course.rb b/spec/dummy/app/models/course.rb
index fff5aed3e8..fb1a6bf687 100644
--- a/spec/dummy/app/models/course.rb
+++ b/spec/dummy/app/models/course.rb
@@ -20,6 +20,12 @@ class Course < ApplicationRecord
validates :name, presence: true
+ # Used to test the backtrace alert
+ after_save do
+ raise "raised" if ENV["TEST_BACKTRACE_ALERT"] == "1"
+ # raise "raised"
+ end
+
def has_skills
true
end
diff --git a/spec/dummy/app/models/user.rb b/spec/dummy/app/models/user.rb
index ac3b9a2ff2..ed46e707cf 100644
--- a/spec/dummy/app/models/user.rb
+++ b/spec/dummy/app/models/user.rb
@@ -84,4 +84,8 @@ def self.ransackable_attributes(auth_object = nil)
def accounts
[OpenStruct.new(id: 1, name: "Foo"), OpenStruct.new(id: 2, name: "Bar")]
end
+
+ def is_developer?
+ true
+ end
end
diff --git a/spec/dummy/config/initializers/avo.rb b/spec/dummy/config/initializers/avo.rb
index 15e214f8dc..b5145a45c6 100644
--- a/spec/dummy/config/initializers/avo.rb
+++ b/spec/dummy/config/initializers/avo.rb
@@ -17,6 +17,8 @@
## == App context ==
config.current_user_method = :current_user
+ # config.is_admin_method = :is_admin?
+ # config.is_developer_method = :is_developer?
config.model_resource_mapping = {
User: "User"
}
diff --git a/spec/system/avo/alert_backtrace_spec.rb b/spec/system/avo/alert_backtrace_spec.rb
new file mode 100644
index 0000000000..cb143112ff
--- /dev/null
+++ b/spec/system/avo/alert_backtrace_spec.rb
@@ -0,0 +1,19 @@
+require "rails_helper"
+
+RSpec.describe "Alert Backtrace", type: :system do
+ before do
+ ENV["TEST_BACKTRACE_ALERT"] = "1"
+ end
+
+ it "responds with a backtrace alert" do
+ visit "/admin/resources/courses/new"
+
+ fill_in "Name", with: "Test"
+
+ save
+
+ expect(page).to have_text "raised"
+ expect(page).to have_text "Backtrace:"
+ expect(page).to have_text "/dummy/app/models/course.rb:25:in `block in
"
+ end
+end