workarea-variant_list 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +20 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  4. data/.github/ISSUE_TEMPLATE/documentation-request.md +17 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  6. data/.gitignore +22 -0
  7. data/CHANGELOG.md +49 -0
  8. data/CODE_OF_CONDUCT.md +3 -0
  9. data/CONTRIBUTING.md +3 -0
  10. data/Gemfile +10 -0
  11. data/LICENSE +52 -0
  12. data/README.md +32 -0
  13. data/Rakefile +60 -0
  14. data/app/assets/javascripts/workarea/storefront/variant_list/modules/sortable_tables.js +107 -0
  15. data/app/assets/stylesheets/workarea/storefront/variant_list/components/_product_details.scss +40 -0
  16. data/app/controllers/workarea/storefront/cart_bulk_items_controller.rb +30 -0
  17. data/app/view_models/workarea/storefront/product_templates/variant_list_view_model.rb +20 -0
  18. data/app/view_models/workarea/storefront/variant_view_model.rb +27 -0
  19. data/app/views/workarea/storefront/cart_bulk_items/create.html.haml +71 -0
  20. data/app/views/workarea/storefront/products/templates/_variant_list.html.haml +82 -0
  21. data/bin/rails +20 -0
  22. data/config/initializers/appends.rb +9 -0
  23. data/config/initializers/config.rb +4 -0
  24. data/config/locales/en.yml +11 -0
  25. data/config/routes.rb +7 -0
  26. data/lib/workarea/variant_list.rb +11 -0
  27. data/lib/workarea/variant_list/engine.rb +8 -0
  28. data/lib/workarea/variant_list/version.rb +5 -0
  29. data/test/dummy/Rakefile +6 -0
  30. data/test/dummy/app/assets/config/manifest.js +4 -0
  31. data/test/dummy/app/assets/images/.keep +0 -0
  32. data/test/dummy/app/assets/javascripts/application.js +13 -0
  33. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  34. data/test/dummy/app/controllers/application_controller.rb +3 -0
  35. data/test/dummy/app/controllers/concerns/.keep +0 -0
  36. data/test/dummy/app/helpers/application_helper.rb +2 -0
  37. data/test/dummy/app/jobs/application_job.rb +2 -0
  38. data/test/dummy/app/mailers/application_mailer.rb +4 -0
  39. data/test/dummy/app/models/concerns/.keep +0 -0
  40. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  41. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  42. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  43. data/test/dummy/bin/bundle +3 -0
  44. data/test/dummy/bin/rails +4 -0
  45. data/test/dummy/bin/rake +4 -0
  46. data/test/dummy/bin/setup +38 -0
  47. data/test/dummy/bin/update +29 -0
  48. data/test/dummy/bin/yarn +11 -0
  49. data/test/dummy/config.ru +5 -0
  50. data/test/dummy/config/application.rb +28 -0
  51. data/test/dummy/config/boot.rb +5 -0
  52. data/test/dummy/config/cable.yml +10 -0
  53. data/test/dummy/config/environment.rb +5 -0
  54. data/test/dummy/config/environments/development.rb +54 -0
  55. data/test/dummy/config/environments/production.rb +91 -0
  56. data/test/dummy/config/environments/test.rb +44 -0
  57. data/test/dummy/config/initializers/application_controller_renderer.rb +6 -0
  58. data/test/dummy/config/initializers/assets.rb +14 -0
  59. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  60. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  61. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  62. data/test/dummy/config/initializers/inflections.rb +16 -0
  63. data/test/dummy/config/initializers/mime_types.rb +4 -0
  64. data/test/dummy/config/initializers/workarea.rb +5 -0
  65. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  66. data/test/dummy/config/locales/en.yml +33 -0
  67. data/test/dummy/config/puma.rb +56 -0
  68. data/test/dummy/config/routes.rb +5 -0
  69. data/test/dummy/config/secrets.yml +32 -0
  70. data/test/dummy/config/spring.rb +6 -0
  71. data/test/dummy/db/seeds.rb +2 -0
  72. data/test/dummy/lib/assets/.keep +0 -0
  73. data/test/dummy/log/.keep +0 -0
  74. data/test/integration/workarea/storefront/cart_bulk_items_integration_test.rb +73 -0
  75. data/test/system/workarea/storefront/product_variant_list_system_test.rb +92 -0
  76. data/test/teaspoon_env.rb +6 -0
  77. data/test/test_helper.rb +10 -0
  78. data/workarea-variant_list.gemspec +21 -0
  79. metadata +142 -0
