workarea-variant_list 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +20 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
- data/.github/ISSUE_TEMPLATE/documentation-request.md +17 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.gitignore +22 -0
- data/CHANGELOG.md +49 -0
- data/CODE_OF_CONDUCT.md +3 -0
- data/CONTRIBUTING.md +3 -0
- data/Gemfile +10 -0
- data/LICENSE +52 -0
- data/README.md +32 -0
- data/Rakefile +60 -0
- data/app/assets/javascripts/workarea/storefront/variant_list/modules/sortable_tables.js +107 -0
- data/app/assets/stylesheets/workarea/storefront/variant_list/components/_product_details.scss +40 -0
- data/app/controllers/workarea/storefront/cart_bulk_items_controller.rb +30 -0
- data/app/view_models/workarea/storefront/product_templates/variant_list_view_model.rb +20 -0
- data/app/view_models/workarea/storefront/variant_view_model.rb +27 -0
- data/app/views/workarea/storefront/cart_bulk_items/create.html.haml +71 -0
- data/app/views/workarea/storefront/products/templates/_variant_list.html.haml +82 -0
- data/bin/rails +20 -0
- data/config/initializers/appends.rb +9 -0
- data/config/initializers/config.rb +4 -0
- data/config/locales/en.yml +11 -0
- data/config/routes.rb +7 -0
- data/lib/workarea/variant_list.rb +11 -0
- data/lib/workarea/variant_list/engine.rb +8 -0
- data/lib/workarea/variant_list/version.rb +5 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/config/manifest.js +4 -0
- data/test/dummy/app/assets/images/.keep +0 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/controllers/concerns/.keep +0 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/jobs/application_job.rb +2 -0
- data/test/dummy/app/mailers/application_mailer.rb +4 -0
- data/test/dummy/app/models/concerns/.keep +0 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +38 -0
- data/test/dummy/bin/update +29 -0
- data/test/dummy/bin/yarn +11 -0
- data/test/dummy/config.ru +5 -0
- data/test/dummy/config/application.rb +28 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/cable.yml +10 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +54 -0
- data/test/dummy/config/environments/production.rb +91 -0
- data/test/dummy/config/environments/test.rb +44 -0
- data/test/dummy/config/initializers/application_controller_renderer.rb +6 -0
- data/test/dummy/config/initializers/assets.rb +14 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/workarea.rb +5 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +33 -0
- data/test/dummy/config/puma.rb +56 -0
- data/test/dummy/config/routes.rb +5 -0
- data/test/dummy/config/secrets.yml +32 -0
- data/test/dummy/config/spring.rb +6 -0
- data/test/dummy/db/seeds.rb +2 -0
- data/test/dummy/lib/assets/.keep +0 -0
- data/test/dummy/log/.keep +0 -0
- data/test/integration/workarea/storefront/cart_bulk_items_integration_test.rb +73 -0
- data/test/system/workarea/storefront/product_variant_list_system_test.rb +92 -0
- data/test/teaspoon_env.rb +6 -0
- data/test/test_helper.rb +10 -0
- data/workarea-variant_list.gemspec +21 -0
- 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 })
|
data/bin/rails
ADDED
@@ -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,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
|
data/config/routes.rb
ADDED
data/test/dummy/Rakefile
ADDED
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
|
+
*/
|
File without changes
|
File without changes
|