diff --git a/backend/app/assets/javascripts/spree/backend.js b/backend/app/assets/javascripts/spree/backend.js
index 2b3a52922b0..f3e3a792958 100644
--- a/backend/app/assets/javascripts/spree/backend.js
+++ b/backend/app/assets/javascripts/spree/backend.js
@@ -25,6 +25,7 @@
//= require spree/backend/admin
//= require spree/backend/calculator
//= require spree/backend/checkouts/edit
+//= require spree/backend/components/modals
//= require spree/backend/components/number_with_currency
//= require spree/backend/components/tabs
//= require spree/backend/components/tooltips
diff --git a/backend/app/assets/javascripts/spree/backend/components/modals.js b/backend/app/assets/javascripts/spree/backend/components/modals.js
new file mode 100644
index 00000000000..0e6a819173f
--- /dev/null
+++ b/backend/app/assets/javascripts/spree/backend/components/modals.js
@@ -0,0 +1,21 @@
+/**
+ * Use this file to retrieve and store the modal that should be global to the
+ * site
+ *
+ * e.g.
+ * Spree.ready(function() {
+ * $('#batch-preview').each(function() {
+ * Spree.Views.Modals.batchPreview($(this))
+ * });
+ * });
+ *
+ */
+Spree.ready(function() {
+ $('#batch-preview').each(function() {
+ Spree.Views.Modals.batchPreview($(this))
+ })
+
+ $('#batch-result').each(function() {
+ Spree.Views.Modals.batchResult($(this))
+ })
+});
diff --git a/backend/app/assets/javascripts/spree/backend/components/selectable_table/summary.js b/backend/app/assets/javascripts/spree/backend/components/selectable_table/summary.js
index 3f85777a5d5..daab8cf8527 100644
--- a/backend/app/assets/javascripts/spree/backend/components/selectable_table/summary.js
+++ b/backend/app/assets/javascripts/spree/backend/components/selectable_table/summary.js
@@ -1,7 +1,12 @@
Backbone.on('selectableTable:init', function(selectableTable){
if(selectableTable.$el.find('.selectable').length > 0) {
var tr = document.createElement('tr')
- new Spree.Views.Tables.SelectableTable.Summary({el: tr, model: selectableTable.model , columns: selectableTable.maxColumns()});
+ new Spree.Views.Tables.SelectableTable.Summary({
+ el: tr,
+ model: selectableTable.model,
+ columns: selectableTable.maxColumns(),
+ selectableTable: selectableTable
+ });
selectableTable.$el.find('thead').prepend(tr);
}
})
diff --git a/backend/app/assets/javascripts/spree/backend/namespaces.js b/backend/app/assets/javascripts/spree/backend/namespaces.js
index 61f449e4998..b33fb03d7fe 100644
--- a/backend/app/assets/javascripts/spree/backend/namespaces.js
+++ b/backend/app/assets/javascripts/spree/backend/namespaces.js
@@ -10,6 +10,7 @@ _.extend(window.Spree, {
Payment: {},
Promotions: {},
Stock: {},
- Tables: {}
+ Tables: {},
+ Modals: {}
}
})
diff --git a/backend/app/assets/javascripts/spree/backend/templates/index.js b/backend/app/assets/javascripts/spree/backend/templates/index.js
index 39d4a178d98..08e018e53aa 100644
--- a/backend/app/assets/javascripts/spree/backend/templates/index.js
+++ b/backend/app/assets/javascripts/spree/backend/templates/index.js
@@ -1,6 +1,7 @@
//= require spree/backend/templates/_image
//= require spree/backend/templates/tables/selectable_label
//= require spree/backend/templates/tables/return_item_sum_amount
+//= require spree/backend/templates/tables/actions
//= require spree/backend/templates/orders/customer_details/autocomplete
//= require spree/backend/templates/orders/details_adjustment_row
//= require spree/backend/templates/orders/line_item
diff --git a/backend/app/assets/javascripts/spree/backend/templates/tables/actions.hbs b/backend/app/assets/javascripts/spree/backend/templates/tables/actions.hbs
new file mode 100644
index 00000000000..86610eba337
--- /dev/null
+++ b/backend/app/assets/javascripts/spree/backend/templates/tables/actions.hbs
@@ -0,0 +1,5 @@
+{{#if itemSelected}}
+ {{#each actions}}
+ {{{this}}}
+ {{/each}}
+{{/if}}
diff --git a/backend/app/assets/javascripts/spree/backend/templates/tables/selectable_label.hbs b/backend/app/assets/javascripts/spree/backend/templates/tables/selectable_label.hbs
index 83d5290f7d9..63d1e0b9fa1 100644
--- a/backend/app/assets/javascripts/spree/backend/templates/tables/selectable_label.hbs
+++ b/backend/app/assets/javascripts/spree/backend/templates/tables/selectable_label.hbs
@@ -1,6 +1,10 @@
|
-
+ |
{{item_selected_label}}
|
+{{#if actionable}}
+
+ |
+{{/if}}
diff --git a/backend/app/assets/javascripts/spree/backend/views/index.js b/backend/app/assets/javascripts/spree/backend/views/index.js
index 670ce59032b..4e1f0440e39 100644
--- a/backend/app/assets/javascripts/spree/backend/views/index.js
+++ b/backend/app/assets/javascripts/spree/backend/views/index.js
@@ -28,3 +28,8 @@
//= require 'spree/backend/views/tables/selectable_table'
//= require 'spree/backend/views/tables/selectable_table/summary'
//= require 'spree/backend/views/tables/selectable_table/sum_return_item_amount'
+//= require 'spree/backend/views/tables/selectable_table/actions'
+//= require 'spree/backend/views/modals'
+//= require 'spree/backend/views/modals/batch'
+//= require 'spree/backend/views/modals/batch/preview'
+//= require 'spree/backend/views/modals/batch/result'
diff --git a/backend/app/assets/javascripts/spree/backend/views/modals.js b/backend/app/assets/javascripts/spree/backend/views/modals.js
new file mode 100644
index 00000000000..ddb1a5fe328
--- /dev/null
+++ b/backend/app/assets/javascripts/spree/backend/views/modals.js
@@ -0,0 +1,32 @@
+/**
+ * Use this file to store the global modal
+ *
+ * e.g.
+ * Spree.Views.Modals = {
+ * myModal: function($el = null) {
+ * if($el != null) {
+ * this.myGlobalModal = new Spree.Views.Modals.MyModal({el: $el})
+ * }
+ *
+ * return this.myGlobalModal;
+ * },
+ * }
+ *
+ */
+Spree.Views.Modals = {
+ batchPreview: function($el = null) {
+ if($el != null) {
+ this.modalBatchPreview = new Spree.Views.Modals.Batch.Preview({el: $el})
+ }
+
+ return this.modalBatchPreview;
+ },
+
+ batchResult: function($el = null) {
+ if($el != null) {
+ this.modalBatchResult = new Spree.Views.Modals.Batch.Result({el: $el})
+ }
+
+ return this.modalBatchResult;
+ }
+};
diff --git a/backend/app/assets/javascripts/spree/backend/views/modals/batch.js b/backend/app/assets/javascripts/spree/backend/views/modals/batch.js
new file mode 100644
index 00000000000..85508e3e81e
--- /dev/null
+++ b/backend/app/assets/javascripts/spree/backend/views/modals/batch.js
@@ -0,0 +1 @@
+Spree.Views.Modals.Batch = {}
diff --git a/backend/app/assets/javascripts/spree/backend/views/modals/batch/preview.js b/backend/app/assets/javascripts/spree/backend/views/modals/batch/preview.js
new file mode 100644
index 00000000000..4dc89fbd64a
--- /dev/null
+++ b/backend/app/assets/javascripts/spree/backend/views/modals/batch/preview.js
@@ -0,0 +1,37 @@
+Spree.Views.Modals.Batch.Preview = Backbone.View.extend({
+ events: {
+ 'click #btn-process': 'process'
+ },
+
+ show: function(options) {
+ this.processBatchUrl = options.processBatchUrl;
+ this.searchForm = options.searchForm;
+ this.action = options.action;
+ this.$el.modal('show');
+ },
+
+ process: function() {
+ var inputAction = document.createElement('input');
+ inputAction.name = 'batch_action_type';
+ inputAction.value = this.action;
+ this.searchForm.append(inputAction);
+
+ this.$el.modal('hide');
+
+ Spree.ajax({
+ type: 'POST',
+ url: this.processBatchUrl,
+ data: this.searchForm.serialize(),
+ success: function() {
+ Spree.Views.Modals.batchResult().show()
+ },
+ error: function(msg) {
+ if (msg.responseJSON["error"]) {
+ show_flash('error', msg.responseJSON["error"]);
+ } else {
+ show_flash('error', "There was a problem adding this coupon code.");
+ }
+ }
+ });
+ }
+})
diff --git a/backend/app/assets/javascripts/spree/backend/views/modals/batch/result.js b/backend/app/assets/javascripts/spree/backend/views/modals/batch/result.js
new file mode 100644
index 00000000000..d6ec038a1e0
--- /dev/null
+++ b/backend/app/assets/javascripts/spree/backend/views/modals/batch/result.js
@@ -0,0 +1,14 @@
+Spree.Views.Modals.Batch.Result = Backbone.View.extend({
+ events: {
+ 'click #btn-close': 'close'
+ },
+
+ show: function() {
+ this.$el.modal('show');
+ },
+
+ close: function() {
+ this.$el.modal('hide');
+ document.location.reload();
+ }
+})
diff --git a/backend/app/assets/javascripts/spree/backend/views/tables/selectable_table/actions.js b/backend/app/assets/javascripts/spree/backend/views/tables/selectable_table/actions.js
new file mode 100644
index 00000000000..8cf972837c0
--- /dev/null
+++ b/backend/app/assets/javascripts/spree/backend/views/tables/selectable_table/actions.js
@@ -0,0 +1,67 @@
+Spree.Views.Tables.SelectableTable.Actions = Backbone.View.extend({
+ events: {
+ 'click a.batch-action': 'previewBatchAction'
+ },
+
+ initialize: function(options) {
+ this.listenTo(this.model, 'change', this.render);
+
+ this.selectableTable = options.selectableTable;
+ this.actions = this.selectableTable.$el.data('actions');
+ this.searchForm = $(this.selectableTable.$el.data('searchFormSelector')).clone();
+ this.previewBatchUrl = this.selectableTable.$el.data('previewBatchUrl');
+ this.processBatchUrl = this.selectableTable.$el.data('processBatchUrl');
+
+ this.render();
+ },
+
+ render: function() {
+ var html = HandlebarsTemplates['tables/actions']({
+ itemSelected: this.model.get('selectedItems').length > 0 || this.model.get('allSelected'),
+ actions: this.actions
+ });
+
+ this.$el.html(html);
+ },
+
+ previewBatchAction: function(e) {
+ var self = this;
+
+ var action = $(e.currentTarget).data('action');
+ var inputAction = document.createElement('input');
+ inputAction.name = 'batch_action_type';
+ inputAction.value = action;
+
+ if(this.selectableTable.$el.find('.selectAll:checked').length == 0) {
+ this.selectableTable.$el.find('.selectable:checked').each(function(_i, item){
+ self.searchForm.append($(item).clone());
+ })
+ }
+
+ this.searchForm.append(inputAction);
+
+ options = {
+ processBatchUrl: this.processBatchUrl,
+ searchForm: this.searchForm,
+ action: action
+ }
+
+ Spree.ajax({
+ type: 'POST',
+ url: this.previewBatchUrl,
+ data: this.searchForm.serialize(),
+ success: function() {
+ Spree.Views.Modals.batchPreview().show(options);
+ },
+ error: function(msg) {
+ if (msg.responseJSON["error"]) {
+ show_flash('error', msg.responseJSON["error"]);
+ } else {
+ show_flash('error', "There was a problem adding this coupon code.");
+ }
+ }
+ });
+
+ this.searchForm = $(this.selectableTable.$el.data('searchFormSelector')).clone();
+ }
+});
diff --git a/backend/app/assets/javascripts/spree/backend/views/tables/selectable_table/summary.js b/backend/app/assets/javascripts/spree/backend/views/tables/selectable_table/summary.js
index ec0d075ead0..4df2de12bc5 100644
--- a/backend/app/assets/javascripts/spree/backend/views/tables/selectable_table/summary.js
+++ b/backend/app/assets/javascripts/spree/backend/views/tables/selectable_table/summary.js
@@ -14,6 +14,15 @@ Spree.Views.Tables.SelectableTable.Summary = Backbone.View.extend({
this.listenTo(this.model, 'change', this.render)
this.colspan = options.columns - 1;
+ this.actionableColspan = 0
+
+ this.selectableTable = options.selectableTable;
+ this.actionable = this.selectableTable.$el.hasClass('actionable')
+
+ if(this.actionable) {
+ this.colspan = options.columns - 3
+ this.actionableColspan = 3
+ }
this.render();
},
@@ -23,12 +32,22 @@ Spree.Views.Tables.SelectableTable.Summary = Backbone.View.extend({
var all_items_selected = this.model.get('allSelected');
var html = HandlebarsTemplates['tables/selectable_label']({
+ actionable: this.actionable,
+ actionableColspan: this.actionableColspan,
colspan: this.colspan,
item_selected_label: this.selectedItemLabel(all_items_selected, selectedItemLength),
all_items_selected: all_items_selected
});
this.$el.html(html);
+
+ if(this.actionable) {
+ new Spree.Views.Tables.SelectableTable.Actions({
+ el: this.$el.find('th.actions'),
+ model: this.model,
+ selectableTable: this.selectableTable,
+ });
+ }
},
selectedItemLabel: function(all_selected, selected_item_length) {
diff --git a/backend/app/assets/stylesheets/spree/backend/components/selectable_table.scss b/backend/app/assets/stylesheets/spree/backend/components/selectable_table.scss
new file mode 100644
index 00000000000..00ce5fc219a
--- /dev/null
+++ b/backend/app/assets/stylesheets/spree/backend/components/selectable_table.scss
@@ -0,0 +1,4 @@
+.selectable-table .action-buttons {
+ margin-right: 0px;
+ float: right;
+}
diff --git a/backend/app/assets/stylesheets/spree/backend/spree_admin.scss b/backend/app/assets/stylesheets/spree/backend/spree_admin.scss
index 0711d85a822..b0a36a81286 100644
--- a/backend/app/assets/stylesheets/spree/backend/spree_admin.scss
+++ b/backend/app/assets/stylesheets/spree/backend/spree_admin.scss
@@ -33,6 +33,7 @@
@import 'spree/backend/components/navigation';
@import 'spree/backend/components/sidebar';
@import 'spree/backend/components/editable_table';
+@import 'spree/backend/components/selectable_table';
@import 'spree/backend/components/pills';
@import 'spree/backend/components/tabs';
diff --git a/backend/app/controllers/spree/admin/products_controller.rb b/backend/app/controllers/spree/admin/products_controller.rb
index f01e00fd1e5..27f48f12009 100644
--- a/backend/app/controllers/spree/admin/products_controller.rb
+++ b/backend/app/controllers/spree/admin/products_controller.rb
@@ -5,6 +5,11 @@ module Admin
class ProductsController < ResourceController
helper 'spree/products'
+ include Spree::Backend::Batch
+ set_batch_actions [
+ { action: 'Spree::BatchAction::DestroyRecordAction', icon: :trash, label: :delete }
+ ]
+
before_action :load_data, except: [:index]
update.before :update_before
helper_method :clone_object_url
diff --git a/backend/app/helpers/spree/admin/actionable_helper.rb b/backend/app/helpers/spree/admin/actionable_helper.rb
new file mode 100644
index 00000000000..7fc4db7e993
--- /dev/null
+++ b/backend/app/helpers/spree/admin/actionable_helper.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Spree
+ module Admin
+ module ActionableHelper
+ extend ActiveSupport::Concern
+
+ def batch_action_buttons(batch_actions)
+ batch_actions.map do |batch_action|
+ link_to_batch_action(batch_action)
+ end
+ end
+
+ def link_to_batch_action(icon:, label:, action:)
+ options = {}
+ options[:class] = "fa fa-#{icon} icon_link with-tip batch-action no-text"
+ options[:title] = label
+ options[:data] = { action: action }
+ options.delete(:no_text)
+ link_to('', '#', options)
+ end
+ end
+ end
+end
diff --git a/backend/app/views/spree/admin/batch_actions/_preview.html.erb b/backend/app/views/spree/admin/batch_actions/_preview.html.erb
new file mode 100644
index 00000000000..0a8c77e5bd8
--- /dev/null
+++ b/backend/app/views/spree/admin/batch_actions/_preview.html.erb
@@ -0,0 +1,19 @@
+
+
+
+
+
+ <%= content %>
+
+
+
+
+
diff --git a/backend/app/views/spree/admin/batch_actions/_process.html.erb b/backend/app/views/spree/admin/batch_actions/_process.html.erb
new file mode 100644
index 00000000000..92406d8ff57
--- /dev/null
+++ b/backend/app/views/spree/admin/batch_actions/_process.html.erb
@@ -0,0 +1,18 @@
+
+
+
+
+
+ <%= content %>
+
+
+
+
+
diff --git a/backend/app/views/spree/admin/batch_actions/destroy_record_action/_preview.html.erb b/backend/app/views/spree/admin/batch_actions/destroy_record_action/_preview.html.erb
new file mode 100644
index 00000000000..e0c92827fdc
--- /dev/null
+++ b/backend/app/views/spree/admin/batch_actions/destroy_record_action/_preview.html.erb
@@ -0,0 +1 @@
+<%= t('spree.batch_actions.destroy_record_action.confirm', total_count: @batch_action_collection.total_count) %>
diff --git a/backend/app/views/spree/admin/batch_actions/destroy_record_action/_result.html.erb b/backend/app/views/spree/admin/batch_actions/destroy_record_action/_result.html.erb
new file mode 100644
index 00000000000..62294c4150b
--- /dev/null
+++ b/backend/app/views/spree/admin/batch_actions/destroy_record_action/_result.html.erb
@@ -0,0 +1 @@
+<%= t('spree.batch_actions.destroy_record_action.result', total_count: @batch_action_collection.total_count) %>
diff --git a/backend/app/views/spree/admin/products/index.html.erb b/backend/app/views/spree/admin/products/index.html.erb
index 3cab1ef3d33..e8379e1ed72 100644
--- a/backend/app/views/spree/admin/products/index.html.erb
+++ b/backend/app/views/spree/admin/products/index.html.erb
@@ -53,16 +53,29 @@
<%= paginate @collection, theme: "solidus_admin" %>
<% if @collection.any? %>
-
+ <%=
+ content_tag(
+ :table,
+ class: "index selectable-table actionable",
+ id: 'listing_products',
+ data: {
+ actions: batch_action_buttons(batch_actions),
+ search_form_selector: 'form.spree\\/product_search',
+ preview_batch_url: preview_batch_admin_products_path,
+ process_batch_url: process_batch_admin_products_path
+ }
+ ) do %>
-
-
-
-
-
+
+
+
+
+
+
+ |
<%= Spree::Variant.human_attribute_name(:sku) %> |
|
<%= sort_link @search,:name, Spree::Product.human_attribute_name(:name), { default_order: "desc" }, {title: 'admin_products_listing_name_title'} %> |
@@ -73,6 +86,7 @@
<% @collection.each do |product| %>
id="<%= spree_dom_id product %>" data-hook="admin_products_index_rows">
+ <%= check_box_tag 'q[id_in][]', product.id, false, class: 'selectable' %> |
<%= product.sku %> |
<%= render 'spree/admin/shared/image', image: product.gallery.images.first, size: :mini %>
@@ -89,7 +103,7 @@
|
<% end %>
-
+ <% end %>
<% else %>
<%= render 'spree/admin/shared/no_objects_found',
diff --git a/backend/app/views/spree/admin/shared/_head.html.erb b/backend/app/views/spree/admin/shared/_head.html.erb
index be396021d45..9e90e2dea5d 100644
--- a/backend/app/views/spree/admin/shared/_head.html.erb
+++ b/backend/app/views/spree/admin/shared/_head.html.erb
@@ -46,3 +46,21 @@
<% end %>
<%= yield :head %>
+
+<%=
+render(
+ 'spree/admin/batch_actions/preview',
+ target: 'batch-preview',
+ title: 'Preview batch command',
+ content: ''
+ )
+%>
+
+<%=
+render(
+ 'spree/admin/batch_actions/process',
+ target: 'batch-result',
+ title: 'Process batch command',
+ content: ''
+ )
+%>
diff --git a/backend/config/routes.rb b/backend/config/routes.rb
index d5e7952eadc..48051242c4e 100644
--- a/backend/config/routes.rb
+++ b/backend/config/routes.rb
@@ -2,6 +2,13 @@
Spree::Core::Engine.routes.draw do
namespace :admin do
+ concern :actionable_routes do
+ collection do
+ post :preview_batch
+ post :process_batch
+ end
+ end
+
get '/search/users', to: "search#users", as: :search_users
get '/search/products', to: "search#products", as: :search_products
@@ -28,7 +35,7 @@
resources :tax_categories
- resources :products do
+ resources :products, concerns: :actionable_routes do
resources :product_properties do
collection do
post :update_positions
diff --git a/backend/lib/spree/backend.rb b/backend/lib/spree/backend.rb
index 6c63a1e36be..db9c818d22a 100644
--- a/backend/lib/spree/backend.rb
+++ b/backend/lib/spree/backend.rb
@@ -15,4 +15,5 @@
require 'spree/backend/action_callbacks'
require 'spree/backend/callbacks'
+require 'spree/backend/batch'
require 'spree/backend/engine'
diff --git a/backend/lib/spree/backend/batch.rb b/backend/lib/spree/backend/batch.rb
new file mode 100644
index 00000000000..76a24a83b41
--- /dev/null
+++ b/backend/lib/spree/backend/batch.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module Spree
+ module Backend
+ module Batch
+ extend ActiveSupport::Concern
+
+ included do
+ helper_method :batch_actions
+ before_action :batch_action_collection, only: [:preview_batch, :process_batch]
+ helper 'spree/admin/actionable'
+ end
+
+ module ClassMethods
+ attr_writer :batch_actions
+
+ def batch_actions
+ @batch_actions || []
+ end
+
+ protected
+
+ def set_batch_actions(args)
+ @batch_actions = args
+ end
+
+ def add_batch_action(batch_action)
+ @batch_actions = batch_actions << batch_action
+ end
+
+ def add_batch_actions(other_batch_actions)
+ @batch_actions = batch_actions + other_batch_actions
+ end
+ end
+
+ def collection_actions
+ super + [:preview_batch, :process_batch]
+ end
+
+ def batch_action_collection
+ @batch_action_collection = @collection
+ end
+
+ def preview_batch
+ session[:return_to] = request.url
+
+ @batch_action_type = params[:batch_action_type]
+ @batch_action_name = @batch_action_type.constantize.new.class.name.demodulize.underscore
+
+ respond_to do |format|
+ format.js do
+ render inline: "$('#batch-preview .modal-body').html('<%= escape_javascript( render(partial: \"spree/admin/batch_actions/#{@batch_action_name}/preview\") ) %>')"
+ end
+ end
+ end
+
+ def process_batch
+ session[:return_to] = request.url
+
+ @batch_action = params[:batch_action_type].constantize.new
+ @batch_action_name = @batch_action.class.name.demodulize.underscore
+ @result = @batch_action.call(batch_action_collection)
+
+ respond_to do |format|
+ format.js do
+ render inline: "$('#batch-result .modal-body').html('<%= escape_javascript( render(partial: \"spree/admin/batch_actions/#{@batch_action_name}/result\") ) %>')"
+ end
+ end
+ end
+
+ protected
+
+ def batch_actions
+ self.class.batch_actions
+ end
+ end
+ end
+end
diff --git a/backend/spec/lib/spree/backend/batch_spec.rb b/backend/spec/lib/spree/backend/batch_spec.rb
new file mode 100644
index 00000000000..b4826bcd652
--- /dev/null
+++ b/backend/spec/lib/spree/backend/batch_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Spree::Backend::Batch do
+ let(:described_module) { Spree::Backend::Batch }
+ let(:controller_class) do
+ class FakesController < ApplicationController
+ include Spree::Backend::Batch
+ set_batch_actions []
+ end
+ end
+
+ before do
+ controller_class
+ end
+
+ describe '.set_batch_actions' do
+ before do
+ FakesController.class_eval do
+ set_batch_actions [{ action: 'Spree::Action' }]
+ end
+ end
+
+ it 'has set batch actions' do
+ expect(FakesController.batch_actions).to eq [{ action: 'Spree::Action' }]
+ end
+ end
+
+ describe '.add_batch_actions' do
+ before do
+ FakesController.class_eval do
+ add_batch_actions [{ action: 'Spree::Action' }]
+ end
+ end
+
+ it 'has set batch actions' do
+ expect(FakesController.batch_actions).to eq [{ action: 'Spree::Action' }]
+ end
+ end
+
+ describe '.add_batch_action' do
+ before do
+ FakesController.class_eval do
+ add_batch_action({ action: 'Spree::Action' })
+ end
+ end
+
+ it 'has set batch actions' do
+ expect(FakesController.batch_actions).to eq [{ action: 'Spree::Action' }]
+ end
+ end
+end
diff --git a/core/app/models/spree/batch_action/base.rb b/core/app/models/spree/batch_action/base.rb
new file mode 100644
index 00000000000..48cd8761eba
--- /dev/null
+++ b/core/app/models/spree/batch_action/base.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Spree
+ module BatchAction
+ class Base
+ def call(*_args)
+ raise NotImplementedError, "Please implement '#call' in your action: #{self.class.name}"
+ end
+ end
+ end
+end
diff --git a/core/app/models/spree/batch_action/destroy_record_action.rb b/core/app/models/spree/batch_action/destroy_record_action.rb
new file mode 100644
index 00000000000..0890e224a75
--- /dev/null
+++ b/core/app/models/spree/batch_action/destroy_record_action.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Spree
+ module BatchAction
+ class DestroyRecordAction < Base
+ def call(collection)
+ collection.map do |record|
+ next if record.try(:discard)
+ next if record.try(:destroy)
+
+ record.id
+ end
+ end
+ end
+ end
+end
diff --git a/core/config/locales/en.yml b/core/config/locales/en.yml
index 3df2dd198a3..da967dec50d 100644
--- a/core/config/locales/en.yml
+++ b/core/config/locales/en.yml
@@ -1084,6 +1084,10 @@ en:
balance_due: Balance Due
base_amount: Base Amount
base_percent: Base Percent
+ batch_actions:
+ destroy_record_action:
+ confirm: You are going to destroy %{total_count} items. Are you sure?
+ result: "%{total_count} items have been deleted."
bill_address: Bill Address
billing: Billing
billing_address: Billing Address
@@ -1823,6 +1827,7 @@ en:
price_sack: Price Sack
process: Process
product: Product
+ product_batch: Product Batch
product_details: Product Details
product_has_no_description: This product has no description
product_not_available_in_this_currency: This product is not available in the selected
diff --git a/core/spec/models/spree/batch_action/base_spec.rb b/core/spec/models/spree/batch_action/base_spec.rb
new file mode 100644
index 00000000000..c51ea7a2623
--- /dev/null
+++ b/core/spec/models/spree/batch_action/base_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Spree::BatchAction::Base, type: :model do
+ subject(:call) { Spree::BatchAction::Base.new.call }
+
+ it 'raises an error when call is not overridden' do
+ expect { call }.to raise_error(NotImplementedError)
+ end
+end