Revert front end validation and implement backend validation for changes in datetime order cycle values [OFN-11613]

This commit is contained in:
wandji20
2024-07-24 20:06:25 +01:00
parent 91fddeaa8b
commit ea238829a8
13 changed files with 108 additions and 153 deletions

View File

@@ -19,6 +19,8 @@ angular.module('admin.orderCycles')
$scope.submit = ($event, destination) ->
$event.preventDefault()
$scope.order_cycle?.trigger_action = $($event.target).data('trigger-action');
$scope.order_cycle?.confirm = $($event.target).data('confirm');
StatusMessage.display 'progress', t('js.saving')
OrderCycle.update(destination, $scope.order_cycle_form)

View File

@@ -22,6 +22,8 @@ angular.module('admin.orderCycles').controller "AdminSimpleEditOrderCycleCtrl",
$scope.submit = ($event, destination) ->
$event.preventDefault()
$scope.order_cycle?.trigger_action = $($event.target).data('trigger-action');
$scope.order_cycle?.confirm = $($event.target).data('confirm');
StatusMessage.display 'progress', t('js.saving')
OrderCycle.mirrorIncomingToOutgoingProducts()
OrderCycle.update(destination, $scope.order_cycle_form) if OrderCycle.confirmNoDistributors()

View File

@@ -161,14 +161,25 @@ angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, $
StatusMessage.display('failure', t('js.order_cycles.create_failure'))
update: (destination, form) ->
oc = new OrderCycleResource({order_cycle: this.dataForSubmit()})
oc = new OrderCycleResource({
order_cycle: this.dataForSubmit(),
confirm: this.order_cycle.confirm,
trigger_action: this.order_cycle.trigger_action
})
oc.$update {order_cycle_id: this.order_cycle.id, reloading: (if destination? then 1 else 0)}, (data) =>
# Hide all confirmation buttons in warning modal
$('#linked-order-warning-modal .modal-actions button.secondary').css({ display: 'none' })
# Show the appropriate confirmation button, open warning modal, and return
if data.trigger_action
StatusMessage.display 'notice', "You have unsaved changes"
$("#linked-order-warning-modal button[data-trigger-action=#{data.trigger_action}]").css({ display: 'block' });
$('.warning-modal button.modal-target-trigger').trigger('click');
return;
form.$setPristine() if form
if destination?
$window.location = destination
else
if ($window.adminOrderCycleUpdateCallback)
adminOrderCycleUpdateCallback(data.order_cycle);
StatusMessage.display 'success', t('js.order_cycles.update_success')
, (response) ->
if response.data.errors?

View File

@@ -24,6 +24,19 @@
max-width: 100%;
height: auto;
}
.flex-column {
display: flex;
flex-direction: column;
}
.gap-1 {
gap: 1rem;
}
.gap-2 {
gap: 2rem;
}
}
/* prevent arrow on selected admin menu item appearing above modal */

View File

@@ -11,6 +11,7 @@ module Admin
before_action :remove_protected_attrs, only: [:update]
before_action :require_order_cycle_set_params, only: [:bulk_update]
around_action :protect_invalid_destroy, only: :destroy
before_action :verify_datetime_change, only: :update
def index
respond_to do |format|
@@ -70,12 +71,7 @@ module Admin
respond_to do |format|
flash[:success] = t('.success') if params[:reloading] == '1'
format.html { redirect_to_after_update_path }
format.json {
render json: { success: true, order_cycle: {
orders_open_at: @order_cycle.orders_open_at&.strftime('%Y-%m-%d %H:%M'),
orders_close_at: @order_cycle.orders_close_at&.strftime('%Y-%m-%d %H:%M')
} }
}
format.json { render json: { success: true } }
end
elsif request.format.html?
render :checkout_options
@@ -240,7 +236,7 @@ module Admin
else
begin
yield
rescue ActiveRecord::InvalidForeignKey
rescue ActiveRecord::InvalidForeignKey, ActiveRecord::DeleteRestrictionError
redirect_to main_app.admin_order_cycles_url
flash[:error] = I18n.t('admin.order_cycles.destroy_errors.orders_present')
end
@@ -299,5 +295,23 @@ module Admin
collection_attributes: [:id] + PermittedAttributes::OrderCycle.basic_attributes
).to_h.with_indifferent_access
end
# Check that order cycle datetime values changed if it has existing orders
def verify_datetime_change
return unless params[:order_cycle][:confirm]
return unless @order_cycle.orders.exists?
return if same_dates(@order_cycle.orders_open_at&.to_s,
order_cycle_params[:orders_open_at]) &&
same_dates(@order_cycle.orders_close_at&.to_s, order_cycle_params[:orders_close_at])
render json: { trigger_action: params[:order_cycle][:trigger_action] }
end
def same_dates(string1, string2)
false unless string1 && string2
DateTime.parse(string1).strftime('%Y-%m-%d %H:%M') ==
DateTime.parse(string2).strftime('%Y-%m-%d %H:%M')
end
end
end

