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 @@ + 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 @@ + 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 %> - - - - - + + + + + + + @@ -73,6 +86,7 @@ <% @collection.each do |product| %> id="<%= spree_dom_id product %>" data-hook="admin_products_index_rows"> + <% end %> -
<%= 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'} %>
<%= 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 %> <% 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