diff --git a/app/assets/javascripts/darkswarm/controllers/cart_form_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/cart_form_controller.js.coffee new file mode 100644 index 0000000000..73fe91793f --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/cart_form_controller.js.coffee @@ -0,0 +1,2 @@ +Darkswarm.controller "CartFormCtrl", ($scope) -> + diff --git a/app/assets/javascripts/darkswarm/directives/disable_dynamically.js.coffee b/app/assets/javascripts/darkswarm/directives/disable_dynamically.js.coffee new file mode 100644 index 0000000000..7cff41f3f4 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/disable_dynamically.js.coffee @@ -0,0 +1,12 @@ +# Allows disabling of link buttons via disabled attribute. +# This is normally ignored, ie the link appears disabled but is still clickable. + +Darkswarm.directive "disableDynamically", -> + restrict: 'A' + + link: (scope, element, attrs) -> + element.on 'click', (e) -> + if attrs.disabled + e.preventDefault() + return + diff --git a/app/assets/javascripts/darkswarm/directives/on_hand.js.coffee b/app/assets/javascripts/darkswarm/directives/on_hand.js.coffee index 8ad5a8748a..66c20d86fb 100644 --- a/app/assets/javascripts/darkswarm/directives/on_hand.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/on_hand.js.coffee @@ -1,4 +1,4 @@ -Darkswarm.directive "ofnOnHand", -> +Darkswarm.directive "ofnOnHand", (StockQuantity) -> restrict: 'A' require: "ngModel" scope: true @@ -24,6 +24,4 @@ Darkswarm.directive "ofnOnHand", -> viewValue scope.available_quantity = -> - on_hand = parseInt(attr.ofnOnHand) - finalized_quantity = parseInt(attr.finalizedquantity) || 0 # finalizedquantity is optional - on_hand + finalized_quantity + StockQuantity.available_quantity(attr.ofnOnHand, attr.finalizedquantity) diff --git a/app/assets/javascripts/darkswarm/directives/validate_stock_quantity.js.coffee b/app/assets/javascripts/darkswarm/directives/validate_stock_quantity.js.coffee new file mode 100644 index 0000000000..3ceee62a4d --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/validate_stock_quantity.js.coffee @@ -0,0 +1,16 @@ +Darkswarm.directive "validateStockQuantity", (StockQuantity) -> + restrict: 'A' + require: "ngModel" + scope: true + + link: (scope, element, attr, ngModel) -> + ngModel.$parsers.push (selectedQuantity) -> + valid_number = parseInt(selectedQuantity) != NaN + valid_quantity = parseInt(selectedQuantity) <= scope.available_quantity() + + ngModel.$setValidity('stock', (valid_number && valid_quantity) ); + + selectedQuantity + + scope.available_quantity = -> + StockQuantity.available_quantity(attr.ofnOnHand, attr.finalizedquantity) diff --git a/app/assets/javascripts/darkswarm/services/stock_quantity.js.coffee b/app/assets/javascripts/darkswarm/services/stock_quantity.js.coffee new file mode 100644 index 0000000000..efc6b67eb4 --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/stock_quantity.js.coffee @@ -0,0 +1,7 @@ +Darkswarm.factory "StockQuantity", -> + new class StockQuantity + available_quantity: (on_hand, finalized_quantity) -> + on_hand = parseInt(on_hand) + finalized_quantity = parseInt(finalized_quantity) || 0 # finalized_quantity is optional + + on_hand + finalized_quantity diff --git a/app/assets/stylesheets/darkswarm/shopping-cart.css.scss b/app/assets/stylesheets/darkswarm/shopping-cart.css.scss index bd4a5019c8..d00f3915e7 100644 --- a/app/assets/stylesheets/darkswarm/shopping-cart.css.scss +++ b/app/assets/stylesheets/darkswarm/shopping-cart.css.scss @@ -72,6 +72,12 @@ } // Shopping cart +#update-cart { + #errorExplanation { + display: none; + } +} + #cart-detail { .cart-item-delete, .bought-item-delete { a { @@ -102,6 +108,12 @@ float: right; } } + + input { + &.ng-invalid-stock, &.ng-invalid-number { + border: 1px solid $clr-brick; + } + } } .item-thumb-image { diff --git a/app/views/spree/orders/_line_item.html.haml b/app/views/spree/orders/_line_item.html.haml index cfd4d4a500..918a520fdf 100644 --- a/app/views/spree/orders/_line_item.html.haml +++ b/app/views/spree/orders/_line_item.html.haml @@ -23,7 +23,11 @@ = line_item.single_display_amount_with_adjustments.to_html %td.text-center.cart-item-quantity{"data-hook" => "cart_item_quantity"} - finalized_quantity = @order.completed? ? line_item.quantity : 0 - = item_form.number_field :quantity, :min => 0, "ofn-on-hand" => "#{variant.on_demand && 9999 || variant.on_hand}", "finalizedquantity" => finalized_quantity, "ng-model" => "line_item_#{line_item.id}", :class => "line_item_quantity", :size => 5 + = item_form.number_field :quantity, + :min => 0, "ofn-on-hand" => "#{variant.on_demand && 9999 || variant.on_hand}", + "finalizedquantity" => finalized_quantity, :class => "line_item_quantity", :size => 5, + "ng-model" => "line_item_#{line_item.id}", + "validate-stock-quantity" => true %td.cart-item-total.text-right{"data-hook" => "cart_item_total"} = line_item.display_amount_with_adjustments.to_html unless line_item.quantity.nil? diff --git a/app/views/spree/orders/edit.html.haml b/app/views/spree/orders/edit.html.haml index f654cfebb1..d9cdb977b3 100644 --- a/app/views/spree/orders/edit.html.haml +++ b/app/views/spree/orders/edit.html.haml @@ -33,7 +33,8 @@ - else %div{"data-hook" => "outside_cart_form"} - = form_for @order, :url => main_app.update_cart_path, :html => {:id => 'update-cart'} do |order_form| + = form_for @order, :url => main_app.update_cart_path, + :html => {id: 'update-cart', name: "form", "ng-controller"=> 'CartFormCtrl'} do |order_form| %div{"data-hook" => "inside_cart_form"} %div{"data-hook" => "cart_items"} .row diff --git a/app/views/spree/orders/form/_cart_actions_row.html.haml b/app/views/spree/orders/form/_cart_actions_row.html.haml index ec3ce122ea..a7005a5eb6 100644 --- a/app/views/spree/orders/form/_cart_actions_row.html.haml +++ b/app/views/spree/orders/form/_cart_actions_row.html.haml @@ -1,7 +1,7 @@ %tr %td{colspan:"2"} %td - = button_tag :class => 'secondary radius expand small', :id => 'update-button' do + %button#update-button.secondary.radius.expand.small{"ng-class" => "{ alert: form.$dirty && form.$valid }"} %i.ofn-i_023-refresh = t(:update) %td diff --git a/app/views/spree/orders/form/_cart_links.html.haml b/app/views/spree/orders/form/_cart_links.html.haml index 8da012cd78..798a428f7c 100644 --- a/app/views/spree/orders/form/_cart_links.html.haml +++ b/app/views/spree/orders/form/_cart_links.html.haml @@ -1,5 +1,5 @@ .row.links{'data-hook' => "cart_buttons"} - %a.button.large.secondary{href: current_shop_products_path} + %a.continue-shopping.button.large.secondary{href: current_shop_products_path, "ng-disabled" => "#{@insufficient_stock_lines.any?}", "disable-dynamically" => true} = t :orders_edit_continue - %a#checkout-link.button.large.primary.right{href: main_app.checkout_path} + %a#checkout-link.button.large.primary.right{href: main_app.checkout_path, "ng-disabled" => "#{@insufficient_stock_lines.any?}", "disable-dynamically" => true} = t :orders_edit_checkout diff --git a/config/locales/en.yml b/config/locales/en.yml index 601a916c02..1aee3bfa2c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3406,7 +3406,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using format: ! '%Y-%m-%d' js_format: 'yy-mm-dd' orders: - error_flash_for_unavailable_items: "An item in your cart has become unavailable." + error_flash_for_unavailable_items: "An item in your cart has become unavailable. Please update the selected quantities." edit: login_to_view_order: "Please log in to view your order." bought: diff --git a/spec/controllers/spree/orders_controller_spec.rb b/spec/controllers/spree/orders_controller_spec.rb index c018a98873..783c868366 100644 --- a/spec/controllers/spree/orders_controller_spec.rb +++ b/spec/controllers/spree/orders_controller_spec.rb @@ -161,7 +161,7 @@ describe Spree::OrdersController, type: :controller do it "displays a flash message when we view the cart" do spree_get :edit expect(response.status).to eq 200 - expect(flash[:error]).to eq("An item in your cart has become unavailable.") + expect(flash[:error]).to eq I18n.t('spree.orders.error_flash_for_unavailable_items') end end @@ -173,7 +173,7 @@ describe Spree::OrdersController, type: :controller do it "displays a flash message when we view the cart" do spree_get :edit expect(response.status).to eq 200 - expect(flash[:error]).to eq("An item in your cart has become unavailable.") + expect(flash[:error]).to eq I18n.t('spree.orders.error_flash_for_unavailable_items') end end end diff --git a/spec/features/consumer/shopping/cart_spec.rb b/spec/features/consumer/shopping/cart_spec.rb index 95a51ce1ec..9068504be6 100644 --- a/spec/features/consumer/shopping/cart_spec.rb +++ b/spec/features/consumer/shopping/cart_spec.rb @@ -213,6 +213,40 @@ feature "full-page cart", js: true do expect(page).to have_content "Insufficient stock available, only 2 remaining" expect(page).to have_field "order_line_items_attributes_0_quantity", with: '1' end + + describe "full UX for correcting selected quantities with insufficient stock" do + before do + add_product_to_cart order, product_with_tax, quantity: 5 + variant.update_attributes! on_hand: 4, on_demand: false + end + + it "gives clear user feedback during the correcting process" do + visit main_app.cart_path + + # shows a relevant Flash message + expect(page).to have_selector ".alert-box", text: I18n.t('spree.orders.error_flash_for_unavailable_items') + + # "Continue Shopping" and "Checkout" buttons are disabled + expect(page).to have_selector "a.continue-shopping[disabled=disabled]" + expect(page).to have_selector "a#checkout-link[disabled=disabled]" + + # Quantity field clearly marked as invalid and "Update" button is not highlighted + expect(page).to have_selector "#order_line_items_attributes_0_quantity.ng-invalid-stock" + expect(page).to_not have_selector "#update-button.alert" + + fill_in "order_line_items_attributes_0_quantity", with: 4 + + # Quantity field not marked as invalid and "Update" button is highlighted after correction + expect(page).to_not have_selector "#order_line_items_attributes_0_quantity.ng-invalid-stock" + expect(page).to have_selector "#update-button.alert" + + click_button I18n.t("update") + + # "Continue Shopping" and "Checkout" buttons are not disabled after cart is updated + expect(page).to_not have_selector "a.continue-shopping[disabled=disabled]" + expect(page).to_not have_selector "a#checkout-link[disabled=disabled]" + end + end end end