mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-13 23:37:47 +00:00
Merge pull request #9804 from jibees/9420-resend-confirmation-email-in-bulk
Admin, Orders list: Resend confirmation email in bulk
This commit is contained in:
15
app/components/confirm_modal_component.rb
Normal file
15
app/components/confirm_modal_component.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ConfirmModalComponent < ModalComponent
|
||||
def initialize(id:, confirm_actions: nil, controllers: nil)
|
||||
super(id: id, close_button: true)
|
||||
@confirm_actions = confirm_actions
|
||||
@controllers = controllers
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def close_button_class
|
||||
"secondary"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
%div{ id: @id, "data-controller": "modal #{@controllers}", "data-action": "keyup@document->modal#closeIfEscapeKey" }
|
||||
.reveal-modal-bg.fade{ "data-modal-target": "background", "data-action": "click->modal#close" }
|
||||
.reveal-modal.fade.tiny.help-modal{ "data-modal-target": "modal" }
|
||||
= content
|
||||
|
||||
.modal-actions
|
||||
%input{ class: "button icon-plus #{close_button_class}", type: 'button', value: t('js.admin.modals.cancel'), "data-action": "click->modal#close" }
|
||||
%input{ class: "button icon-plus primary", type: 'button', value: t('js.admin.modals.confirm'), "data-action": @confirm_actions }
|
||||
@@ -0,0 +1,4 @@
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
@@ -1,26 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class HelpModalComponent < ViewComponent::Base
|
||||
class HelpModalComponent < ModalComponent
|
||||
def initialize(id:, close_button: true)
|
||||
@id = id
|
||||
@close_button = close_button
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def close_button_class
|
||||
if namespace == "admin"
|
||||
"red"
|
||||
else
|
||||
"primary"
|
||||
end
|
||||
end
|
||||
|
||||
def close_button?
|
||||
!!@close_button
|
||||
end
|
||||
|
||||
def namespace
|
||||
helpers.controller_path.split("/").first
|
||||
super(id: id, close_button: close_button)
|
||||
end
|
||||
end
|
||||
|
||||
26
app/components/modal_component.rb
Normal file
26
app/components/modal_component.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ModalComponent < ViewComponent::Base
|
||||
def initialize(id:, close_button: true)
|
||||
@id = id
|
||||
@close_button = close_button
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def close_button_class
|
||||
if namespace == "admin"
|
||||
"red"
|
||||
else
|
||||
"primary"
|
||||
end
|
||||
end
|
||||
|
||||
def close_button?
|
||||
!!@close_button
|
||||
end
|
||||
|
||||
def namespace
|
||||
helpers.controller_path.split("/").first
|
||||
end
|
||||
end
|
||||
@@ -16,4 +16,11 @@ class ApplicationReflex < StimulusReflex::Reflex
|
||||
#
|
||||
# For code examples, considerations and caveats, see:
|
||||
# https://docs.stimulusreflex.com/rtfm/patterns#internationalization
|
||||
include CanCan::ControllerAdditions
|
||||
|
||||
delegate :current_user, to: :connection
|
||||
|
||||
def current_ability
|
||||
Spree::Ability.new(current_user)
|
||||
end
|
||||
end
|
||||
|
||||
13
app/reflexes/resend_confirmation_email_reflex.rb
Normal file
13
app/reflexes/resend_confirmation_email_reflex.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ResendConfirmationEmailReflex < ApplicationReflex
|
||||
def confirm(order_ids)
|
||||
Spree::Order.where(id: order_ids).find_each do |o|
|
||||
Spree::OrderMailer.confirm_email_for_customer(o.id, true).deliver_later if can? :resend, o
|
||||
end
|
||||
|
||||
flash[:success] = I18n.t("admin.resend_confirmation_emails_feedback", count: order_ids.count)
|
||||
cable_ready.dispatch_event(name: "modal:close")
|
||||
morph "#flashes", render(partial: "shared/flashes", locals: { flashes: flash })
|
||||
end
|
||||
end
|
||||
@@ -2,6 +2,6 @@
|
||||
- if defined? flashes
|
||||
- flashes.each do |type, msg|
|
||||
%alert.animate-show{"data-controller": "flash"}
|
||||
%div{type: "#{type}", class: "alert-box #{type == 'error' ? 'alert' : type}"}
|
||||
.flash{type: "#{type}", class: "alert-box #{type == 'error' ? 'alert' : type}"}
|
||||
%span= msg
|
||||
%a.small.close{"data-action": "click->flash#close"} ×
|
||||
|
||||
@@ -30,13 +30,15 @@
|
||||
="#{t('admin.actions')}".html_safe
|
||||
%span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" }
|
||||
%div.menu{ 'ng-show' => "expanded" }
|
||||
%div.menu_item
|
||||
%span.name{ "data-controller": "modal-link", "data-action": "click->modal-link#open", "data-modal-link-target-value": "resend_confirmation" }
|
||||
= t('.resend_confirmation')
|
||||
%div.menu_item
|
||||
%span.name.invoices-modal{'ng-controller' => 'bulkInvoiceCtrl', 'ng-click' => 'createBulkInvoice()' }
|
||||
= t('.print_invoices')
|
||||
%div.menu_item
|
||||
%span.name{'ng-controller' => 'bulkCancelCtrl', 'ng-click' => 'cancelSelectedOrders()' }
|
||||
= t('.cancel_orders')
|
||||
|
||||
|
||||
= render partial: 'per_page_controls', locals: { position: "right" }
|
||||
|
||||
@@ -63,7 +65,7 @@
|
||||
%tbody
|
||||
%tr{ng: {repeat: 'order in orders track by order.id', class: {even: "'even'", odd: "'odd'"}}, 'ng-class' => "{'state-{{order.state}}': true, 'row-loading': rowStatus[order.id] == 'loading'}"}
|
||||
%td.align-center
|
||||
%input{type: 'checkbox', 'ng-model' => 'checkboxes[order.id]', 'ng-change' => 'toggleSelection(order.id)'}
|
||||
%input{type: 'checkbox', 'ng-model' => 'checkboxes[order.id]', 'ng-change' => 'toggleSelection(order.id)', value: '{{order.id}}', name: 'order_ids[]'}
|
||||
%td.align-center
|
||||
{{order.distributor_name}}
|
||||
%td.align-center
|
||||
@@ -118,3 +120,7 @@
|
||||
= t('.no_orders_found')
|
||||
|
||||
= render 'spree/admin/shared/custom-confirm'
|
||||
|
||||
= render ConfirmModalComponent.new(id: "resend_confirmation", confirm_actions: "click->resend-confirmation-email#confirm", controllers: "resend-confirmation-email") do
|
||||
.margin-bottom-30
|
||||
= t('.resend_confirmation_confirm_html')
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
- if flash[:success]
|
||||
.flash.success= flash[:success]
|
||||
|
||||
= render partial: "shared/flashes"
|
||||
|
||||
= render partial: "spree/layouts/admin/progress_spinner"
|
||||
|
||||
%header#header{"data-hook" => ""}
|
||||
|
||||
@@ -1,33 +1,10 @@
|
||||
import { Controller } from "stimulus";
|
||||
import { useOpenAndCloseAsAModal } from "./mixins/useOpenAndCloseAsAModal";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["background", "modal"];
|
||||
|
||||
open() {
|
||||
this.backgroundTarget.style.display = "block";
|
||||
this.modalTarget.style.display = "block";
|
||||
|
||||
setTimeout(() => {
|
||||
this.modalTarget.classList.add("in");
|
||||
this.backgroundTarget.classList.add("in");
|
||||
document.querySelector("body").classList.add("modal-open");
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.modalTarget.classList.remove("in");
|
||||
this.backgroundTarget.classList.remove("in");
|
||||
document.querySelector("body").classList.remove("modal-open");
|
||||
|
||||
setTimeout(() => {
|
||||
this.backgroundTarget.style.display = "none";
|
||||
this.modalTarget.style.display = "none";
|
||||
}, 200);
|
||||
}
|
||||
|
||||
closeIfEscapeKey(e) {
|
||||
if (e.code == "Escape") {
|
||||
this.close();
|
||||
}
|
||||
connect() {
|
||||
useOpenAndCloseAsAModal(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import { Controller } from "stimulus";
|
||||
import ModalLinkController from "./modal_link_controller";
|
||||
|
||||
export default class extends Controller {
|
||||
static values = { target: String };
|
||||
|
||||
open() {
|
||||
let helpModal = document.getElementById(this.targetValue);
|
||||
let helpModalController =
|
||||
this.application.getControllerForElementAndIdentifier(
|
||||
helpModal,
|
||||
"help-modal"
|
||||
);
|
||||
helpModalController.open();
|
||||
export default class extends ModalLinkController {
|
||||
getIdentifier() {
|
||||
return "help-modal";
|
||||
}
|
||||
}
|
||||
|
||||
31
app/webpacker/controllers/mixins/useOpenAndCloseAsAModal.js
Normal file
31
app/webpacker/controllers/mixins/useOpenAndCloseAsAModal.js
Normal file
@@ -0,0 +1,31 @@
|
||||
export const useOpenAndCloseAsAModal = (controller) => {
|
||||
Object.assign(controller, {
|
||||
open: function () {
|
||||
this.backgroundTarget.style.display = "block";
|
||||
this.modalTarget.style.display = "block";
|
||||
|
||||
setTimeout(() => {
|
||||
this.modalTarget.classList.add("in");
|
||||
this.backgroundTarget.classList.add("in");
|
||||
document.querySelector("body").classList.add("modal-open");
|
||||
});
|
||||
}.bind(controller),
|
||||
|
||||
close: function () {
|
||||
this.modalTarget.classList.remove("in");
|
||||
this.backgroundTarget.classList.remove("in");
|
||||
document.querySelector("body").classList.remove("modal-open");
|
||||
|
||||
setTimeout(() => {
|
||||
this.backgroundTarget.style.display = "none";
|
||||
this.modalTarget.style.display = "none";
|
||||
}, 200);
|
||||
}.bind(controller),
|
||||
|
||||
closeIfEscapeKey: function (e) {
|
||||
if (e.code == "Escape") {
|
||||
this.close();
|
||||
}
|
||||
}.bind(controller),
|
||||
});
|
||||
};
|
||||
15
app/webpacker/controllers/modal_controller.js
Normal file
15
app/webpacker/controllers/modal_controller.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Controller } from "stimulus";
|
||||
import { useOpenAndCloseAsAModal } from "./mixins/useOpenAndCloseAsAModal";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["background", "modal"];
|
||||
|
||||
connect() {
|
||||
useOpenAndCloseAsAModal(this);
|
||||
window.addEventListener("modal:close", this.close.bind(this));
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
window.removeEventListener("modal:close", this.close);
|
||||
}
|
||||
}
|
||||
18
app/webpacker/controllers/modal_link_controller.js
Normal file
18
app/webpacker/controllers/modal_link_controller.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Controller } from "stimulus";
|
||||
|
||||
export default class extends Controller {
|
||||
static values = { target: String };
|
||||
|
||||
open() {
|
||||
let modal = document.getElementById(this.targetValue);
|
||||
let modalController = this.application.getControllerForElementAndIdentifier(
|
||||
modal,
|
||||
this.getIdentifier()
|
||||
);
|
||||
modalController.open();
|
||||
}
|
||||
|
||||
getIdentifier() {
|
||||
return "modal";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import ApplicationController from "./application_controller";
|
||||
|
||||
export default class extends ApplicationController {
|
||||
connect() {
|
||||
super.connect();
|
||||
}
|
||||
|
||||
confirm() {
|
||||
const order_ids = [];
|
||||
document
|
||||
.querySelectorAll("#listing_orders input[name='order_ids[]']:checked")
|
||||
.forEach((checkbox) => {
|
||||
order_ids.push(checkbox.value);
|
||||
});
|
||||
|
||||
this.stimulate("ResendConfirmationEmailReflex#confirm", order_ids);
|
||||
}
|
||||
}
|
||||
@@ -127,3 +127,4 @@
|
||||
@import "app/components/pagination_component/pagination_component";
|
||||
@import "app/components/table_header_component/table_header_component";
|
||||
@import "app/components/search_input_component/search_input_component";
|
||||
@import 'app/components/confirm_modal_component/confirm_modal_component';
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
font-size: 120%;
|
||||
color: $color-1;
|
||||
font-weight: 600;
|
||||
margin-top: 0;
|
||||
|
||||
&.notice { background-color: rgba($color-notice, 0.8) }
|
||||
&.success { background-color: rgba($color-success, 0.8) }
|
||||
|
||||
@@ -77,3 +77,4 @@ ofn-modal {
|
||||
@import '../admin/shared/scroll_bar';
|
||||
|
||||
@import 'app/components/help_modal_component/help_modal_component';
|
||||
@import 'app/components/confirm_modal_component/confirm_modal_component';
|
||||
|
||||
@@ -48,6 +48,9 @@ Openfoodnetwork::Application.configure do
|
||||
reconnect_attempts: 1
|
||||
}
|
||||
|
||||
config.action_cable.url = "#{ENV['OFN_URL']}/cable"
|
||||
config.action_cable.allowed_request_origins = [/http:\/\/#{ENV['OFN_URL']}\/*/, /https:\/\/#{ENV['OFN_URL']}\/*/]
|
||||
|
||||
# Enable serving of images, stylesheets, and JavaScripts from an asset server
|
||||
# config.action_controller.asset_host = "http://assets.example.com"
|
||||
|
||||
|
||||
@@ -48,6 +48,9 @@ Openfoodnetwork::Application.configure do
|
||||
reconnect_attempts: 1
|
||||
}
|
||||
|
||||
config.action_cable.url = "#{ENV['OFN_URL']}/cable"
|
||||
config.action_cable.allowed_request_origins = [/http:\/\/#{ENV['OFN_URL']}\/*/, /https:\/\/#{ENV['OFN_URL']}\/*/]
|
||||
|
||||
# Enable serving of images, stylesheets, and JavaScripts from an asset server
|
||||
# config.action_controller.asset_host = "http://assets.example.com"
|
||||
|
||||
|
||||
@@ -1566,6 +1566,10 @@ en:
|
||||
stripe_connect_settings:
|
||||
resource: Stripe Connect configuration
|
||||
|
||||
resend_confirmation_emails_feedback:
|
||||
one: "Confirmation email sent for 1 order."
|
||||
other: "Confirmation emails sent for %{count} orders."
|
||||
|
||||
# API
|
||||
#
|
||||
api:
|
||||
@@ -3036,8 +3040,10 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
deleting_item_will_cancel_order: "This operation will result in one or more empty orders, which will be cancelled. Do you wish to proceed?"
|
||||
modals:
|
||||
got_it: "Got it"
|
||||
confirm: "Confirm"
|
||||
close: "Close"
|
||||
continue: "Continue"
|
||||
cancel: "Cancel"
|
||||
invite: "Invite"
|
||||
invite_title: "Invite an unregistered user"
|
||||
tag_rule_help:
|
||||
@@ -3905,6 +3911,8 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
viewing: "Viewing %{start} to %{end}."
|
||||
print_invoices: "Print Invoices"
|
||||
cancel_orders: "Cancel Orders"
|
||||
resend_confirmation: "Resend Confirmation"
|
||||
resend_confirmation_confirm_html: "This will resend the confirmation email to the customer.<br />Are you sure you want to proceed?"
|
||||
selected:
|
||||
zero: "No order selected"
|
||||
one: "1 order selected"
|
||||
|
||||
@@ -122,6 +122,62 @@ describe '
|
||||
end
|
||||
end
|
||||
|
||||
context "bulk actions" do
|
||||
context "resend confirmation email" do
|
||||
it "can bulk send email to 2 orders" do
|
||||
login_as_admin_and_visit spree.admin_orders_path
|
||||
|
||||
page.find("#listing_orders tbody tr:nth-child(1) input[name='order_ids[]']").click
|
||||
page.find("#listing_orders tbody tr:nth-child(2) input[name='order_ids[]']").click
|
||||
|
||||
page.find("span.icon-reorder", text: "ACTIONS").click
|
||||
within ".ofn-drop-down-with-prepend .menu" do
|
||||
page.find("span", text: "Resend Confirmation").click
|
||||
end
|
||||
|
||||
expect(page).to have_content "Are you sure you want to proceed?"
|
||||
|
||||
within ".reveal-modal" do
|
||||
expect {
|
||||
find_button("Confirm").click
|
||||
}.to enqueue_job(ActionMailer::MailDeliveryJob).exactly(:twice)
|
||||
end
|
||||
|
||||
expect(page).to have_content "Confirmation emails sent for 2 orders."
|
||||
end
|
||||
|
||||
context "for a hub manager" do
|
||||
before do
|
||||
login_to_admin_as user
|
||||
visit spree.admin_orders_path
|
||||
end
|
||||
|
||||
it "cannnot send emails to orders if permission have been revoked in the meantime" do
|
||||
page.find("#listing_orders tbody tr:nth-child(1) input[name='order_ids[]']").click
|
||||
|
||||
# Find the clicked order
|
||||
order = Spree::Order.find_by(id: page.find("#listing_orders tbody tr:nth-child(1) input[name='order_ids[]']").value)
|
||||
# Revoke permission for the current user on that specific order by changing its owners
|
||||
order.update_attribute(:created_by, create(:user))
|
||||
order.update_attribute(:distributor, create(:distributor_enterprise))
|
||||
|
||||
page.find("span.icon-reorder", text: "ACTIONS").click
|
||||
within ".ofn-drop-down-with-prepend .menu" do
|
||||
page.find("span", text: "Resend Confirmation").click
|
||||
end
|
||||
|
||||
expect(page).to have_content "Are you sure you want to proceed?"
|
||||
|
||||
within ".reveal-modal" do
|
||||
expect {
|
||||
find_button("Confirm").click
|
||||
}.to_not enqueue_job(ActionMailer::MailDeliveryJob)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with a capturable order" do
|
||||
before do
|
||||
order.finalize! # ensure order has a payment to capture
|
||||
|
||||
Reference in New Issue
Block a user