View File

@@ -24,6 +24,7 @@ class OrderCycle < ApplicationRecord
where incoming: false
}, class_name: "Exchange", dependent: :destroy
has_many :orders, class_name: 'Spree::Order', dependent: :restrict_with_exception
has_many :suppliers, -> { distinct }, source: :sender, through: :cached_incoming_exchanges
has_many :distributors, -> { distinct }, source: :receiver, through: :cached_outgoing_exchanges
has_many :order_cycle_schedules, dependent: :destroy

View File

@@ -0,0 +1,14 @@
.modal-body.flex-column-gap-1
%h6
= t('admin.order_cycles.edit.linked_schedule_warning_modal.title')
%div{ style: 'font-size: 1rem;' }
= t('admin.order_cycles.edit.linked_schedule_warning_modal.content')
%p.modal-actions.justify-end.gap-1
%button.button.secondary{ "ng-click": "submit($event, null)", type: "button", style: "display: none;", data: { action: 'click->modal#close', 'trigger-action': 'save' } }
= t('admin.order_cycles.edit.linked_schedule_warning_modal.proceed')
%button.button.secondary{ "ng-click": "submit($event, '#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')", type: "button", style: "display: none;", data: { action: 'click->modal#close', 'trigger-action': 'saveAndNext' } }
= t('admin.order_cycles.edit.linked_schedule_warning_modal.proceed')
%button.button.secondary{ "ng-click": "submit($event, '#{main_app.admin_order_cycles_path}')", type: "button", style: "display: none;", data: { action: 'click->modal#close', 'trigger-action': 'saveAndBack' } }
= t('admin.order_cycles.edit.linked_schedule_warning_modal.proceed')
%button.button.primary{ type: "button", 'data-action': 'click->modal#close' }
= t('admin.order_cycles.edit.linked_schedule_warning_modal.cancel')

View File

@@ -11,7 +11,7 @@
= f.label :orders_open_at, t('.orders_open')
.omega.six.columns.fullwidth_inputs
- if viewing_as_coordinator_of?(@order_cycle)
= f.text_field :orders_open_at, data: { controller: "flatpickr", "flatpickr-enable-time-value": true, action: 'order-cycle#toggleSaveBtns', 'order-cycle-target': 'input' }, 'ng-model' => 'order_cycle.orders_open_at', 'ng-if' => 'loaded()', 'change-warning' => 'order_cycle', class: "datetimepicker"
= f.text_field :orders_open_at, data: { controller: "flatpickr", "flatpickr-enable-time-value": true }, 'ng-model' => 'order_cycle.orders_open_at', 'ng-if' => 'loaded()', 'change-warning' => 'order_cycle', class: "datetimepicker"
- else
{{ order_cycle.orders_open_at }}
@@ -24,7 +24,7 @@
= f.label :orders_close, t('.orders_close')
.six.columns.omega.fullwidth_inputs
- if viewing_as_coordinator_of?(@order_cycle)
= f.text_field :orders_close_at, data: { controller: "flatpickr", "flatpickr-enable-time-value": true, action: 'order-cycle#toggleSaveBtns', 'order-cycle-target': 'input' }, 'ng-model' => 'order_cycle.orders_close_at', 'ng-if' => 'loaded()', 'change-warning' => 'order_cycle', class: "datetimepicker"
= f.text_field :orders_close_at, data: { controller: "flatpickr", "flatpickr-enable-time-value": true }, 'ng-model' => 'order_cycle.orders_close_at', 'ng-if' => 'loaded()', 'change-warning' => 'order_cycle', class: "datetimepicker"
- else
{{ order_cycle.orders_close_at }}

