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 11 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
20 changes: 18 additions & 2 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,6 +26,8 @@ def title
end

def back_path
return helpers.resources_path(resource: @resource) if params[:controller] == "avo/bulk_update"

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

Expand Down Expand Up @@ -76,13 +86,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, id: 4),
style: :primary,
color: :primary,
icon: "avo/edit",
form_class: "flex flex-col sm:flex-row sm:inline-flex" do
"Bulk update"
end
end

def scopes_list
Avo::Advanced::Scopes::ListComponent.new(
scopes: @scopes,
Expand Down
152 changes: 152 additions & 0 deletions app/controllers/avo/bulk_update_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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
if params_to_apply.blank?
flash[:warning] = t("avo.no_changes_made")
redirect_to after_bulk_update_path
return
end

updated_count, failed_records = update_records

if failed_records.empty?
flash[:notice] = t("avo.bulk_update_success", count: updated_count)
else
error_messages = failed_records.flat_map { |fr| fr[:errors] }.uniq
flash[:error] = t("avo.bulk_update_failure", count: failed_records.count, errors: error_messages.join(", "))
end

redirect_to after_bulk_update_path
end

private

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) }

Check failure on line 60 in app/controllers/avo/bulk_update_controller.rb

View workflow job for this annotation

GitHub Actions / lint / runner / standardrb

[rubocop] reported by reviewdog 🐶 [Corrected] Layout/MultilineMethodCallIndentation: Use 2 (not 9) spaces for indenting an expression spanning multiple lines. Raw Output: app/controllers/avo/bulk_update_controller.rb:60:16: C: [Corrected] Layout/MultilineMethodCallIndentation: Use 2 (not 9) spaces for indenting an expression spanning multiple lines. .select { |field| field.is_a?(Avo::Fields::ProgressBarField) } ^^^^^^^
.map(&:id)

Check failure on line 61 in app/controllers/avo/bulk_update_controller.rb

View workflow job for this annotation

GitHub Actions / lint / runner / standardrb

[rubocop] reported by reviewdog 🐶 [Corrected] Layout/MultilineMethodCallIndentation: Use 2 (not 9) spaces for indenting an expression spanning multiple lines. Raw Output: app/controllers/avo/bulk_update_controller.rb:61:16: C: [Corrected] Layout/MultilineMethodCallIndentation: Use 2 (not 9) spaces for indenting an expression spanning multiple lines. .map(&:id) ^^^^
.map(&:to_sym)

Check failure on line 62 in app/controllers/avo/bulk_update_controller.rb

View workflow job for this annotation

GitHub Actions / lint / runner / standardrb

[rubocop] reported by reviewdog 🐶 [Corrected] Layout/MultilineMethodCallIndentation: Use 2 (not 9) spaces for indenting an expression spanning multiple lines. Raw Output: app/controllers/avo/bulk_update_controller.rb:62:16: C: [Corrected] Layout/MultilineMethodCallIndentation: Use 2 (not 9) spaces for indenting an expression spanning multiple lines. .map(&:to_sym) ^^^^
end

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

def update_records
updated_count = 0
failed_records = []

@query.each do |record|
update_record(record, params_to_apply)
if record.save
updated_count += 1
else
add_failed_record(failed_records, record)
end
rescue => e
add_failed_record(failed_records, record, e.message)
end

[updated_count, failed_records]
end

def update_record(record, params_to_apply)
params_to_apply.each do |key, value|
record.public_send(:"#{key}=", value)
rescue => e
log_field_assignment_error(key, e.message)
end

@resource.fill_record(record, params)
end

def log_field_assignment_error(key, error_message)
puts "Błąd przypisywania pola #{key}: #{error_message}"
end

def add_failed_record(failed_records, record, error_message = nil)
errors = error_message ? [error_message] : record.errors.full_messages
failed_records << { record: record, errors: errors }

Check failure on line 103 in app/controllers/avo/bulk_update_controller.rb

View workflow job for this annotation

GitHub Actions / lint / runner / standardrb

[rubocop] reported by reviewdog 🐶 [Corrected] Layout/SpaceInsideHashLiteralBraces: Space inside { detected. Raw Output: app/controllers/avo/bulk_update_controller.rb:103:26: C: [Corrected] Layout/SpaceInsideHashLiteralBraces: Space inside { detected. failed_records << { record: record, errors: errors } ^

Check failure on line 103 in app/controllers/avo/bulk_update_controller.rb

View workflow job for this annotation

GitHub Actions / lint / runner / standardrb

[rubocop] reported by reviewdog 🐶 [Corrected] Layout/SpaceInsideHashLiteralBraces: Space inside } detected. Raw Output: app/controllers/avo/bulk_update_controller.rb:103:57: C: [Corrected] Layout/SpaceInsideHashLiteralBraces: Space inside } detected. failed_records << { record: record, errors: errors } ^
end

def after_bulk_update_path
resources_path(resource: @resource)
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.size == 1 ? values.first : nil)

Check failure on line 114 in app/controllers/avo/bulk_update_controller.rb

View workflow job for this annotation

GitHub Actions / lint / runner / standardrb

[rubocop] reported by reviewdog 🐶 [Corrected] Style/TernaryParentheses: Use parentheses for ternary expressions with complex conditions. Raw Output: app/controllers/avo/bulk_update_controller.rb:114:34: C: [Corrected] Style/TernaryParentheses: Use parentheses for ternary expressions with complex conditions. prefilled[field_name] = (values.size == 1 ? values.first : nil) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
end
end

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