@@ -0,0 +1,40 @@
1
+ /*------------------------------------*\
2
+ #PRODUCT-DETAILS
3
+ \*------------------------------------*/
4
+
5
+ .product-details--variant-list {}
6
+
7
+ .product-details__variant-table {}
8
+
9
+ .product-details__variant-table-head {}
10
+
11
+ .product-details__variant-table-body {}
12
+
13
+ .product-details__variant-table-row {}
14
+
15
+ .product-details__variant-table-cell {}
16
+
17
+ .product-details__variant-table-cell--image {}
18
+
19
+ .product-details__variant-table-cell--name {
20
+ .product-details__variant-table-body & {
21
+ font-weight: bold;
22
+ }
23
+ }
24
+
25
+ .product-details__variant-table-cell--detail {
26
+ text-align: center;
27
+ }
28
+
29
+ .product-details__variant-table-cell--price {
30
+ text-align: right;
31
+
32
+ .product-details__variant-table-body & {
33
+ font-weight: bold;
34
+ }
35
+ }
36
+
37
+ .product-details__variant-table-cell--quantity {
38
+ text-align: right;
39
+ }
40
+
@@ -0,0 +1,30 @@
1
+ module Workarea
2
+ module Storefront
3
+ class CartBulkItemsController < ApplicationController
4
+ include CheckInventory
5
+
6
+ skip_before_action :verify_authenticity_token
7
+
8
+ def create
9
+ add_to_cart = AddMultipleCartItems.new(
10
+ current_order,
11
+ params[:items].map(&:to_unsafe_h).select { |p| p[:quantity].to_i.positive? }
12
+ )
13
+
14
+ if add_to_cart.perform!
15
+ check_inventory
16
+ Pricing.perform(current_order, current_shippings)
17
+
18
+ @cart = CartViewModel.new(current_order, view_model_options)
19
+ @items = add_to_cart.items.map do |cart_item|
20
+ OrderItemViewModel.wrap(cart_item.item, view_model_options)
21
+ end
22
+ else
23
+ flash[:error] =
24
+ t('workarea.storefront.flash_messages.cart_bulk_items_error')
25
+ redirect_to product_path(add_to_cart.items.first.product)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,20 @@
1
+ module Workarea
2
+ module Storefront
3
+ class ProductTemplates::VariantListViewModel < ProductViewModel
4
+ delegate :all_options, to: :option_set
5
+
6
+ def variant_list
7
+ @variant_list ||= variants.map do |variant|
8
+ VariantViewModel.new(
9
+ variant,
10
+ options.merge(inventory: inventory)
11
+ )
12
+ end
13
+ end
14
+
15
+ def option_set
16
+ @option_set ||= ProductViewModel::OptionSet.new(self, options)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ module Workarea
2
+ module Storefront
3
+ class VariantViewModel < ApplicationViewModel
4
+ delegate :inventory_status, :images, :primary_image, :inventory,
5
+ :inventory_purchasable?, :pricing, to: :product
6
+
7
+ def product
8
+ @product ||=
9
+ ProductViewModel.wrap(model.product, options.merge(sku: sku))
10
+ end
11
+
12
+ def details
13
+ @details ||= Hash[
14
+ model.details.map { |k, v| [k.to_s.optionize, [v].flatten.join(', ')] }
15
+ ]
16
+ end
17
+
18
+ def price
19
+ @price ||= pricing.for_sku(sku)
20
+ end
21
+
22
+ def purchasable?
23
+ product.model.purchasable? && inventory_purchasable? && price.persisted?
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,71 @@
1
+ .view
2
+ - if @items.blank?
3
+ = render_message 'error', data: { message_show_dismiss: 'false' } do
4
+ = t('workarea.storefront.flash_messages.cart_item_error')
5
+
6
+ - else
7
+ = render_message 'success', data: { message_show_dismiss: 'false' } do
8
+ = t('workarea.storefront.flash_messages.cart_bulk_items_added')
9
+
10
+ .product-list
11
+ - @items.each do |item|
12
+ .product-list__item{ data: { cart_count: current_order.quantity, analytics: add_to_cart_confirmation_analytics_data(item).to_json } }
13
+ .product-list__item-cell
14
+ .product-list__summary
15
+ %p.product-list__media= link_to image_tag(product_image_url(item.image, :small_thumb), alt: item.product_name, class: 'product-list__media-image'), product_url(item.product, sku: item.sku), class: 'product-list__media-link'
16
+ .product-list__info
17
+ %p.product-list__name= link_to item.product_name, product_path(item.product, sku: item.sku)
18
+ %p.product-list__id= item.sku_name
19
+ - if item.has_options?
20
+ .product-list__option-group
21
+ - item.details.each do |name, value|
22
+ %p.product-list__option #{name.titleize}: #{value}
23
+ - item.customizations.each do |name, value|
24
+ %p.product-list__customization #{name.titleize}: #{value}
25
+ = append_partials('storefront.cart_item_details', item: item, index: 0)
26
+ .product-list__item-cell
27
+ %table.table
28
+ %thead
29
+ %tr
30
+ %th.table__prices= t('workarea.storefront.orders.price')
31
+ %th.table__quantity= t('workarea.storefront.orders.quantity')
32
+ %th.table__prices= t('workarea.storefront.orders.total')
33
+ %tbody
34
+ %tr
35
+ %td.table__prices
36
+ = render 'workarea/storefront/carts/pricing', item: item, css_block: 'table'
37
+ %td.table__quantity
38
+ = form_tag cart_item_path(item), method: :patch, class: 'inline-form', data: { analytics: update_cart_item_analytics_data(item).to_json } do
39
+ .inline-form__cell
40
+ .value= number_field_tag :quantity, item.quantity, min: 1, required: true, class: 'text-box text-box--x-small', data: { form_submitting_control: '' }, title: t('workarea.storefront.orders.quantity'), id: dom_id(item, 'cart_item')
41
+ %p.inline-form__cell.hidden-if-js-enabled= button_tag t('workarea.storefront.carts.update'), value: 'change_quantity', class: 'button'
42
+ %td.table__prices
43
+ - item.total_adjustments.each do |adjustment|
44
+ %p.table__price
45
+ - if item.total_adjustments.many?
46
+ %span.table__price-label= adjustment.description.titleize
47
+
48
+ - if adjustment.discount?
49
+ %strong.table__price-discount= number_to_currency(adjustment.amount)
50
+ - else
51
+ %span= number_to_currency(adjustment.amount)
52
+
53
+ - if item.total_adjustments.many?
54
+ %p.table__price
55
+ %span.table__price-label= t('workarea.storefront.orders.item_total')
56
+ %span= number_to_currency(item.total_price)
57
+
58
+ .grid.grid--auto.grid--center
59
+ .grid__cell= link_to t('workarea.storefront.carts.continue_shopping'), root_path, class: 'button button--large', data: { dialog_close_button: true }
60
+ .grid__cell= link_to t('workarea.storefront.carts.view_cart'), cart_path, class: 'button button--large'
61
+ .grid__cell= link_to t('workarea.storefront.carts.checkout'), checkout_path, class: 'button button--large'
62
+
63
+ - if @cart.recommendations.any?
64
+ .hidden.hidden--for-small-only
65
+ %h2= t('workarea.storefront.recommendations.you_may_also_like')
66
+
67
+ .grid
68
+ - @cart.recommendations.each do |product|
69
+ .grid__cell.grid__cell--50.grid__cell--33-at-medium.grid__cell--16-at-wide
70
+ .product-summary.product-summary--small{ itemscope: true, itemtype: 'http://schema.org/Product' }
71
+ = render 'workarea/storefront/products/summary', product: product
@@ -0,0 +1,82 @@
1
+ .grid.grid--rev
2
+ .grid__cell.grid__cell--60-at-medium
3
+
4
+ .product-details__name
5
+ %h1{ itemprop: 'name' }= product.name
6
+
7
+ %p.product-details__id
8
+ %span{ itemprop: 'productID' }= product.id
9
+
10
+ .product-prices.product-prices--details{ itemprop: 'offers', itemscope: true, itemtype: 'http://schema.org/Offer' }
11
+ = render 'workarea/storefront/products/pricing', product: product
12
+
13
+ - if product.description.present?
14
+ .product-details__description
15
+ %p= truncated_product_description(product, t('workarea.storefront.products.read_more'))
16
+
17
+ = form_tag cart_bulk_items_path, method: 'post', class: 'product-details__add-to-cart-form', data: { dialog_form: { dialogOptions: { closeAll: true, initModules: true } }, analytics: add_to_cart_analytics_data(product).to_json } do
18
+ = hidden_field_tag :via, params[:via], id: dom_id(product, 'via')
19
+
20
+ %table.product-details__variant-table{ data: { sortable_table: '' } }
21
+ %thead.product-details__variant-table-head
22
+ %tr.product-details__variant-table-row
23
+ %th.product-details__variant-table-cell.product-details__variant-table-cell--image
24
+ %th.product-details__variant-table-cell.product-details__variant-table-cell--sku{ data: { sortable_table_header: '' } }= t('workarea.storefront.variant_list.products.sku')
25
+ - product.all_options.each do |option|
26
+ %th.product-details__variant-table-cell.product-details__variant-table-cell--detail{ data: { sortable_table_header: '' } }= option.titleize
27
+ %th.product-details__variant-table-cell.product-details__variant-table-cell--price{ data: { sortable_table_header: '' } }= t('workarea.storefront.variant_list.products.price')
28
+ %th.product-details__variant-table-cell.product-details__variant-table-cell--quantity= t('workarea.storefront.products.quantity')
29
+ %tbody.product-details__variant-table-body
30
+ - product.variant_list.each_with_index do |variant, index|
31
+ %tr.product-details__variant-table-row
32
+ %td.product-details__variant-table-cell.product-details__variant-table-cell--image
33
+ = image_tag product_image_url(variant.primary_image, :small_thumb), alt: t('workarea.storefront.products.image_alt_attribute', name: variant.name), itemprop: 'image'
34
+ %td.product-details__variant-table-cell.product-details__variant-table-cell--name
35
+ = variant.name
36
+
37
+ - product.all_options.each do |option|
38
+ %td.product-details__variant-table-cell.product-details__variant-table-cell--detail{ class: "product-details__variant-table-cell--detail-#{option}" }
39
+ - if variant.details[option].present?
40
+ = variant.details[option]
41
+ - else
42
+ \-
43
+
44
+ %td.product-details__variant-table-cell.product-details__variant-table-cell--price= number_to_currency(variant.price.sell)
45
+ %td.product-details__variant-table-cell.product-details__variant-table-cell--quantity
46
+ - if variant.purchasable?
47
+ = hidden_field_tag 'items[][sku]', variant.sku, id: nil
48
+
49
+ .property
50
+ .value
51
+ = number_field_tag "items[][quantity]", params[:items].try(:[], index).try(:[], :quantity) || 0, class: 'text-box text-box--x-small', min: 0, id: "quantity_#{dom_id(variant)}"
52
+ .value__note= variant.inventory_status
53
+ - else
54
+ %p= t('workarea.storefront.variant_list.products.variant_unavailable')
55
+
56
+ = append_partials('storefront.add_to_cart_form', product: product)
57
+
58
+ - if product.purchasable?
59
+ %p.product-details__add-to-cart-action= button_tag t('workarea.storefront.products.add_to_cart'), value: 'add_to_cart', class: 'button button--large'
60
+
61
+ - else
62
+ = hidden_field_tag :quantity, params[:quantity] || 1, id: "quantity#{dom_id(product)}"
63
+ %p.product-details__unavailable= t('workarea.storefront.products.unavailable')
64
+
65
+ = append_partials('storefront.product_details', product: product)
66
+
67
+ %p.product-details__full-details=link_to t('workarea.storefront.products.view_full_details'), product_path(product, color: params[:color]), class: 'text-button', itemprop: 'url'
68
+
69
+ .grid__cell.grid__cell--40-at-medium
70
+
71
+ .product-details__primary-image
72
+ = link_to(product_image_url(product.primary_image, :zoom), target: '_blank', rel: 'noopener', class: 'product-details__primary-image-link', data: { dialog_button: '' }) do
73
+ = image_tag product_image_url(product.primary_image, :detail), alt: t('workarea.storefront.products.image_alt_attribute', name: product.name), itemprop: 'image', class: 'product-details__primary-image-link-image'
74
+
75
+ - if product.images.length > 1
76
+ .product-details__alt-images
77
+ .grid.grid--auto
78
+ - product.images.each_with_index do |image, index|
79
+ .grid__cell
80
+ .product-details__alt-image
81
+ - button_class = index == 0 ? 'product-details__alt-image-link product-details__alt-image-link--selected' : 'product-details__alt-image-link'
82
+ = link_to(image_tag(product_image_url(image, :small_thumb), alt: t('workarea.storefront.products.zoom')), product_image_url(image, :zoom), class: button_class, target: '_blank', rel: 'noopener', data: { alternate_image_button: { src: product_image_url(image, :detail) }.to_json })
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path('../..', __FILE__)
6
+ ENGINE_PATH = File.expand_path('../../lib/workarea/variant_list/engine', __FILE__)
7
+ APP_PATH = File.expand_path('../../test/dummy/config/application', __FILE__)
8
+
9
+ # Set up gems listed in the Gemfile.
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
11
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
12
+
13
+ require "action_controller/railtie"
14
+ require "action_view/railtie"
15
+ require "action_mailer/railtie"
16
+ require "rails/test_unit/railtie"
17
+ require "sprockets/railtie"
18
+ require 'teaspoon-mocha'
19
+
20
+ require 'rails/engine/commands'
@@ -0,0 +1,9 @@
1
+ Workarea.append_stylesheets(
2
+ 'storefront.components',
3
+ 'workarea/storefront/variant_list/components/product_details'
4
+ )
5
+
6
+ Workarea.append_javascripts(
7
+ 'storefront.modules',
8
+ 'workarea/storefront/variant_list/modules/sortable_tables'
9
+ )
@@ -0,0 +1,4 @@
1
+ Workarea.configure do |config|
2
+ config.product_templates << :variant_list
3
+ config.product_quickview_templates.try(:push, :variant_list)
4
+ end
@@ -0,0 +1,11 @@
1
+ en:
2
+ workarea:
3
+ storefront:
4
+ flash_messages:
5
+ cart_bulk_items_added: These items have been added to your cart.
6
+ cart_bulk_items_error: Some items could not be added to your cart.
7
+ variant_list:
8
+ products:
9
+ variant_unavailable: This SKU is currently unavailable for purchase.
10
+ sku: SKU
11
+ price: Price
@@ -0,0 +1,7 @@
1
+ Workarea::Storefront::Engine.routes.draw do
2
+ scope '(:locale)', constraints: Workarea::I18n.routes_constraint do
3
+ resource :cart, only: [] do
4
+ resource :bulk_items, only: :create, controller: :cart_bulk_items
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ require 'workarea'
2
+ require 'workarea/storefront'
3
+ require 'workarea/admin'
4
+
5
+ require 'workarea/variant_list/engine'
6
+ require 'workarea/variant_list/version'
7
+
8
+ module Workarea
9
+ module VariantList
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ module Workarea
2
+ module VariantList
3
+ class Engine < ::Rails::Engine
4
+ include Workarea::Plugin
5
+ isolate_namespace Workarea::VariantList
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ module Workarea
2
+ module VariantList
3
+ VERSION = '1.0.2'.freeze
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require_relative 'config/application'
5
+
6
+ Rails.application.load_tasks
@@ -0,0 +1,4 @@
1
+
2
+ //= link_tree ../images
3
+ //= link_directory ../javascripts .js
4
+ //= link_directory ../stylesheets .css
File without changes
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file. JavaScript code in this file should be added after the last require_* statement.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,3 @@
1
+ class ApplicationController < ActionController::Base
2
+ protect_from_forgery with: :exception
3
+ end
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
@@ -0,0 +1,2 @@
1
+ class ApplicationJob < ActiveJob::Base
2
+ end
@@ -0,0 +1,4 @@
1
+ class ApplicationMailer < ActionMailer::Base
2
+ default from: 'from@example.com'
3
+ layout 'mailer'
4
+ end
File without changes
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Dummy</title>
5
+ <%= csrf_meta_tags %>
6
+
7
+ <%= stylesheet_link_tag 'application', media: 'all' %>
8
+ <%= javascript_include_tag 'application' %>
9
+ </head>
10
+
11
+ <body>
12
+ <%= yield %>
13
+ </body>
14
+ </html>