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 65 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
0185150
WIP
Nevelito Feb 26, 2025
633f97d
implement bulk update
Nevelito Mar 4, 2025
83e2247
Merge branch 'main' into add_bulk_update
Nevelito Mar 4, 2025
0f85794
optymalize code
Nevelito Mar 5, 2025
fa8c558
fix standardeb errors
Nevelito Mar 10, 2025
7a67ccd
rubocop changes in bulk update
Nevelito Mar 10, 2025
7d35c46
fix next rubocop errors
Nevelito Mar 10, 2025
ade1422
small changes with rubocop and locales
Nevelito Mar 10, 2025
a2be7fb
small changes
Nevelito Mar 10, 2025
0278fca
codeclimate fix
Nevelito Mar 10, 2025
750dd25
fix codeclimate bugs
Nevelito Mar 11, 2025
ec17628
fix bugs
Nevelito Mar 11, 2025
3d3b87d
fix errors
Nevelito Mar 11, 2025
669edce
fix standardrb errors
Nevelito Mar 11, 2025
154b062
add locales
Nevelito Mar 11, 2025
fd56fe0
normilize locales
Nevelito Mar 11, 2025
182c522
changes locales, add specs, optymalize code
Nevelito Mar 12, 2025
c8f635a
fix bugs
Nevelito Mar 12, 2025
b7f529b
fix bugs
Nevelito Mar 12, 2025
aaa324e
fix bulk update controller
Nevelito Mar 12, 2025
095bacd
Merge branch 'main' into add_bulk_update
Nevelito Apr 28, 2025
4166a5f
Merge branch 'main' into add_bulk_update
Nevelito Apr 28, 2025
859296e
fix complexity
Nevelito Apr 28, 2025
d8213a8
fix too many returns
Nevelito Apr 28, 2025
9a74b8a
new idea changing back path def
Nevelito Apr 28, 2025
0fa296b
change case to if conditions
Nevelito Apr 28, 2025
94bfe9a
fix bugs
Nevelito Apr 28, 2025
8321f59
change complexity od function
Nevelito Apr 29, 2025
40af2d8
Merge branch 'main' into add_bulk_update
Nevelito Apr 29, 2025
1764142
fix bugs
Nevelito Apr 29, 2025
9ae1366
request changes
Nevelito Apr 30, 2025
f40706d
fix bulg upade select field issue
Nevelito May 5, 2025
2146fbd
route bulk update as put instead post
Paul-Bob May 6, 2025
b0ea750
rm unused method
Paul-Bob May 6, 2025
c675317
Merge branch 'main' into add_bulk_update
Paul-Bob May 6, 2025
04fae6b
Request changes
Nevelito May 6, 2025
4739bec
Update app/components/avo/views/resource_index_component.rb
Paul-Bob May 7, 2025
19eb3fb
delete unnecessary code
Nevelito May 12, 2025
d6faf95
Merge branch 'main' into add_bulk_update
Nevelito May 16, 2025
6710069
optymalize code
Nevelito May 16, 2025
8686622
optymalize code
Nevelito May 16, 2025
589cf6d
fix codeclimate error
Nevelito May 16, 2025
a9fd0e7
fix errors
Nevelito May 16, 2025
fb8ec92
changes js controller
Nevelito May 21, 2025
69fda6a
Merge branch 'main' into add_bulk_update
Nevelito May 21, 2025
017946a
fix spec
Nevelito May 21, 2025
385552c
fix
Nevelito May 21, 2025
657c5ca
tweaks
Paul-Bob May 22, 2025
9553272
add button in correct way
Nevelito Jun 3, 2025
42f5da7
set bulk update button hidden at start
Nevelito Jun 3, 2025
f9d828e
fix issue with correct updating
Nevelito Jun 16, 2025
d203f7b
fix rubocop errors
Nevelito Jun 16, 2025
88fa6ed
fix codeclimate and rubocop
Nevelito Jun 16, 2025
d4a0715
Merge branch 'main' into add_bulk_update
Nevelito Jun 16, 2025
8c9a874
fix bulk update spec
Nevelito Jun 16, 2025
58e9b38
fix rubocop issues
Nevelito Jun 16, 2025
977d046
small change
Nevelito Jun 16, 2025
9d7ef48
fix bugs
Nevelito Jun 16, 2025
495b762
fix bugs
Nevelito Jun 16, 2025
e6f1feb
Merge branch 'main' into add_bulk_update
Paul-Bob Jun 17, 2025
65a1524
tweak bulk edit button
Paul-Bob Jun 17, 2025
c87da90
request changes
Nevelito Jun 17, 2025
6deb60d
tweak
Nevelito Jun 17, 2025
39070f8
Merge branch 'main' into add_bulk_update
Nevelito Jun 30, 2025
d09bbb7
add locales
Nevelito Jun 30, 2025
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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use the put as well for bulk updates, any argument why we should use post?

Copy link
Contributor Author

@Nevelito Nevelito Apr 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can test it, when it is using put there are problems and records will not update. the case is that it have to update min. two records

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested and works well, committed here

let me know if you see any limitation and if we should revert this commit


: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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method appears to be unused, shouldn't we incorporate it within render_bulk_update_button?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you right, fixed


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