spree_legacy_product_properties 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.md +20 -0
- data/README.md +59 -0
- data/Rakefile +23 -0
- data/app/controllers/spree/admin/products_controller_decorator.rb +26 -0
- data/app/controllers/spree/admin/properties_controller.rb +26 -0
- data/app/finders/spree/product_properties/find_available.rb +20 -0
- data/app/finders/spree/products/find_decorator.rb +47 -0
- data/app/helpers/spree/admin/properties_helper.rb +9 -0
- data/app/models/concerns/spree/filter_param.rb +21 -0
- data/app/models/spree/permission_sets/property_management.rb +10 -0
- data/app/models/spree/product_decorator.rb +107 -0
- data/app/models/spree/product_property.rb +51 -0
- data/app/models/spree/products/duplicator_decorator.rb +29 -0
- data/app/models/spree/property.rb +86 -0
- data/app/models/spree/property_prototype.rb +9 -0
- data/app/models/spree/prototype_decorator.rb +10 -0
- data/app/presenters/spree/filters/properties_presenter.rb +23 -0
- data/app/presenters/spree/filters/property_presenter.rb +42 -0
- data/app/services/spree/products/prepare_nested_attributes_decorator.rb +22 -0
- data/app/views/spree/admin/products/form/_properties.html.erb +48 -0
- data/app/views/spree/admin/properties/_filters.html.erb +5 -0
- data/app/views/spree/admin/properties/_form.html.erb +8 -0
- data/app/views/spree/admin/properties/_table_header.html.erb +9 -0
- data/app/views/spree/admin/properties/_table_row.html.erb +25 -0
- data/app/views/spree/admin/properties/edit.html.erb +2 -0
- data/app/views/spree/admin/properties/index.html.erb +22 -0
- data/app/views/spree/admin/properties/new.html.erb +1 -0
- data/app/views/spree/admin/properties/update.turbo_stream.erb +1 -0
- data/config/initializers/spree.rb +62 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20221229000000_create_spree_properties_and_product_properties.rb +113 -0
- data/lib/generators/spree_legacy_product_properties/install/install_generator.rb +20 -0
- data/lib/spree_legacy_product_properties/configuration.rb +13 -0
- data/lib/spree_legacy_product_properties/engine.rb +23 -0
- data/lib/spree_legacy_product_properties/factories.rb +34 -0
- data/lib/spree_legacy_product_properties/version.rb +7 -0
- data/lib/spree_legacy_product_properties.rb +12 -0
- metadata +120 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 52e57ea0de8690904e22e45a92e2fdc608723cc4583f47d3e5f2bdb004e9d139
|
|
4
|
+
data.tar.gz: 6cb6cb7f5d729ced258a185ca106fa0b878c1e1c108908ef40caf1202fcd8d54
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 84b05d1d995acbb3ab9d78b71ffb49130490798f162788b04bfd91c95ea9b94ec71c0477e3d43b8aa0bf9c0e8815b90ffed06fa27c58987ac2d9491b10c777de
|
|
7
|
+
data.tar.gz: 9822db1173331febc6152cf567b61513fb0eadfb08a7a05240a31efc8ea9fed423fd8f5bfb37d8e2a9b1cef58e3a5abb66dc6418572bbdcca875818992581082
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2015-2025 Vendo Connect Inc., Vendo Sp. z o.o.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Spree Legacy product properties
|
|
2
|
+
|
|
3
|
+
This is a Legacy product properties extension for [Spree Commerce](https://spreecommerce.org), an open source e-commerce platform built with Ruby on Rails.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
1. Add this extension to your Gemfile with this line:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
bundle add spree_legacy_product_properties
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2. Run the install generator
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
bundle exec rails g spree_legacy_product_properties:install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
3. Restart your server
|
|
20
|
+
|
|
21
|
+
If your server was running, restart it so that it can find the assets properly.
|
|
22
|
+
|
|
23
|
+
## Developing
|
|
24
|
+
|
|
25
|
+
1. Create a dummy app
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bundle update
|
|
29
|
+
bundle exec rake test_app
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
2. Add your new code
|
|
33
|
+
3. Run tests
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bundle exec rspec
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
When testing your applications integration with this extension you may use it's factories.
|
|
40
|
+
Simply add this require statement to your spec_helper:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
require 'spree_legacy_product_properties/factories'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Releasing a new version
|
|
47
|
+
|
|
48
|
+
```shell
|
|
49
|
+
bundle exec gem bump -p -t
|
|
50
|
+
bundle exec gem release
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For more options please see [gem-release README](https://github.com/svenfuchs/gem-release)
|
|
54
|
+
|
|
55
|
+
## Contributing
|
|
56
|
+
|
|
57
|
+
If you'd like to contribute, please take a look at the
|
|
58
|
+
[instructions](CONTRIBUTING.md) for installing dependencies and crafting a good
|
|
59
|
+
pull request.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require 'bundler'
|
|
2
|
+
Bundler::GemHelper.install_tasks
|
|
3
|
+
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
|
+
require 'spree/testing_support/extension_rake'
|
|
6
|
+
|
|
7
|
+
RSpec::Core::RakeTask.new
|
|
8
|
+
|
|
9
|
+
task :default do
|
|
10
|
+
if Dir['spec/dummy'].empty?
|
|
11
|
+
Rake::Task[:test_app].invoke
|
|
12
|
+
Dir.chdir('../../')
|
|
13
|
+
end
|
|
14
|
+
Rake::Task[:spec].invoke
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc 'Generates a dummy app for testing'
|
|
18
|
+
task :test_app do
|
|
19
|
+
ENV['LIB_NAME'] = 'spree_legacy_product_properties'
|
|
20
|
+
Rake::Task['extension:test_app'].execute(
|
|
21
|
+
install_admin: true
|
|
22
|
+
)
|
|
23
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
module ProductsControllerDecorator
|
|
4
|
+
def self.prepended(base)
|
|
5
|
+
base.new_action.before :build_product_properties
|
|
6
|
+
base.edit_action.before :build_product_properties
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def build_product_properties
|
|
12
|
+
return unless Spree::Config[:product_properties_enabled]
|
|
13
|
+
|
|
14
|
+
Spree::Property.all.each do |property|
|
|
15
|
+
@product.product_properties.build(property: property) unless @product.product_properties.find do |product_property|
|
|
16
|
+
product_property.property_id == property.id
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if defined?(Spree::Admin::ProductsController)
|
|
25
|
+
Spree::Admin::ProductsController.prepend(Spree::Admin::ProductsControllerDecorator)
|
|
26
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
class PropertiesController < ResourceController
|
|
4
|
+
include ProductsBreadcrumbConcern
|
|
5
|
+
add_breadcrumb Spree.t(:properties), :admin_properties_path
|
|
6
|
+
|
|
7
|
+
before_action :add_breadcrumbs
|
|
8
|
+
|
|
9
|
+
protected
|
|
10
|
+
|
|
11
|
+
def update_turbo_stream_enabled?
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add_breadcrumbs
|
|
16
|
+
if @property.present? && @property.persisted?
|
|
17
|
+
add_breadcrumb @property.presentation, spree.edit_admin_property_path(@property)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def permitted_resource_params
|
|
22
|
+
params.require(:property).permit(:name, :presentation, :position, :kind, :display_on)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module ProductProperties
|
|
3
|
+
class FindAvailable
|
|
4
|
+
include ProductFilterable
|
|
5
|
+
|
|
6
|
+
def initialize(scope: ProductProperty.spree_base_scopes, products_scope: Product.spree_base_scopes)
|
|
7
|
+
@scope = scope
|
|
8
|
+
@products_scope = products_scope
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def execute
|
|
12
|
+
find_available(scope, products_scope).includes(:translations)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
attr_reader :scope, :products_scope
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Products
|
|
3
|
+
module FindDecorator
|
|
4
|
+
def initialize(scope:, params:)
|
|
5
|
+
@properties = params.dig(:filter, :properties)
|
|
6
|
+
super
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def execute
|
|
10
|
+
products = super
|
|
11
|
+
# Apply property filtering after all other filters
|
|
12
|
+
by_properties(products)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
attr_reader :properties
|
|
18
|
+
|
|
19
|
+
def properties?
|
|
20
|
+
properties.present? && properties.values.reject(&:empty?).present?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def by_properties(products)
|
|
24
|
+
return products unless properties?
|
|
25
|
+
|
|
26
|
+
product_ids = []
|
|
27
|
+
index = 0
|
|
28
|
+
|
|
29
|
+
properties.to_unsafe_hash.each do |property_filter_param, product_properties_values|
|
|
30
|
+
next if property_filter_param.blank? || product_properties_values.empty?
|
|
31
|
+
|
|
32
|
+
values = product_properties_values.split(',').reject(&:empty?).uniq.map(&:parameterize)
|
|
33
|
+
|
|
34
|
+
next if values.empty?
|
|
35
|
+
|
|
36
|
+
ids = scope.unscope(:order, :includes).with_property_values(property_filter_param, values).ids
|
|
37
|
+
product_ids = index == 0 ? ids : product_ids & ids
|
|
38
|
+
index += 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
products.where(id: product_ids)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
Spree::Products::Find.prepend(Spree::Products::FindDecorator)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module FilterParam
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
before_save :set_filter_param
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
protected
|
|
10
|
+
|
|
11
|
+
def set_filter_param
|
|
12
|
+
return if param_candidate.blank?
|
|
13
|
+
|
|
14
|
+
self.filter_param = param_candidate.parameterize
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def param_candidate
|
|
18
|
+
name
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module ProductDecorator
|
|
3
|
+
def self.prepended(base)
|
|
4
|
+
base.has_many :product_properties, dependent: :destroy, inverse_of: :product
|
|
5
|
+
base.has_many :properties, through: :product_properties
|
|
6
|
+
|
|
7
|
+
base.accepts_nested_attributes_for :product_properties, allow_destroy: true, reject_if: lambda { |pp|
|
|
8
|
+
pp[:property_id].blank? || (pp[:id].blank? && pp[:value].blank?)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
base.whitelisted_ransackable_associations |= %w[properties]
|
|
12
|
+
|
|
13
|
+
# Property scopes
|
|
14
|
+
base.add_search_scope :with_property do |property|
|
|
15
|
+
joins(:properties).where(Spree::Product.property_conditions(property))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
base.add_search_scope :with_property_value do |property, value|
|
|
19
|
+
if Spree.use_translations?
|
|
20
|
+
joins(:properties).
|
|
21
|
+
join_translation_table(Spree::Property).
|
|
22
|
+
join_translation_table(Spree::ProductProperty).
|
|
23
|
+
where(Spree::ProductProperty.translation_table_alias => { value: value }).
|
|
24
|
+
where(Spree::Product.property_conditions(property))
|
|
25
|
+
else
|
|
26
|
+
joins(:properties).
|
|
27
|
+
where(Spree::ProductProperty.table_name => { value: value }).
|
|
28
|
+
where(Spree::Product.property_conditions(property))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
base.add_search_scope :with_property_values do |property_filter_param, property_values|
|
|
33
|
+
joins(product_properties: :property).
|
|
34
|
+
where(Spree::Property.table_name => { filter_param: property_filter_param }).
|
|
35
|
+
where(Spree::ProductProperty.table_name => { filter_param: property_values.map(&:parameterize) })
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.property_conditions(property)
|
|
40
|
+
properties_table = Spree::Property.table_name
|
|
41
|
+
|
|
42
|
+
case property
|
|
43
|
+
when Spree::Property then { "#{properties_table}.id" => property.id }
|
|
44
|
+
when Integer then { "#{properties_table}.id" => property }
|
|
45
|
+
else
|
|
46
|
+
if Spree::Property.column_for_attribute('id').type == :uuid
|
|
47
|
+
["#{properties_table}.name = ? OR #{properties_table}.id = ?", property, property]
|
|
48
|
+
else
|
|
49
|
+
{ "#{properties_table}.name" => property }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def property(property_name)
|
|
55
|
+
if product_properties.loaded?
|
|
56
|
+
product_properties.detect { |pp| pp.property.name == property_name }.try(:value)
|
|
57
|
+
else
|
|
58
|
+
product_properties.joins(:property).find_by(spree_properties: { name: property_name }).try(:value)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def set_property(property_name, property_value, property_presentation = property_name)
|
|
63
|
+
property_name = property_name.to_s.parameterize
|
|
64
|
+
ApplicationRecord.transaction do
|
|
65
|
+
prop = if Spree::Property.where(name: property_name).exists?
|
|
66
|
+
existing_property = Spree::Property.where(name: property_name).first
|
|
67
|
+
existing_property.presentation ||= property_presentation
|
|
68
|
+
existing_property.save
|
|
69
|
+
existing_property
|
|
70
|
+
else
|
|
71
|
+
Spree::Property.create(name: property_name, presentation: property_presentation)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
product_property = if Spree::ProductProperty.where(product: self, property: prop).exists?
|
|
75
|
+
Spree::ProductProperty.where(product: self, property: prop).first
|
|
76
|
+
else
|
|
77
|
+
Spree::ProductProperty.new(product: self, property: prop)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
product_property.value = property_value
|
|
81
|
+
product_property.save!
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def remove_property(property_name)
|
|
86
|
+
product_properties.joins(:property).find_by(spree_properties: { name: property_name.parameterize })&.destroy
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def storefront_description
|
|
90
|
+
property('short_description') || description
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def add_associations_from_prototype
|
|
96
|
+
if prototype_id && (prototype = Spree::Prototype.find_by(id: prototype_id))
|
|
97
|
+
prototype.properties.each do |prop|
|
|
98
|
+
product_properties.create(property: prop, value: 'Placeholder')
|
|
99
|
+
end
|
|
100
|
+
self.option_types = prototype.option_types
|
|
101
|
+
self.taxons = prototype.taxons
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
Spree::Product.prepend(Spree::ProductDecorator)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class ProductProperty < Spree.base_class
|
|
3
|
+
include Spree::FilterParam
|
|
4
|
+
include Spree::TranslatableResource
|
|
5
|
+
|
|
6
|
+
if Spree.always_use_translations?
|
|
7
|
+
TRANSLATABLE_FIELDS = %i[value filter_param].freeze
|
|
8
|
+
translates(*TRANSLATABLE_FIELDS)
|
|
9
|
+
else
|
|
10
|
+
TRANSLATABLE_FIELDS = %i[value].freeze
|
|
11
|
+
translates(*TRANSLATABLE_FIELDS, column_fallback: true)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
self::Translation.class_eval do
|
|
15
|
+
normalizes :value, with: ->(value) { value&.to_s&.squish&.presence }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
normalizes :value, with: ->(value) { value&.to_s&.squish&.presence }
|
|
19
|
+
|
|
20
|
+
acts_as_list scope: :product
|
|
21
|
+
|
|
22
|
+
with_options inverse_of: :product_properties do
|
|
23
|
+
belongs_to :product, touch: true, class_name: 'Spree::Product'
|
|
24
|
+
belongs_to :property, touch: true, class_name: 'Spree::Property'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
validates :property, presence: true
|
|
28
|
+
validates :property_id, uniqueness: { scope: :product_id }
|
|
29
|
+
validates :value, presence: true
|
|
30
|
+
|
|
31
|
+
default_scope { order(:position) }
|
|
32
|
+
|
|
33
|
+
scope :filterable, -> { joins(:property).where(Property.table_name => { filterable: true }) }
|
|
34
|
+
scope :for_products, ->(products) { where(product_id: products) }
|
|
35
|
+
scope :sort_by_property_position, -> {
|
|
36
|
+
unscope(:order).joins(:property).order(Spree::Property.table_name => { position: :asc })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
self.whitelisted_ransackable_attributes = ['value', 'filter_param']
|
|
40
|
+
self.whitelisted_ransackable_associations = ['property']
|
|
41
|
+
|
|
42
|
+
# virtual attributes for use with AJAX completion stuff
|
|
43
|
+
delegate :name, :presentation, to: :property, prefix: true, allow_nil: true
|
|
44
|
+
|
|
45
|
+
protected
|
|
46
|
+
|
|
47
|
+
def param_candidate
|
|
48
|
+
value
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Products
|
|
3
|
+
module DuplicatorDecorator
|
|
4
|
+
def call(product:, include_images: true)
|
|
5
|
+
result = super
|
|
6
|
+
return result unless result.success?
|
|
7
|
+
|
|
8
|
+
new_product = result.value
|
|
9
|
+
new_product.product_properties = duplicate_properties(product.product_properties) if new_product.persisted?
|
|
10
|
+
|
|
11
|
+
result
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
protected
|
|
15
|
+
|
|
16
|
+
def duplicate_properties(product_properties)
|
|
17
|
+
product_properties.map do |prop|
|
|
18
|
+
new_prop = prop.dup
|
|
19
|
+
new_prop.product = nil
|
|
20
|
+
new_prop.created_at = nil
|
|
21
|
+
new_prop.updated_at = nil
|
|
22
|
+
new_prop
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Spree::Products::Duplicator.prepend(Spree::Products::DuplicatorDecorator)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class Property < Spree.base_class
|
|
3
|
+
include Spree::FilterParam
|
|
4
|
+
include Spree::Metadata
|
|
5
|
+
include Spree::ParameterizableName
|
|
6
|
+
include Spree::UniqueName
|
|
7
|
+
include Spree::DisplayOn
|
|
8
|
+
include Spree::TranslatableResource
|
|
9
|
+
|
|
10
|
+
TRANSLATABLE_FIELDS = %i[presentation].freeze
|
|
11
|
+
translates(*TRANSLATABLE_FIELDS, column_fallback: !Spree.always_use_translations?)
|
|
12
|
+
|
|
13
|
+
self::Translation.class_eval do
|
|
14
|
+
normalizes :presentation, with: ->(value) { value&.to_s&.squish&.presence }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
acts_as_list
|
|
18
|
+
|
|
19
|
+
has_many :property_prototypes, class_name: 'Spree::PropertyPrototype'
|
|
20
|
+
has_many :prototypes, through: :property_prototypes, class_name: 'Spree::Prototype'
|
|
21
|
+
|
|
22
|
+
has_many :product_properties, dependent: :delete_all, inverse_of: :property
|
|
23
|
+
has_many :products, through: :product_properties
|
|
24
|
+
|
|
25
|
+
validates :name, :presentation, presence: true
|
|
26
|
+
|
|
27
|
+
default_scope { order(:position) }
|
|
28
|
+
scope :sorted, -> { order(:name) }
|
|
29
|
+
scope :filterable, -> { where(filterable: true) }
|
|
30
|
+
|
|
31
|
+
KIND_OPTIONS = { short_text: 0, long_text: 1, number: 2, rich_text: 3 }.freeze
|
|
32
|
+
enum :kind, KIND_OPTIONS
|
|
33
|
+
|
|
34
|
+
DEPENDENCY_UPDATE_FIELDS = [:presentation, :name, :kind, :filterable, :display_on, :position].freeze
|
|
35
|
+
|
|
36
|
+
after_touch :touch_all_products
|
|
37
|
+
after_update :touch_all_products, if: -> { DEPENDENCY_UPDATE_FIELDS.any? { |field| saved_changes.key?(field) } }
|
|
38
|
+
after_save :ensure_product_properties_have_filter_params
|
|
39
|
+
|
|
40
|
+
self.whitelisted_ransackable_attributes = ['presentation', 'filterable']
|
|
41
|
+
|
|
42
|
+
def uniq_values(product_properties_scope: nil)
|
|
43
|
+
with_uniq_values_cache_key(product_properties_scope) do
|
|
44
|
+
properties = product_properties
|
|
45
|
+
properties = properties.where(id: product_properties_scope) if product_properties_scope.present?
|
|
46
|
+
properties.where('value IS NOT NULL AND value != ?', '').pluck(:filter_param, :value).uniq
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns the metafield type for the property kind
|
|
51
|
+
# @return [String] eg. 'Spree::Metafields::ShortText'
|
|
52
|
+
def kind_to_metafield_type
|
|
53
|
+
case kind
|
|
54
|
+
when 'short_text'
|
|
55
|
+
'Spree::Metafields::ShortText'
|
|
56
|
+
when 'long_text'
|
|
57
|
+
'Spree::Metafields::LongText'
|
|
58
|
+
when 'number'
|
|
59
|
+
'Spree::Metafields::Number'
|
|
60
|
+
when 'rich_text'
|
|
61
|
+
'Spree::Metafields::RichText'
|
|
62
|
+
else
|
|
63
|
+
'Spree::Metafields::ShortText'
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def touch_all_products
|
|
70
|
+
products.touch_all
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def with_uniq_values_cache_key(product_properties_scope, &block)
|
|
74
|
+
return block.call if product_properties_scope.present?
|
|
75
|
+
|
|
76
|
+
uniq_values_cache_key = ['property-uniq-values', cache_key_with_version]
|
|
77
|
+
Rails.cache.fetch(uniq_values_cache_key) { block.call }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def ensure_product_properties_have_filter_params
|
|
81
|
+
return unless filterable?
|
|
82
|
+
|
|
83
|
+
product_properties.where(filter_param: [nil, '']).where('value IS NOT NULL AND value != ?', '').find_each(&:save)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class PropertyPrototype < Spree.base_class
|
|
3
|
+
belongs_to :prototype, class_name: 'Spree::Prototype'
|
|
4
|
+
belongs_to :property, class_name: 'Spree::Property'
|
|
5
|
+
|
|
6
|
+
validates :prototype, :property, presence: true
|
|
7
|
+
validates :prototype_id, uniqueness: { scope: :property_id }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module PrototypeDecorator
|
|
3
|
+
def self.prepended(base)
|
|
4
|
+
base.has_many :property_prototypes, class_name: 'Spree::PropertyPrototype'
|
|
5
|
+
base.has_many :properties, through: :property_prototypes, class_name: 'Spree::Property'
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
Spree::Prototype.prepend(Spree::PrototypeDecorator)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Filters
|
|
3
|
+
class PropertiesPresenter
|
|
4
|
+
def initialize(product_properties_scope:)
|
|
5
|
+
@product_properties = product_properties_scope.includes(:property)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def to_a
|
|
9
|
+
grouped_options.map do |property, product_properties|
|
|
10
|
+
PropertyPresenter.new(property: property, product_properties: product_properties)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
attr_reader :product_properties
|
|
17
|
+
|
|
18
|
+
def grouped_options
|
|
19
|
+
product_properties.group_by(&:property)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Filters
|
|
3
|
+
class PropertyPresenter
|
|
4
|
+
def initialize(property:, product_properties:)
|
|
5
|
+
@property = property
|
|
6
|
+
@product_properties = product_properties
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
attr_reader :product_properties
|
|
10
|
+
|
|
11
|
+
delegate_missing_to :property
|
|
12
|
+
|
|
13
|
+
def uniq_values
|
|
14
|
+
property.uniq_values(product_properties_scope: product_properties)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
{
|
|
19
|
+
id: property.id,
|
|
20
|
+
name: property.name,
|
|
21
|
+
presentation: property.presentation,
|
|
22
|
+
values: values_hash
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :property
|
|
29
|
+
|
|
30
|
+
def values_hash
|
|
31
|
+
value_hashes = uniq_values.map do |filter_param, value|
|
|
32
|
+
{
|
|
33
|
+
value: value,
|
|
34
|
+
filter_param: filter_param
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
value_hashes.sort_by { |e| e[:value] }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Products
|
|
3
|
+
module PrepareNestedAttributesDecorator
|
|
4
|
+
def call
|
|
5
|
+
# Mark product properties for removal when value is left blank
|
|
6
|
+
if params[:product_properties_attributes].present?
|
|
7
|
+
params[:product_properties_attributes].each do |key, product_property_params|
|
|
8
|
+
next unless product_property_params[:id].present?
|
|
9
|
+
next if product_property_params[:value].present?
|
|
10
|
+
|
|
11
|
+
# https://api.rubyonrails.org/v7.1.3.4/classes/ActiveRecord/NestedAttributes/ClassMethods.html
|
|
12
|
+
params[:product_properties_attributes][key]['_destroy'] = '1'
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Spree::Products::PrepareNestedAttributes.prepend(Spree::Products::PrepareNestedAttributesDecorator)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<% if Spree::Config[:product_properties_enabled] %>
|
|
2
|
+
<div class="card mb-6">
|
|
3
|
+
<div class="card-header flex items-center justify-between">
|
|
4
|
+
<h5 class="card-title"><%= Spree.t(:properties) %></h5>
|
|
5
|
+
|
|
6
|
+
<% if can?(:manage, Spree::Property) %>
|
|
7
|
+
<%= link_to_with_icon 'adjustments', Spree.t('admin.manage_properties'), spree.admin_properties_path, class: 'btn btn-sm btn-light' %>
|
|
8
|
+
<% end %>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="card-body p-0">
|
|
11
|
+
<table class="table">
|
|
12
|
+
<thead>
|
|
13
|
+
<tr>
|
|
14
|
+
<th scope="col"><%= Spree.t(:property) %></th>
|
|
15
|
+
<th scope="col"><%= Spree.t(:value) %></th>
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
<tbody>
|
|
19
|
+
<%= f.fields_for :product_properties, sorted_product_properties(f.object) do |product_property_form| %>
|
|
20
|
+
<% property = product_property_form.object.property %>
|
|
21
|
+
<%= product_property_form.hidden_field :id %>
|
|
22
|
+
<%= product_property_form.hidden_field :property_id %>
|
|
23
|
+
<tr>
|
|
24
|
+
<td class="align-top pt-4 w-1/4">
|
|
25
|
+
<code><%= property.name %></code>
|
|
26
|
+
</td>
|
|
27
|
+
<td class="w-3/4">
|
|
28
|
+
<div class="form-group">
|
|
29
|
+
<% if property.long_text? %>
|
|
30
|
+
<%= product_property_form.text_area :value, { class: 'form-input', data: { controller: 'textarea-autogrow'} } %>
|
|
31
|
+
<% elsif property.number? %>
|
|
32
|
+
<%= product_property_form.number_field :value, class: 'form-input' %>
|
|
33
|
+
<% elsif property.rich_text? %>
|
|
34
|
+
<div class="trix-container">
|
|
35
|
+
<%= product_property_form.rich_text_area :value %>
|
|
36
|
+
</div>
|
|
37
|
+
<% else %>
|
|
38
|
+
<%= product_property_form.text_field :value, class: 'form-input' %>
|
|
39
|
+
<% end %>
|
|
40
|
+
</div>
|
|
41
|
+
</td>
|
|
42
|
+
</tr>
|
|
43
|
+
<% end %>
|
|
44
|
+
</tbody>
|
|
45
|
+
</table>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<% end %>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<%= search_form_for [:admin, @search], class: "filter-wrap", data: {controller: "filters"} do |f| %>
|
|
2
|
+
<%= render 'spree/admin/shared/filters_search_bar', param: :name_cont, label: Spree.t(:name) %>
|
|
3
|
+
<%= render "spree/admin/shared/filter_badge_template" %>
|
|
4
|
+
<div data-filters-target="badgesContainer" class="filter-badges-container"></div>
|
|
5
|
+
<% end %>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<div data-controller="slug-form" class="card mb-6">
|
|
2
|
+
<div class="card-body">
|
|
3
|
+
<%= f.spree_text_field :presentation, data: { slug_form_target: :name, action: 'input->slug-form#updateUrlFromName' }, required: true, autofocus: f.object.new_record? %>
|
|
4
|
+
<%= f.spree_text_field :name, label: Spree.t(:internal_name), data: { slug_form_target: :url }, required: true %>
|
|
5
|
+
<%= f.spree_select :kind, options_for_select(Spree::Property.kinds.map{ |k| [k.first.humanize, k.first.to_sym] }, @object.kind) %>
|
|
6
|
+
<%= f.spree_select :display_on, display_on_options, { label: Spree.t(:display_on) } %>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<tr>
|
|
2
|
+
<th class="no-border handel-head"></th>
|
|
3
|
+
<th scope="col"><%= Spree.t(:internal_name) %></th>
|
|
4
|
+
<th scope="col"><%= Spree.t(:presentation) %></th>
|
|
5
|
+
<th scope="col"><%= Spree.t(:kind) %></th>
|
|
6
|
+
<th scope="col"><%= Spree.t(:products) %></th>
|
|
7
|
+
<th scope="col"><%= Spree.t(:visibility) %></th>
|
|
8
|
+
<th scope="col"></th>
|
|
9
|
+
</tr>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<tr id="<%= spree_dom_id property %>" data-controller="row-link" data-sortable-update-url="<%= spree.admin_property_path(property) %>">
|
|
2
|
+
<td class="w-5 move-handle text-center">
|
|
3
|
+
<%= icon('grip-vertical', class: 'rounded-md hover:bg-gray-100 p-2') %>
|
|
4
|
+
</td>
|
|
5
|
+
<td class="w-20 cursor-pointer py-0" data-action="click->row-link#openLink">
|
|
6
|
+
<code><%= property.name %></code>
|
|
7
|
+
</td>
|
|
8
|
+
<td class="w-20 cursor-pointer" data-action="click->row-link#openLink">
|
|
9
|
+
<%= link_to property.presentation, spree.edit_admin_property_path(property), class: 'no-underline flex items-center font-bold text-gray-900', data: { row_link_target: :link, turbo_frame: '_top' } %>
|
|
10
|
+
</td>
|
|
11
|
+
<td class="w-20 cursor-pointer" data-action="click->row-link#openLink">
|
|
12
|
+
<span class="badge badge-light"><%= property.kind.humanize %></span>
|
|
13
|
+
</td>
|
|
14
|
+
<td class="w-10 cursor-pointer" data-action="click->row-link#openLink">
|
|
15
|
+
<%= property.products.count %>
|
|
16
|
+
</td>
|
|
17
|
+
<td>
|
|
18
|
+
<%= form_with model: property, url: admin_property_path(property), method: :patch, data: { controller: 'auto-submit' } do |f| %>
|
|
19
|
+
<%= f.select :display_on, display_on_options, {}, { class: 'form-select form-select-sm', data: { action: 'auto-submit#submit' } } %>
|
|
20
|
+
<% end %>
|
|
21
|
+
</td>
|
|
22
|
+
<td class="w-10 actions">
|
|
23
|
+
<%= link_to_edit(property, class: 'btn btn-light btn-sm', data: { turbo_frame: '_top' }) if can?(:edit, property) %>
|
|
24
|
+
</td>
|
|
25
|
+
</tr>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<% content_for(:page_title) do %>
|
|
2
|
+
<%= Spree.t(:properties) %>
|
|
3
|
+
<% end %>
|
|
4
|
+
|
|
5
|
+
<% content_for(:page_alerts) do %>
|
|
6
|
+
<div class="alert alert-info">
|
|
7
|
+
Properties allow you to enrich your Product information.
|
|
8
|
+
</div>
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<% content_for :page_actions do %>
|
|
12
|
+
<%= render_admin_partials(:properties_actions_partials) %>
|
|
13
|
+
<%= link_to_with_icon 'plus', Spree.t(:new_property), new_object_url, class: "btn btn-primary" if can?(:create, Spree::Property) %>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<%= render_admin_partials(:properties_header_partials) %>
|
|
17
|
+
|
|
18
|
+
<%= render 'spree/admin/shared/index_table', sortable: true %>
|
|
19
|
+
|
|
20
|
+
<p class="documentation-link-container">
|
|
21
|
+
<%= external_link_to "Learn more about properties", "https://spreecommerce.org/docs/user/manage-products/product-properties" %>
|
|
22
|
+
</p>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= render 'spree/admin/shared/new_resource' %>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= turbo_render_alerts %>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Rails.application.config.after_initialize do
|
|
2
|
+
# Register product_properties_enabled preference if not already defined
|
|
3
|
+
unless Spree::Config.respond_to?(:product_properties_enabled)
|
|
4
|
+
Spree::Core::Configuration.preference :product_properties_enabled, :boolean, default: false
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
# Register permitted attributes for product properties
|
|
8
|
+
unless Spree::PermittedAttributes::ATTRIBUTES.include?(:product_properties_attributes)
|
|
9
|
+
Spree::PermittedAttributes::ATTRIBUTES.push(:product_properties_attributes, :property_attributes)
|
|
10
|
+
|
|
11
|
+
Spree::PermittedAttributes.class_eval do
|
|
12
|
+
mattr_accessor :product_properties_attributes
|
|
13
|
+
mattr_accessor :property_attributes
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
Spree::PermittedAttributes.product_properties_attributes = [
|
|
17
|
+
:property_name, :property_id, :value, :position, :_destroy
|
|
18
|
+
]
|
|
19
|
+
Spree::PermittedAttributes.property_attributes = [
|
|
20
|
+
:name, :presentation, :position, :kind, :display_on
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# Re-delegate the new attributes
|
|
24
|
+
Spree::Core::ControllerHelpers::StrongParameters.delegate(
|
|
25
|
+
:permitted_product_properties_attributes,
|
|
26
|
+
:permitted_property_attributes,
|
|
27
|
+
to: :permitted_attributes,
|
|
28
|
+
prefix: false
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Override permitted_product_attributes to include product_properties_attributes
|
|
33
|
+
Spree::Core::ControllerHelpers::StrongParameters.module_eval do
|
|
34
|
+
def permitted_product_attributes
|
|
35
|
+
permitted_attributes.product_attributes + [
|
|
36
|
+
variants_attributes: permitted_variant_attributes + ['id', :_destroy],
|
|
37
|
+
master_attributes: permitted_variant_attributes + ['id'],
|
|
38
|
+
product_properties_attributes: permitted_product_properties_attributes + ['id', :_destroy]
|
|
39
|
+
]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Register product form partial for properties
|
|
44
|
+
if defined?(Spree::Admin) && Rails.application.config.respond_to?(:spree_admin)
|
|
45
|
+
Rails.application.config.spree_admin.product_form_partials << 'spree/admin/products/form/properties'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Register admin navigation
|
|
49
|
+
if defined?(Spree::Admin) && Spree.respond_to?(:admin)
|
|
50
|
+
sidebar_nav = Spree.admin.navigation.sidebar
|
|
51
|
+
|
|
52
|
+
products_item = sidebar_nav.find(:products)
|
|
53
|
+
if products_item
|
|
54
|
+
builder = Spree::Admin::Navigation::Builder.new(sidebar_nav, products_item)
|
|
55
|
+
builder.add :properties,
|
|
56
|
+
label: :properties,
|
|
57
|
+
url: :admin_properties_path,
|
|
58
|
+
position: 50,
|
|
59
|
+
if: -> { can?(:manage, Spree::Property) && Spree::Config.product_properties_enabled }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
class CreateSpreePropertiesAndProductProperties < ActiveRecord::Migration[7.2]
|
|
2
|
+
def json_column_type
|
|
3
|
+
connection.adapter_name.downcase.include?('postgresql') ? :jsonb : :json
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def change
|
|
7
|
+
unless table_exists?(:spree_properties)
|
|
8
|
+
create_table :spree_properties do |t|
|
|
9
|
+
t.string :name
|
|
10
|
+
t.string :presentation, null: false
|
|
11
|
+
t.string :filter_param
|
|
12
|
+
t.boolean :filterable, default: false, null: false
|
|
13
|
+
t.integer :kind, default: 0
|
|
14
|
+
t.string :display_on, default: 'both'
|
|
15
|
+
t.integer :position, default: 0
|
|
16
|
+
t.column :public_metadata, json_column_type
|
|
17
|
+
t.column :private_metadata, json_column_type
|
|
18
|
+
|
|
19
|
+
t.timestamps
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
add_index :spree_properties, :name, unique: true
|
|
23
|
+
add_index :spree_properties, :filter_param
|
|
24
|
+
add_index :spree_properties, :filterable
|
|
25
|
+
add_index :spree_properties, :position
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
unless table_exists?(:spree_product_properties)
|
|
29
|
+
create_table :spree_product_properties do |t|
|
|
30
|
+
t.string :value
|
|
31
|
+
t.references :product, null: false
|
|
32
|
+
t.references :property, null: false
|
|
33
|
+
t.integer :position, default: 0
|
|
34
|
+
t.string :filter_param
|
|
35
|
+
t.boolean :show_property, default: true
|
|
36
|
+
|
|
37
|
+
t.timestamps
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
add_index :spree_product_properties, [:property_id, :product_id], unique: true
|
|
41
|
+
add_index :spree_product_properties, :filter_param
|
|
42
|
+
add_index :spree_product_properties, :position
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
unless table_exists?(:spree_property_prototypes)
|
|
46
|
+
create_table :spree_property_prototypes do |t|
|
|
47
|
+
t.references :prototype
|
|
48
|
+
t.references :property
|
|
49
|
+
|
|
50
|
+
t.timestamps
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
add_index :spree_property_prototypes, [:prototype_id, :property_id], unique: true, name: 'index_property_prototypes_on_prototype_id_and_property_id'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
unless table_exists?(:spree_property_translations)
|
|
57
|
+
create_table :spree_property_translations do |t|
|
|
58
|
+
t.references :spree_property, null: false
|
|
59
|
+
t.string :locale, null: false
|
|
60
|
+
t.string :presentation
|
|
61
|
+
|
|
62
|
+
t.timestamps
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
add_index :spree_property_translations, :locale
|
|
66
|
+
add_index :spree_property_translations, [:spree_property_id, :locale], unique: true, name: 'unique_property_id_per_locale'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
unless table_exists?(:spree_product_property_translations)
|
|
70
|
+
create_table :spree_product_property_translations do |t|
|
|
71
|
+
t.references :spree_product_property, null: false
|
|
72
|
+
t.string :locale, null: false
|
|
73
|
+
t.string :value
|
|
74
|
+
|
|
75
|
+
t.timestamps
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
add_index :spree_product_property_translations, :locale
|
|
79
|
+
add_index :spree_product_property_translations, [:spree_product_property_id, :locale], unique: true, name: 'unique_product_property_id_per_locale'
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# For existing installations that already have these tables but may be missing columns
|
|
83
|
+
if table_exists?(:spree_properties)
|
|
84
|
+
unless column_exists?(:spree_properties, :public_metadata)
|
|
85
|
+
add_column :spree_properties, :public_metadata, json_column_type
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
unless column_exists?(:spree_properties, :private_metadata)
|
|
89
|
+
add_column :spree_properties, :private_metadata, json_column_type
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
unless column_exists?(:spree_properties, :display_on)
|
|
93
|
+
add_column :spree_properties, :display_on, :string, default: 'both'
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
unless column_exists?(:spree_properties, :position)
|
|
97
|
+
add_column :spree_properties, :position, :integer, default: 0
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
unless column_exists?(:spree_properties, :kind)
|
|
101
|
+
add_column :spree_properties, :kind, :integer, default: 0
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
unless column_exists?(:spree_properties, :filter_param)
|
|
105
|
+
add_column :spree_properties, :filter_param, :string
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
unless column_exists?(:spree_properties, :filterable)
|
|
109
|
+
add_column :spree_properties, :filterable, :boolean, default: false, null: false
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module SpreeLegacyProductProperties
|
|
2
|
+
module Generators
|
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
|
4
|
+
class_option :migrate, type: :boolean, default: true
|
|
5
|
+
|
|
6
|
+
def add_migrations
|
|
7
|
+
run 'bundle exec rake railties:install:migrations FROM=spree_legacy_product_properties'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def run_migrations
|
|
11
|
+
run_migrations = options[:migrate] || ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]'))
|
|
12
|
+
if run_migrations
|
|
13
|
+
run 'bin/rails db:migrate'
|
|
14
|
+
else
|
|
15
|
+
puts 'Skipping rails db:migrate, don\'t forget to run it!'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module SpreeLegacyProductProperties
|
|
2
|
+
class Configuration < Spree::Preferences::Configuration
|
|
3
|
+
|
|
4
|
+
# Some example preferences are shown below, for more information visit:
|
|
5
|
+
# https://docs.spreecommerce.org/developer/contributing/creating-an-extension
|
|
6
|
+
|
|
7
|
+
# preference :enabled, :boolean, default: true
|
|
8
|
+
# preference :dark_chocolate, :boolean, default: true
|
|
9
|
+
# preference :color, :string, default: 'Red'
|
|
10
|
+
# preference :favorite_number, :integer
|
|
11
|
+
# preference :supported_locales, :array, default: [:en]
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module SpreeLegacyProductProperties
|
|
2
|
+
class Engine < Rails::Engine
|
|
3
|
+
require 'spree/core'
|
|
4
|
+
isolate_namespace Spree
|
|
5
|
+
engine_name 'spree_legacy_product_properties'
|
|
6
|
+
|
|
7
|
+
config.generators do |g|
|
|
8
|
+
g.test_framework :rspec
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
initializer 'spree_legacy_product_properties.environment', before: :load_config_initializers do |_app|
|
|
12
|
+
SpreeLegacyProductProperties::Config = SpreeLegacyProductProperties::Configuration.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.activate
|
|
16
|
+
Dir.glob(File.join(File.dirname(__FILE__), '../../app/**/*_decorator*.rb')) do |c|
|
|
17
|
+
Rails.configuration.cache_classes ? require(c) : load(c)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
config.to_prepare(&method(:activate).to_proc)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
FactoryBot.define do
|
|
2
|
+
factory :property, class: Spree::Property do
|
|
3
|
+
sequence(:name) { |n| "baseball_cap_color_#{n}" }
|
|
4
|
+
presentation { 'cap color' }
|
|
5
|
+
|
|
6
|
+
trait :filterable do
|
|
7
|
+
filterable { true }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
trait :brand do
|
|
11
|
+
name { 'brand' }
|
|
12
|
+
presentation { 'Brand' }
|
|
13
|
+
filter_param { 'brand' }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
trait :manufacturer do
|
|
17
|
+
name { 'manufacturer' }
|
|
18
|
+
presentation { 'Manufacturer' }
|
|
19
|
+
filter_param { 'manufacturer' }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
trait :material do
|
|
23
|
+
name { 'material' }
|
|
24
|
+
presentation { 'Material' }
|
|
25
|
+
filter_param { 'material' }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
factory :product_property, class: Spree::ProductProperty do
|
|
30
|
+
product
|
|
31
|
+
value { "val-#{rand(50)}" }
|
|
32
|
+
property
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require 'spree'
|
|
2
|
+
require 'spree_legacy_product_properties/engine'
|
|
3
|
+
require 'spree_legacy_product_properties/version'
|
|
4
|
+
require 'spree_legacy_product_properties/configuration'
|
|
5
|
+
|
|
6
|
+
module SpreeLegacyProductProperties
|
|
7
|
+
mattr_accessor :queue
|
|
8
|
+
|
|
9
|
+
def self.queue
|
|
10
|
+
@@queue ||= Spree.queues.default
|
|
11
|
+
end
|
|
12
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: spree_legacy_product_properties
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Vendo Connect Inc., Vendo Sp. z o.o.
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: spree
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 5.4.0.beta
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 5.4.0.beta
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: spree_admin
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 5.4.0.beta
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 5.4.0.beta
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: spree_dev_tools
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
description: Legacy product properties system extracted from Spree core. Replaced
|
|
55
|
+
by Metafields in Spree 5.x.
|
|
56
|
+
email: hello@spreecommerce.org
|
|
57
|
+
executables: []
|
|
58
|
+
extensions: []
|
|
59
|
+
extra_rdoc_files: []
|
|
60
|
+
files:
|
|
61
|
+
- LICENSE.md
|
|
62
|
+
- README.md
|
|
63
|
+
- Rakefile
|
|
64
|
+
- app/controllers/spree/admin/products_controller_decorator.rb
|
|
65
|
+
- app/controllers/spree/admin/properties_controller.rb
|
|
66
|
+
- app/finders/spree/product_properties/find_available.rb
|
|
67
|
+
- app/finders/spree/products/find_decorator.rb
|
|
68
|
+
- app/helpers/spree/admin/properties_helper.rb
|
|
69
|
+
- app/models/concerns/spree/filter_param.rb
|
|
70
|
+
- app/models/spree/permission_sets/property_management.rb
|
|
71
|
+
- app/models/spree/product_decorator.rb
|
|
72
|
+
- app/models/spree/product_property.rb
|
|
73
|
+
- app/models/spree/products/duplicator_decorator.rb
|
|
74
|
+
- app/models/spree/property.rb
|
|
75
|
+
- app/models/spree/property_prototype.rb
|
|
76
|
+
- app/models/spree/prototype_decorator.rb
|
|
77
|
+
- app/presenters/spree/filters/properties_presenter.rb
|
|
78
|
+
- app/presenters/spree/filters/property_presenter.rb
|
|
79
|
+
- app/services/spree/products/prepare_nested_attributes_decorator.rb
|
|
80
|
+
- app/views/spree/admin/products/form/_properties.html.erb
|
|
81
|
+
- app/views/spree/admin/properties/_filters.html.erb
|
|
82
|
+
- app/views/spree/admin/properties/_form.html.erb
|
|
83
|
+
- app/views/spree/admin/properties/_table_header.html.erb
|
|
84
|
+
- app/views/spree/admin/properties/_table_row.html.erb
|
|
85
|
+
- app/views/spree/admin/properties/edit.html.erb
|
|
86
|
+
- app/views/spree/admin/properties/index.html.erb
|
|
87
|
+
- app/views/spree/admin/properties/new.html.erb
|
|
88
|
+
- app/views/spree/admin/properties/update.turbo_stream.erb
|
|
89
|
+
- config/initializers/spree.rb
|
|
90
|
+
- config/routes.rb
|
|
91
|
+
- db/migrate/20221229000000_create_spree_properties_and_product_properties.rb
|
|
92
|
+
- lib/generators/spree_legacy_product_properties/install/install_generator.rb
|
|
93
|
+
- lib/spree_legacy_product_properties.rb
|
|
94
|
+
- lib/spree_legacy_product_properties/configuration.rb
|
|
95
|
+
- lib/spree_legacy_product_properties/engine.rb
|
|
96
|
+
- lib/spree_legacy_product_properties/factories.rb
|
|
97
|
+
- lib/spree_legacy_product_properties/version.rb
|
|
98
|
+
homepage: https://github.com/spree/spree-legacy-product-properties
|
|
99
|
+
licenses:
|
|
100
|
+
- MIT
|
|
101
|
+
metadata: {}
|
|
102
|
+
rdoc_options: []
|
|
103
|
+
require_paths:
|
|
104
|
+
- lib
|
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '3.2'
|
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - ">="
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '0'
|
|
115
|
+
requirements:
|
|
116
|
+
- none
|
|
117
|
+
rubygems_version: 4.0.2
|
|
118
|
+
specification_version: 4
|
|
119
|
+
summary: Legacy Product Properties for Spree Commerce
|
|
120
|
+
test_files: []
|