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 all 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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<%= field_wrapper(**field_wrapper_args) do %>
<%= @form.select @field.id, options, {
include_blank: @field.include_blank
include_blank: (params[:controller] == "avo/bulk_update") || @field.include_blank
},
multiple: @field.multiple,
aria: {
Expand Down
11 changes: 11 additions & 0 deletions app/components/avo/resource_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,17 @@ def render_back_button(control)
end
end

def render_bulk_edit_button(control)
a_link bulk_edit_path,
style: :primary,
color: :primary,
icon: "avo/edit",
class: "hidden",
form_class: "flex flex-col sm:flex-row sm:inline-flex" do
control.label
end
end

def render_actions_list(actions_list)
return unless can_see_the_actions_button?

Expand Down
10 changes: 9 additions & 1 deletion app/components/avo/views/resource_edit_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class Avo::Views::ResourceEditComponent < Avo::ResourceComponent
prop :actions, default: [].freeze
prop :view, default: Avo::ViewInquirer.new(:edit).freeze
prop :display_breadcrumbs, default: true, reader: :public
prop :query

def after_initialize
@display_breadcrumbs = @reflection.blank? && display_breadcrumbs
Expand Down Expand Up @@ -72,7 +73,14 @@ def form_method
end

def form_url
if is_edit?
if params[:controller] == "avo/bulk_update"
helpers.handle_bulk_update_path(
resource_name: @resource.name,
fields: {
avo_resource_ids: params[:fields][:avo_resource_ids]
}
)
elsif is_edit?
helpers.resource_path(
record: @resource.record,
resource: @resource
Expand Down
14 changes: 14 additions & 0 deletions app/components/avo/views/resource_index_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ def title
end
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_name: @resource.name, **args)
end

# The Create button is dependent on the new? policy method.
# The create? should be called only when the user clicks the Save button so the developers gets access to the params from the form.
def can_see_the_create_button?
Expand Down
20 changes: 15 additions & 5 deletions app/controllers/avo/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,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 @@ -406,8 +406,10 @@ 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
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

last_crumb_args = {}
# If we're accessing this resource via another resource add the parent to the breadcrumbs.
Expand All @@ -428,8 +430,16 @@ 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 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
80 changes: 80 additions & 0 deletions app/controllers/avo/bulk_update_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
module Avo
class BulkUpdateController < ResourcesController
before_action :set_query, only: [:edit, :handle]

def edit
@resource.hydrate(record: @resource.model_class.new(bulk_values))

set_component_for :bulk_edit, fallback_view: :edit
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_to_apply = params[@resource_name.downcase.to_sym].compact_blank || {}

@query.each do |record|
@resource.fill_record(record, params_to_apply)
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

# This method returns a hash of the attributes of the model and their values
# If all the records have the same value for an attribute, the value is assigned to the attribute, otherwise nil is assigned
def bulk_values
@resource.model_class.attribute_names.map do |attribute_key|
values = @query.map { _1.public_send(attribute_key) }.uniq
value_to_assign = (values.size == 1) ? values.first : nil

[attribute_key, value_to_assign]
end.to_h
end

def set_query
resource_ids = params[:fields]&.dig(:avo_resource_ids)&.split(",") || []
@query = decrypted_query || (resource_ids.any? ? @resource.find_record(resource_ids, params: params) : [])
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
74 changes: 56 additions & 18 deletions app/javascript/js/controllers/item_select_all_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default class extends Controller {
this.resourceName = this.element.dataset.resourceName
this.selectedResourcesObserver = new AttributeObserver(this.element, 'data-selected-resources', this)
this.selectedResourcesObserver.start()
this.updateBulkEditLinkVisibility()
}

elementAttributeValueChanged(element) {
Expand All @@ -39,6 +40,7 @@ export default class extends Controller {
// If some are selected, mark the checkbox as indeterminate.
this.checkboxTarget.indeterminate = true
}
this.updateBulkEditLinkVisibility()
}

disconnect() {
Expand Down Expand Up @@ -71,6 +73,7 @@ export default class extends Controller {
}

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

selectAll(event) {
Expand All @@ -88,32 +91,67 @@ export default class extends Controller {
}

updateLinks(param) {
const actionButtons = document.querySelectorAll(`a[data-actions-picker-target][data-resource-name="${this.resourceName}"]`)
actionButtons.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') {
const resourceIds = JSON.parse(this.element.dataset.selectedResources).join(',')
url.searchParams.set('fields[avo_resource_ids]', resourceIds)
url.searchParams.set('fields[avo_selected_all]', 'false')
} else if (param === 'selectedQuery') {
const selectedQuery = this.element.dataset.itemSelectAllSelectedAllQueryValue
url.searchParams.set('fields[avo_index_query]', selectedQuery)
url.searchParams.set('fields[avo_selected_all]', 'true')
}
this.updateActionLinks(param, '[data-target="actions-list"] > a')
}

updateBulkEditLink(param) {
this.updateActionLinks(param, 'a[href*="/admin/bulk_update/edit"]')
this.updateBulkEditLinkVisibility()
}

updateActionLinks(param, selector) {
Copy link

Choose a reason for hiding this comment

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

Function updateActionLinks has 30 lines of code (exceeds 25 allowed). Consider refactoring.

const selectedResourcesArray = JSON.parse(this.element.dataset.selectedResources)
const selectedResources = selectedResourcesArray.join(',')
const selectedQuery = this.element.dataset.itemSelectAllSelectedAllQueryValue

document.querySelectorAll(selector).forEach((link) => {
try {
const url = this.buildUpdatedUrl(link, param, selectedResources, selectedQuery)
link.href = url.toString()
} catch (error) {
console.error('Error updating link:', link, error)
}
})
}

buildUpdatedUrl(link, param, selectedResources, selectedQuery) {
const url = new URL(link.href)

// Remove old field parameters
Array.from(url.searchParams.keys())
.filter((key) => key.startsWith('fields['))
.forEach((key) => url.searchParams.delete(key))

const isBulkUpdate = url.pathname.includes('/admin/bulk_update/edit')
const resourceIdsKey = 'fields[avo_resource_ids]'
const selectedQueryKey = isBulkUpdate ? 'fields[avo_selected_query]' : 'fields[avo_index_query]'
const selectedAllKey = 'fields[avo_selected_all]'

if (param === 'resourceIds') {
url.searchParams.set(resourceIdsKey, selectedResources)
url.searchParams.set(selectedAllKey, 'false')
} else if (param === 'selectedQuery') {
url.searchParams.set(selectedQueryKey, selectedQuery)
url.searchParams.set(selectedAllKey, 'true')
}

return url
}

updateBulkEditLinkVisibility() {
const bulkUpdateLink = document.querySelector('a[href*="/admin/bulk_update/edit"]')
if (!bulkUpdateLink) return

const selectedResourcesArray = JSON.parse(this.element.dataset.selectedResources)
const resourceCount = selectedResourcesArray.length

if (resourceCount >= 2) {
bulkUpdateLink.classList.remove('hidden')
} else {
bulkUpdateLink.classList.add('hidden')
}
}

resetUnselected() {
this.selectedAllValue = false
this.unselectedMessageTarget.classList.remove('hidden')
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), as: :avo_resources_redirect
get "dashboards", to: redirect(Avo.configuration.root_path), as: :avo_dashboards_redirect

get "/bulk_update/edit", to: "bulk_update#edit", as: "edit_bulk_update"
put "/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
2 changes: 1 addition & 1 deletion lib/avo/concerns/has_controls.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def render_edit_controls
end

def render_index_controls(item:)
[BackButton.new, AttachButton.new(item: item), ActionsList.new(as_index_control: true), CreateButton.new(item: item)]
[BackButton.new, AttachButton.new(item: item), BulkEditButton.new, ActionsList.new(as_index_control: true), CreateButton.new(item: item)]
end

def render_row_controls(item:)
Expand Down
16 changes: 16 additions & 0 deletions lib/avo/resources/controls/bulk_edit_button.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module Avo
module Resources
module Controls
class BulkEditButton < BaseControl
def initialize(**args)
super(**args)

@label = args[:label] || I18n.t("avo.bulk_edit").capitalize
@icon = args[:icon] || "avo/edit"
@style = args[:style] || :primary
@color = args[:color] || :primary
end
end
end
end
end
3 changes: 3 additions & 0 deletions lib/generators/avo/templates/locales/avo.ar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ ar:
attachment_class_detached: "%{attachment_class} تم فصل"
attachment_destroyed: تم حذف المرفق
attachment_failed: فشل في إرفاق %{attachment_class}
bulk_edit: تحرير جماعي
bulk_update_failure: فشل في تحديث السجلات.
bulk_update_success: تم تنفيذ العملية الجماعية بنجاح.
cancel: إلغاء
choose_a_country: اختر دولة
choose_an_option: اختر خيارًا
Expand Down
3 changes: 3 additions & 0 deletions lib/generators/avo/templates/locales/avo.de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ de:
attachment_class_detached: "%{attachment_class} abgehängt."
attachment_destroyed: Anhang gelöscht
attachment_failed: "%{attachment_class} konnte nicht angehängt werden"
bulk_edit: Massenbearbeitung
bulk_update_failure: Aktualisierung der Datensätze fehlgeschlagen.
bulk_update_success: Massenaktion erfolgreich ausgeführt.
cancel: Abbrechen
choose_a_country: Land auswählen
choose_an_option: Option auswählen
Expand Down
3 changes: 3 additions & 0 deletions lib/generators/avo/templates/locales/avo.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ en:
attachment_class_detached: "%{attachment_class} detached."
attachment_destroyed: Attachment destroyed
attachment_failed: Failed to attach %{attachment_class}
bulk_edit: Bulk edit
bulk_update_failure: Failed to update records.
bulk_update_success: Bulk action run successfully.
cancel: Cancel
choose_a_country: Choose a country
choose_an_option: Choose an option
Expand Down
3 changes: 3 additions & 0 deletions lib/generators/avo/templates/locales/avo.es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ es:
attachment_class_detached: "%{attachment_class} adjuntado/a."
attachment_destroyed: Adjunto eliminado
attachment_failed: No se pudo adjuntar %{attachment_class}
bulk_edit: Edición masiva
bulk_update_failure: Error al actualizar los registros.
bulk_update_success: Acción masiva ejecutada con éxito.
cancel: Cancelar
choose_a_country: Elige un país
choose_an_option: Elige una opción
Expand Down
Loading
Loading