View File

@@ -15,46 +15,26 @@
= t :edit_order_cycle
- ng_controller = @order_cycle.simple? ? 'AdminSimpleEditOrderCycleCtrl' : 'AdminEditOrderCycleCtrl'
- has_scheduled_order = @order_cycle.schedules.exists?
= admin_inject_order_cycle_instance(@order_cycle)
= form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.orderCycles', 'ng-controller' => ng_controller, name: 'order_cycle_form', data: { controller: 'modal modal-link order-cycle', "modal-link-target-value": "linked-schedule-warning-modal", 'order-cycle-has-schedule-value': has_scheduled_order, 'order-cycle-init-vals-value': { 'order_cycle[orders_open_at]': @order_cycle.orders_open_at&.strftime('%Y-%m-%d %H:%M'), 'order_cycle[orders_close_at]': @order_cycle.orders_close_at&.strftime('%Y-%m-%d %H:%M') } } } do |f|
= form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.orderCycles', 'ng-controller' => ng_controller, name: 'order_cycle_form'} do |f|
%save-bar{ dirty: "order_cycle_form.$dirty", persist: "true" }
%div#form-actions
%input.red{ type: "button", value: t('.save'), "ng-click": "submit($event, null)", "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid" }
- if @order_cycle.simple?
%input.red{ type: "button", value: t('.save_and_back_to_list'), "ng-click": "submit($event, '#{main_app.admin_order_cycles_path}')", "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid" }
- else
%input.red{ type: "button", value: t('.save_and_next'), "ng-click": "submit($event, '#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')", "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid" }
%input{ type: "button", value: t('.next'), "ng-click": "cancel('#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')", "ng-disabled": "order_cycle_form.$dirty" }
%input{ type: "button", "ng-value": "order_cycle_form.$dirty ? '#{t('.cancel')}' : '#{t('.back_to_list')}'", "ng-click": "cancel('#{main_app.admin_order_cycles_path}')" }
%div#modal-actions{style: "display: none;"}
%input.red{ type: "button", value: t('.save'), "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid", data: { 'action': 'click->modal-link#open click->order-cycle#updateModalConfirmButton', 'target': 'save'} }
- if @order_cycle.simple?
%input.red{ type: "button", value: t('.save_and_back_to_list'), "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid", data: { 'action': 'click->modal-link#open click->order-cycle#updateModalConfirmButton', 'target': 'saveAndBack'} }
- else
%input.red{ type: "button", value: t('.save_and_next'), "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid", data: { 'action': 'click->modal-link#open click->order-cycle#updateModalConfirmButton', 'target': 'saveAndNext'} }
%input{ type: "button", value: t('.next'), "ng-click": "cancel('#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')", "ng-disabled": "order_cycle_form.$dirty" }
%input{ type: "button", "ng-value": "order_cycle_form.$dirty ? '#{t('.cancel')}' : '#{t('.back_to_list')}'", "ng-click": "cancel('#{main_app.admin_order_cycles_path}')" }
%input.red{ type: "button", value: t('.save'), "ng-click": "submit($event, null)", "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid", data: { confirm: "true", 'trigger-action': 'save' } }
- if @order_cycle.simple?
%input.red{ type: "button", value: t('.save_and_back_to_list'), "ng-click": "submit($event, '#{main_app.admin_order_cycles_path}')", "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid", data: { confirm: "true", 'trigger-action': 'saveAndBack' } }
- else
%input.red{ type: "button", value: t('.save_and_next'), "ng-click": "submit($event, '#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')", "ng-disabled": "!order_cycle_form.$dirty || order_cycle_form.$invalid", data: { confirm: "true", 'trigger-action': 'saveAndNext' } }
%input{ type: "button", value: t('.next'), "ng-click": "cancel('#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')", "ng-disabled": "order_cycle_form.$dirty" }
%input{ type: "button", "ng-value": "order_cycle_form.$dirty ? '#{t('.cancel')}' : '#{t('.back_to_list')}'", "ng-click": "cancel('#{main_app.admin_order_cycles_path}')" }
- if @order_cycle.simple?
= render 'simple_form', f: f
- else
= render 'form', f: f
- if has_scheduled_order
= render ModalComponent.new(id: "linked-schedule-warning-modal", close_button: false) do
.content
.modal-body
%h6
= t('admin.order_cycles.edit.linked_schedule_warning_modal.title')
%div{ style: 'font-size: 1rem;' }
= t('admin.order_cycles.edit.linked_schedule_warning_modal.content')
%p.modal-actions.justify-end
%button.button.secondary#modal-confirm{ type: "button", 'data-action': 'click->modal#close', style: 'display: none;', data: { 'order-cycle-target': 'modalConfirm', request: 'save' }, "ng-click": "submit($event, null)" }
= t('admin.order_cycles.edit.linked_schedule_warning_modal.proceed')
%button.button.secondary#modal-confirm{ type: "button", 'data-action': 'click->modal#close', style: 'display: none;', data: { 'order-cycle-target': 'modalConfirm', request: 'saveAndNext' }, "ng-click": "submit($event, '#{main_app.admin_order_cycle_incoming_path(@order_cycle)}')" }
= t('admin.order_cycles.edit.linked_schedule_warning_modal.proceed')
%button.button.secondary#modal-confirm{ type: "button", 'data-action': 'click->modal#close', style: 'display: none;', data: { 'order-cycle-target': 'modalConfirm', request: 'saveAndBack' }, "ng-click": "submit($event, '#{main_app.admin_order_cycles_path}')" }
= t('admin.order_cycles.edit.linked_schedule_warning_modal.proceed')
%button.button.primary{ type: "button", 'data-action': 'click->modal#close' }
= t('admin.order_cycles.edit.linked_schedule_warning_modal.cancel')
- if @order_cycle.orders.exists?
%div.warning-modal{ data: { controller: 'modal modal-link', 'modal-link-target-value': "linked-order-warning-modal" } }
%button.modal-target-trigger{ type: 'button', data: { 'action': 'modal-link#open' }, style: 'display: none;' }
= render ModalComponent.new(id: "linked-order-warning-modal", close_button: false) do
.content.flex-column.gap-2
= render 'date_time_warning_modal_content'

