Skip to content

feature: add bulk update #3695

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
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
9 changes: 8 additions & 1 deletion app/components/avo/views/resource_edit_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
**@resource.stimulus_data_attributes
} do %>
<%= render_cards_component %>
<%= form_with model: @resource.record,
<%= form_with model: model,
scope: @resource.form_scope,
url: form_url,
method: form_method,
Expand All @@ -23,6 +23,13 @@
},
multipart: true do |form| %>
<%= render Avo::ReferrerParamsComponent.new back_path: back_path %>

<% if @prefilled_fields.present? %>
<% @prefilled_fields.each do |field, value| %>
<%= hidden_field_tag "prefilled[#{field}]", value %>
<% end %>
<% end %>

<%= content_tag :div, class: "space-y-12" do %>
<% @resource.get_items.each_with_index do |item, index| %>
<%= render Avo::Items::SwitcherComponent.new(
Expand Down
44 changes: 31 additions & 13 deletions app/components/avo/views/resource_edit_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ class Avo::Views::ResourceEditComponent < Avo::ResourceComponent
prop :view, default: Avo::ViewInquirer.new(:edit).freeze
prop :display_breadcrumbs, default: true, reader: :public

attr_reader :query

def initialize(resource:, query: nil, prefilled_fields: nil, **args)
@query = query
@prefilled_fields = prefilled_fields
super(resource: resource, **args)
end

def after_initialize
@display_breadcrumbs = @reflection.blank? && display_breadcrumbs
end
Expand All @@ -18,18 +26,22 @@ def title
end

def back_path
# The `return_to` param takes precedence over anything else.
return params[:return_to] if params[:return_to].present?

return if via_belongs_to?
return resource_view_path if via_resource?
return resources_path if via_index?

if is_edit? && Avo.configuration.resource_default_view.show? # via resource show or edit page
return helpers.resource_path(record: @resource.record, resource: @resource, **keep_referrer_params)
if params[:controller] == "avo/bulk_update"
helpers.resources_path(resource: @resource)
elsif params[:return_to].present?
# The `return_to` param takes precedence over anything else.
params[:return_to]
elsif via_belongs_to?
nil
elsif via_resource?
resource_view_path
elsif via_index?
resources_path
elsif is_edit? && Avo.configuration.resource_default_view.show? # via resource show or edit page
helpers.resource_path(record: @resource.record, resource: @resource, **keep_referrer_params)
else
resources_path
end

resources_path
end

def resources_path
Expand Down Expand Up @@ -76,13 +88,19 @@ def is_edit?
end

def form_method
return :put if is_edit?
return :put if is_edit? && params[:controller] != "avo/bulk_update"

:post
end

def model
@resource.record
end

def form_url
if is_edit?
if params[:controller] == "avo/bulk_update"
helpers.handle_bulk_update_path(resource_name: @resource.name, query: @query)
elsif is_edit?
helpers.resource_path(
record: @resource.record,
resource: @resource
Expand Down
2 changes: 2 additions & 0 deletions app/components/avo/views/resource_index_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
<%= render Avo::FiltersComponent.new filters: @filters, resource: @resource, applied_filters: @applied_filters, parent_record: @parent_record %>

<%= render partial: "avo/partials/view_toggle_button", locals: { available_view_types: available_view_types, view_type: view_type, turbo_frame: @turbo_frame } %>

<%= render_bulk_update_button %>
</div>
</div>
<% if has_dynamic_filters? %>
Expand Down
25 changes: 25 additions & 0 deletions app/components/avo/views/resource_index_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
class Avo::Views::ResourceIndexComponent < Avo::ResourceComponent
include Avo::ResourcesHelper
include Avo::ApplicationHelper
include Avo::Concerns::ChecksShowAuthorization

prop :resource
prop :resources
Expand Down Expand Up @@ -33,6 +34,20 @@ def view_type
@index_params[:view_type]
end

def bulk_edit_path
# Add the `view` param to let Avo know where to redirect back when the user clicks the `Cancel` button.
args = {via_view: "index"}

if @parent_record.present?
args = {
via_resource_class: parent_resource.class.to_s,
via_record_id: @parent_record.to_param
}
end

helpers.edit_bulk_update_path(resource: @resource, **args)
end

def available_view_types
@index_params[:available_view_types]
end
Expand Down Expand Up @@ -154,6 +169,16 @@ def render_dynamic_filters_button
end
end

def render_bulk_update_button
a_link helpers.edit_bulk_update_path(resource_name: @resource.name),
style: :primary,
color: :primary,
icon: "avo/edit",
form_class: "flex flex-col sm:flex-row sm:inline-flex" do
I18n.t("avo.bulk_update")
end
end

def scopes_list
Avo::Advanced::Scopes::ListComponent.new(
scopes: @scopes,
Expand Down
24 changes: 19 additions & 5 deletions app/controllers/avo/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class BaseController < ApplicationController
before_action :set_resource_name
before_action :set_resource
before_action :set_applied_filters, only: :index
before_action :set_record, only: [:show, :edit, :destroy, :update, :preview]
before_action :set_record, only: [:show, :edit, :destroy, :update, :preview], if: -> { controller_name != "bulk_update" }
before_action :set_record_to_fill, only: [:new, :edit, :create, :update]
before_action :detect_fields
before_action :set_edit_title_and_breadcrumbs, only: [:edit, :update]
Expand Down Expand Up @@ -416,8 +416,7 @@ def filters_to_be_applied
end

def set_edit_title_and_breadcrumbs
@resource = @resource.hydrate(record: @record, view: Avo::ViewInquirer.new(:edit), user: _current_user)
@page_title = @resource.default_panel_name.to_s
set_resource_and_page_title

last_crumb_args = {}
# If we're accessing this resource via another resource add the parent to the breadcrumbs.
Expand All @@ -438,8 +437,23 @@ def set_edit_title_and_breadcrumbs
add_breadcrumb @resource.plural_name.humanize, resources_path(resource: @resource)
end

add_breadcrumb @resource.record_title, resource_path(record: @resource.record, resource: @resource, **last_crumb_args)
add_breadcrumb t("avo.edit").humanize
help_add_breadcrumb(last_crumb_args)
end

def set_resource_and_page_title
if params[:controller] != "avo/bulk_update"
@resource = @resource.hydrate(record: @record, view: Avo::ViewInquirer.new(:edit), user: _current_user)
@page_title = @resource.default_panel_name.to_s
end
end

def help_add_breadcrumb(last_crumb_args)
if params[:controller] != "avo/bulk_update"
add_breadcrumb @resource.record_title, resource_path(record: @resource.record, resource: @resource, **last_crumb_args) if params[:controller] != "avo/bulk_update"
add_breadcrumb t("avo.edit").humanize
else
add_breadcrumb t("avo.bulk_edit")
end
end

def create_success_action
Expand Down
131 changes: 131 additions & 0 deletions app/controllers/avo/bulk_update_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
module Avo
class BulkUpdateController < ResourcesController
before_action :set_query, only: [:edit, :handle]
before_action :set_fields, only: [:edit, :handle]

def edit
@prefilled_fields = prefill_fields(@query, @fields)
@record = @resource.model_class.new(@prefilled_fields.transform_values { |v| v.nil? ? nil : v })

@resource.record = @record
render Avo::Views::ResourceEditComponent.new(
resource: @resource,
query: @query,
prefilled_fields: @prefilled_fields
)
end

def handle
saved = save_records

if saved
flash[:notice] = t("avo.bulk_update_success")
else
flash[:error] = t("avo.bulk_update_failure")
end

redirect_to after_bulk_update_path
end

private

def update_records
params = params_to_apply

@query.each do |record|
@resource.fill_record(record, params)
end
end

def save_records
update_records

all_saved = true

ActiveRecord::Base.transaction do
@query.each do |record|
@record = record
save_record
end
rescue ActiveRecord::RecordInvalid => e
all_saved = false
puts "Failed to save #{record.id}: #{e.message}"
raise ActiveRecord::Rollback
end

all_saved
end

def params_to_apply
prefilled_params = params[:prefilled] || {}
current_params = current_resource_params
progress_fields = progress_bar_fields

current_params.reject do |key, value|
key_sym = key.to_sym

prefilled_value = prefilled_params[key_sym]

progress_field_with_default?(progress_fields, key_sym, prefilled_value, value) || prefilled_value.to_s == value.to_s
end
end

def current_resource_params
resource_key = @resource_name.downcase.to_sym
params[resource_key] || {}
end

def progress_bar_fields
@resource.get_field_definitions
.select { |field| field.is_a?(Avo::Fields::ProgressBarField) }
.map(&:id)
.map(&:to_sym)
end

def progress_field_with_default?(progress_fields, key_sym, prefilled_value, value)
progress_fields.include?(key_sym) && prefilled_value.nil? && value.to_s == "50"
end

def prefill_fields(records, fields)
fields.each_key.with_object({}) do |field_name, prefilled|
values = records.map { |record| record.public_send(field_name) }
values.uniq!
prefilled[field_name] = values.first if values.size == 1
end
end

def set_query
@query = if params[:query].present?
@resource.find_record(params[:query], params: params)
else
find_records_by_resource_ids
end
end

def find_records_by_resource_ids
resource_ids = params[:fields]&.dig(:avo_resource_ids)&.split(",") || []
decrypted_query || (resource_ids.any? ? @resource.find_record(resource_ids, params: params) : [])
end

def set_fields
if @query.blank?
flash[:error] = I18n.t("avo.bulk_update_no_records")
redirect_to after_bulk_update_path
else
@fields = @query.first.attributes.keys.index_with { nil }
end
end

def decrypted_query
encrypted_query = params[:fields]&.dig(:avo_selected_query) || params[:query]

return if encrypted_query.blank?

Avo::Services::EncryptionService.decrypt(message: encrypted_query, purpose: :select_all, serializer: Marshal)
end

def after_bulk_update_path
resources_path(resource: @resource)
end
end
end
8 changes: 8 additions & 0 deletions app/helpers/avo/url_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ def edit_resource_path(resource:, record: nil, resource_id: nil, **args)
avo.send :"edit_resources_#{resource.singular_route_key}_path", record || resource_id, **args
end

def edit_bulk_update_path(resource_name:, id:, **args)
avo.send :edit_bulk_update_path, resource_name, id, **args
end

def handle_bulk_update_path(resource_name:, query:, **args)
avo.send :handle_bulk_update_path, resource_name, query, **args
end

def resource_attach_path(resource, record_id, related_name, related_id = nil)
helpers.avo.resources_associations_new_path(resource.singular_route_key, record_id, related_name)
end
Expand Down
Loading
Loading