Check failure on line 120 in app/controllers/avo/bulk_update_controller.rb

View workflow job for this annotation

GitHub Actions / lint / runner / standardrb

[rubocop] reported by reviewdog 🐶 [Corrected] Layout/IndentationWidth: Use 2 (not 11) spaces for indentation. Raw Output: app/controllers/avo/bulk_update_controller.rb:120:7: C: [Corrected] Layout/IndentationWidth: Use 2 (not 11) spaces for indentation. @resource.find_record(params[:query], params: params) ^^^^^^^^^^^
else

Check failure on line 121 in app/controllers/avo/bulk_update_controller.rb

View workflow job for this annotation

GitHub Actions / lint / runner / standardrb

[rubocop] reported by reviewdog 🐶 [Corrected] Layout/ElseAlignment: Align else with @query. Raw Output: app/controllers/avo/bulk_update_controller.rb:121:16: C: [Corrected] Layout/ElseAlignment: Align else with @query. else ^^^^
find_records_by_resource_ids

Check failure on line 122 in app/controllers/avo/bulk_update_controller.rb

View workflow job for this annotation

GitHub Actions / lint / runner / standardrb

[rubocop] reported by reviewdog 🐶 [Corrected] Layout/IndentationWidth: Use 2 (not 11) spaces for indentation. Raw Output: app/controllers/avo/bulk_update_controller.rb:122:7: C: [Corrected] Layout/IndentationWidth: Use 2 (not 11) spaces for indentation. find_records_by_resource_ids ^^^^^^^^^^^
end
end

def find_records_by_resource_ids
resource_ids = action_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 action_params
@action_params ||= params.permit(:authenticity_token, fields: {})
end

def decrypted_query
encrypted_query = action_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
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
49 changes: 35 additions & 14 deletions app/javascript/js/controllers/item_select_all_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export default class extends Controller {
}

this.updateLinks('resourceIds')
this.updateBulkEditLink('resourceIds')
}

selectAll(event) {
Expand All @@ -82,40 +83,60 @@ export default class extends Controller {

if (this.selectedAllValue) {
this.updateLinks('selectedQuery')
this.updateBulkEditLink('selectedQuery')
} else {
this.updateLinks('resourceIds')
this.updateBulkEditLink('resourceIds')
}
}

updateLinks(param) {
let resourceIds = ''
let selectedQuery = ''
this.updateActionLinks(param, '[data-target="actions-list"] > a', {
resourceIdsKey: 'fields[avo_resource_ids]',
selectedQueryKey: 'fields[avo_index_query]',
selectedAllKey: 'fields[avo_selected_all]',
})
}

updateBulkEditLink(param) {
this.updateActionLinks(param, 'a[href*="/admin/bulk_update/edit"]', {
resourceIdsKey: 'fields[avo_resource_ids]',
selectedQueryKey: 'fields[avo_selected_query]',
})
}

if (param === 'resourceIds') {
resourceIds = JSON.parse(this.element.dataset.selectedResources).join(',')
} else if (param === 'selectedQuery') {
selectedQuery = this.element.dataset.itemSelectAllSelectedAllQueryValue
updateActionLinks(param, selector, keys) {
const params = {
resourceIds: {
value: JSON.parse(this.element.dataset.selectedResources).join(','),
selectedAll: 'false',
key: keys.resourceIdsKey,
},
selectedQuery: {
value: this.element.dataset.itemSelectAllSelectedAllQueryValue,
selectedAll: 'true',
key: keys.selectedQueryKey,
},
}

document.querySelectorAll('[data-target="actions-list"] > a').forEach((link) => {
document.querySelectorAll(selector).forEach((link) => {
try {
const url = new URL(link.href)

Array.from(url.searchParams.keys())
.filter((key) => key.startsWith('fields['))
.forEach((key) => url.searchParams.delete(key))

if (param === 'resourceIds') {
url.searchParams.set('fields[avo_resource_ids]', resourceIds)
url.searchParams.set('fields[avo_selected_all]', 'false')
} else if (param === 'selectedQuery') {
url.searchParams.set('fields[avo_index_query]', selectedQuery)
url.searchParams.set('fields[avo_selected_all]', 'true')
const current = params[param]
url.searchParams.set(current.key, current.value)

if (keys.selectedAllKey) {
url.searchParams.set(keys.selectedAllKey, current.selectedAll)
}

link.href = url.toString()
} catch (error) {
console.error('Error updating link:', link, error)
console.error(`Error updating link (${param}):`, link, error)
}
})
}
Expand Down
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
get "resources", to: redirect(Avo.configuration.root_path)
get "dashboards", to: redirect(Avo.configuration.root_path)

get "/bulk_update/edit", to: "bulk_update#edit", as: "edit_bulk_update"
post "/bulk_update/handle", to: "bulk_update#handle", as: "handle_bulk_update"

resources :media_library, only: [:index, :show, :update, :destroy], path: "media-library"
get "attach-media", to: "media_library#attach"

Expand Down
1 change: 1 addition & 0 deletions lib/avo/resources/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,7 @@ def entity_loader(entity)
end

def record_param
return nil if @record.nil?
@record_param ||= @record.persisted? ? @record.to_param : nil
end

Expand Down
Loading
Loading