View File

@@ -1,52 +0,0 @@
import { Controller } from "stimulus";
export default class extends Controller {
static targets = ['input', 'modalConfirm'];
static values = { initVals: { type: Object, default: {} }, hasSchedule: { type: Boolean, default: false } };
connect() {
if(!this.hasScheduleValue) return;
// Attach update callback method
window.adminOrderCycleUpdateCallback = this.updateCallback.bind(this);
}
toggleSaveBtns() {
if(!this.hasScheduleValue) return;
// Check that datetime input value has a change
const dirty = this.inputTargets.some(ele =>
new Date(this.initValsValue[`${ele.name}`]).getTime() !== new Date(ele.value).getTime());
// Toggle save bar action button
if (dirty) {
this.element.querySelector('#form-actions').style.display = 'none';
this.element.querySelector('#modal-actions').style.display = 'unset';
} else {
this.element.querySelector('#form-actions').style.display = 'unset';
this.element.querySelector('#modal-actions').style.display = 'none';
}
}
updateModalConfirmButton(e) {
if(!this.hasScheduleValue) return;
// Display modal confirm button coresponding to save bar button clicked
this.modalConfirmTargets.forEach(ele => {
if (e.target.getAttribute('data-target') === ele.getAttribute('data-request')) {
ele.style.display = 'unset';
} else {
ele.style.display = 'none';
}
});
}
updateCallback(data) {
// Reset order values and update save bar buttons
this.initValsValue = { 'order_cycle[orders_open_at]': data.orders_open_at, 'order_cycle[orders_close_at]': data.orders_close_at };
this.toggleSaveBtns();
}
disconnect() {
// remove attached update callback method
delete window.adminOrderCycleUpdateCallback;
}
}

View File

@@ -63,24 +63,6 @@ form.order_cycle {
}
}
#linked-schedule-warning-modal {
.reveal-modal {
width: 28rem;
.content {
display: flex;
flex-direction: column;
gap: 2rem;
.modal-body {
display: flex;
flex-direction: column;
gap: 1rem;
}
.modal-actions {
gap: 1rem;
}
}
}
}
#linked-order-warning-modal .reveal-modal{
width: 28rem;
}

View File

