Skip to content
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

[WIP] feature: add bulk update #3695

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
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
127 changes: 127 additions & 0 deletions app/controllers/avo/bulk_update_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
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
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] || {}

resource_key = @resource_name.downcase.to_sym
current_params = params[resource_key] || {}

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

current_params.reject do |key, value|
key_sym = key.to_sym
prefilled_value = prefilled_params[key_sym]

next true if progress_fields.include?(key_sym) && prefilled_value == "" && value.to_s == "50"

prefilled_value.to_s == value.to_s
end
end

def update_records
updated_count = 0
failed_records = []

@query.each do |record|
params_to_apply.each do |key, value|
record.public_send(:"#{key}=", value)
rescue => e
puts "Błąd przypisywania pola #{key}: #{e.message}"
end

@resource.fill_record(record, params)

if record.save
updated_count += 1
else
failed_records << {record: record, errors: record.errors.full_messages}
end
rescue => e
failed_records << {record: record, errors: [e.message]}
end

[updated_count, failed_records]
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 93 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:93: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
if params[:query].present?
@query = @resource.find_record(params[:query], params: params)
else
resource_ids = action_params[:fields]&.dig(:avo_resource_ids)&.split(",") || []
@query = decrypted_query || (resource_ids.any? ? @resource.find_record(resource_ids, params: params) : [])
end
end

def set_fields
if @query.blank?
flash[:error] = "Bulk update cannot be performed without 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
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,8 @@ en:
attachment_class_detached: "%{attachment_class} detached."
attachment_destroyed: Attachment destroyed
attachment_failed: Failed to attach %{attachment_class}
bulk_update_success: "%{count} records were successfully updated."
bulk_update_failure: "Failed to update %{count} records. Errors: %{errors}"
cancel: Cancel
choose_a_country: Choose a country
choose_an_option: Choose an option
Expand Down Expand Up @@ -70,6 +72,7 @@ en:
new: new
next_page: Next page
no_cancel: No, cancel
no_changes_made: No changes were made.
no_cards_present: No cards present
no_item_found: No record found
no_options_available: No options available
Expand Down
Loading