@@ -10,29 +10,19 @@ RSpec.describe '
include AuthenticationHelper
include WebHelper
let(:oc0) {
create(:simple_order_cycle, name: 'oc0',
orders_open_at: nil, orders_close_at: nil)
}
let(:oc1) { create(:order_cycle, name: 'oc1') }
context 'when cycle has attached schedule(s)' do
it "properly toggles order cycle save bar buttons to show warning modal" do
create(:schedule, name: 'Schedule1', order_cycles: [oc0])
context 'when cycle has attached order(s)' do
let(:order) { create(:order_without_full_payment) }
it "show warning modal when datetime field values change" do
# When I go to the admin order cycle edit page
login_as_admin
visit edit_admin_order_cycle_path(oc0)
expect(page).to have_selector("#linked-schedule-warning-modal")
expect(page).not_to have_selector("#modal-actions")
expect(page).to have_selector("#form-actions")
visit edit_admin_order_cycle_path(order.order_cycle)
# change non-date range field
fill_in 'order_cycle_name', with: "OC0 name updated"
fill_in 'order_cycle_name', with: "Order cycle name updated"
expect(page).to have_content('You have unsaved changes')
click_button('Save')
expect(page).not_to have_selector('#linked-schedule-warning-modal .reveal-modal.in')
expect(page).not_to have_content "Orders are linked to this order cycle"
expect(page).to have_content('Your order cycle has been updated.')
# change date range field value
@@ -40,33 +30,32 @@ RSpec.describe '
find('#order_cycle_orders_close_at').click
select_datetime_from_datepicker Time.zone.at(time)
# Enable savebar save buttons to open warning modal
expect(page.find('#order_cycle_orders_close_at').value).to eq time.strftime('%Y-%m-%d %H:%M')
expect(page).not_to have_selector("#form-actions")
expect(page).to have_selector("#modal-actions")
expect(page).to have_content('You have unsaved changes')
expect(page).not_to have_selector('#linked-schedule-warning-modal .reveal-modal.in')
# click save to open warning modal
click_button('Save')
expect(page).to have_selector('#linked-schedule-warning-modal .reveal-modal.in')
expect(page).to have_content('You have unsaved changes')
expect(page).to have_content "Orders are linked to this order cycle."
# confirm to close modal and update order cycle changed fields
click_button('Proceed anyway')
expect(page).not_to have_selector('#linked-schedule-warning-modal .reveal-modal.in')
expect(page).not_to have_content "Orders are linked to this cycle"
expect(page).to have_content('Your order cycle has been updated.')
expect(page.find('#order_cycle_orders_close_at').value).to eq time.strftime('%Y-%m-%d %H:%M')
end
end
context 'when cycle does not have attached schedule' do
let(:order_cycle) {
create(:simple_order_cycle, name: 'My Order cycle',
orders_open_at: nil, orders_close_at: nil)
}
it "does not render warning modal" do
# When I go to the admin order cycle edit page
login_as_admin
visit edit_admin_order_cycle_path(oc1)
expect(page).not_to have_selector("#linked-schedule-warning-modal")
expect(page).not_to have_selector("#modal-actions")
expect(page).to have_selector("#form-actions")
visit edit_admin_order_cycle_path(order_cycle)
# change non-date range field value
fill_in 'order_cycle_name', with: "OC1 name updated"
@@ -74,17 +63,17 @@ RSpec.describe '
# click save
click_button('Save')
expect(page).not_to have_selector('#linked-schedule-warning-modal .reveal-modal.in')
expect(page.find('#order_cycle_name').value).to eq 'OC1 name updated'
expect(page).to have_content('Your order cycle has been updated.')
# change date range field value
# Now change date range field value
time = DateTime.current
find('#order_cycle_orders_close_at').click
select_datetime_from_datepicker Time.zone.at(time)
expect(page).to have_content('You have unsaved changes')
click_button('Save')
expect(page).not_to have_selector("#modal-actions")
expect(page.find('#order_cycle_orders_close_at').value).to eq time.strftime('%Y-%m-%d %H:%M')
expect(page).to have_content('Your order cycle has been updated.')
end
end

View File

@@ -125,9 +125,8 @@ RSpec.describe 'Subscriptions' do
select_datetime_from_datepicker Time.zone.at(1.month.from_now)
find("body").send_keys(:escape)
# Click save and comfirm in warning modal (because date time range value was changed)
click_button('Save')
click_button('Proceed anyway')
click_button 'Save'
visit edit_admin_subscription_path(subscription)
click_button 'edit-products'