Merge branch 'master' into update-to-rails-7.1

This commit is contained in:
Mohamed ABDELLANI
2024-06-19 14:10:25 +01:00
committed by GitHub
903 changed files with 34498 additions and 5340 deletions

12
.env
View File

@@ -10,10 +10,10 @@ TIMEZONE="Melbourne"
DEFAULT_COUNTRY_CODE="AU"
# Locale for translation.
LOCALE="en"
LOCALE="en_AU"
# For multilingual - ENV doesn't have array so pass it as string with commas
AVAILABLE_LOCALES="en,es"
AVAILABLE_LOCALES="en_AU,es"
# Spree zone.
CHECKOUT_ZONE="Australia"
@@ -42,14 +42,6 @@ SMTP_PASSWORD="f00d"
# Javascript error reporting via Bugsnag.
# BUGSNAG_JS_KEY=""
# SingleSignOn login for Discourse
#
# DISCOURSE_SSO_SECRET should be a random string. It must be the same as provided to your Discourse instance.
# DISCOURSE_SSO_SECRET=""
#
# DISCOURSE_URL must be the URL of your Discourse instance.
# DISCOURSE_URL="https://noticeboard.openfoodnetwork.org.au"
# see="https://developers.google.com/maps/documentation/javascript/get-api-key
# GOOGLE_MAPS_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# see https://developers.google.com/maps/documentation/javascript/localization#Region

View File

@@ -5,6 +5,11 @@
#
# cp .env.development .env.local
# Locale for translation. Using a locale other than `en` tests the
# successful fallback to `en`. You will also see up-to-date text used
# in production
LOCALE="en_AU"
VERBOSE_QUERY_LOGS=true
SECRET_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

View File

@@ -1,6 +1,9 @@
# ENV vars for the test environment
# Override locally with `.env.test.local`
# Locale for translation.
LOCALE="en_TEST"
OFN_REDIS_JOBS_URL="redis://localhost:6379/2"
SECRET_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

View File

@@ -45,7 +45,7 @@ The full process is described at https://github.com/openfoodfoundation/openfoodn
[Ready To Go]: https://github.com/orgs/openfoodfoundation/projects/8?filterQuery=status%3A%22Ready+to+go+%F0%9F%9A%80%22
[Transifex pull request]: https://github.com/openfoodfoundation/openfoodnetwork/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aopen+head%3Atransifex
[Draft new release]: https://github.com/openfoodfoundation/openfoodnetwork/releases/new?tag=v&title=v+Code+Name&body=Congrats%0A%0ADescription%0A%0A
[Draft new release]: https://github.com/openfoodfoundation/openfoodnetwork/releases/new?title=v+Code+Name&body=Congrats%0A%0ADescription%0A%0A
[releases]: https://github.com/openfoodfoundation/openfoodnetwork/releases
[#instance-managers]: https://app.slack.com/client/T02G54U79/CG7NJ966B
[#testing]: https://openfoodnetwork.slack.com/app_redirect?channel=C02TZ6X00

View File

@@ -55,6 +55,7 @@ jobs:
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
# JS is required in order for webpacker to compile, in order to render templates containing image urls
- uses: actions/setup-node@v3
with:
node-version-file: .node-version
@@ -64,8 +65,7 @@ jobs:
- name: Set up database
run: |
bundle exec rake db:create
bundle exec rake db:schema:load
bin/rake db:create db:schema:load
- name: Run tests
env:
@@ -83,7 +83,7 @@ jobs:
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/controllers/**/*_spec.rb}"
run: |
git show --no-patch # the commit being tested (which is often a merge due to actions/checkout@v3)
bundle exec rake knapsack_pro:rspec
bin/rake knapsack_pro:rspec
models:
runs-on: ubuntu-22.04
@@ -106,10 +106,10 @@ jobs:
# [n] - where the n is a number of parallel jobs you want to run your tests on.
# Use a higher number if you have slow tests to split them between more parallel jobs.
# Remember to update the value of the `ci_node_index` below to (0..n-1).
ci_node_total: [5]
ci_node_total: [4]
# Indexes for parallel jobs (starting from zero).
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
ci_node_index: [0, 1, 2, 3, 4]
ci_node_index: [0, 1, 2, 3]
steps:
- uses: actions/checkout@v3
@@ -123,20 +123,11 @@ jobs:
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: actions/setup-node@v3
with:
node-version-file: .node-version
- name: Install JS dependencies
run: yarn install --frozen-lockfile
- name: Set up database
run: |
bundle exec rake db:create
bundle exec rake db:schema:load
bin/rake db:create db:schema:load
- name: Run tests
env:
KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC: 09476e2ce491c12083df62768667c674
KNAPSACK_PRO_CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
@@ -150,9 +141,8 @@ jobs:
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/models/**/*_spec.rb}"
run: |
bundle exec rake knapsack_pro:rspec
bin/rake knapsack_pro:rspec
system_admin:
runs-on: ubuntu-22.04
@@ -175,10 +165,10 @@ jobs:
# [n] - where the n is a number of parallel jobs you want to run your tests on.
# Use a higher number if you have slow tests to split them between more parallel jobs.
# Remember to update the value of the `ci_node_index` below to (0..n-1).
ci_node_total: [13]
ci_node_total: [14]
# Indexes for parallel jobs (starting from zero).
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
steps:
- uses: actions/checkout@v3
@@ -201,8 +191,7 @@ jobs:
- name: Set up database
run: |
bundle exec rake db:create
bundle exec rake db:schema:load
bin/rake db:create db:schema:load
- name: Run tests
@@ -221,7 +210,7 @@ jobs:
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/system/admin/**/*_spec.rb}"
run: |
bundle exec rake knapsack_pro:queue:rspec
bin/rake knapsack_pro:queue:rspec
- name: Archive failed tests screenshots
if: failure()
@@ -279,8 +268,7 @@ jobs:
- name: Set up database
run: |
bundle exec rake db:create
bundle exec rake db:schema:load
bin/rake db:create db:schema:load
- name: Run tests
@@ -299,7 +287,7 @@ jobs:
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/system/consumer/**/*_spec.rb}"
run: |
bundle exec rake knapsack_pro:queue:rspec
bin/rake knapsack_pro:queue:rspec
- name: Archive failed tests screenshots
if: failure()
@@ -348,6 +336,7 @@ jobs:
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
# JS is required in order for webpacker to compile, in order to render templates linking to mail.css
- uses: actions/setup-node@v3
with:
node-version-file: .node-version
@@ -357,8 +346,7 @@ jobs:
- name: Set up database
run: |
bundle exec rake db:create
bundle exec rake db:schema:load
bin/rake db:create db:schema:load
- name: Run tests
@@ -377,7 +365,7 @@ jobs:
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/lib/**/*_spec.rb,spec/migrations/**/*_spec.rb,spec/serializers/**/*_spec.rb,engines/**/*_spec.rb}"
run: |
bundle exec rake knapsack_pro:rspec
bin/rake knapsack_pro:rspec
- name: Archive failed tests screenshots
if: failure()
@@ -426,6 +414,7 @@ jobs:
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
# JS is required in order for webpacker to compile, in order to render templates linking to mail.css
- uses: actions/setup-node@v3
with:
node-version-file: .node-version
@@ -435,11 +424,9 @@ jobs:
- name: Set up database
run: |
bundle exec rake db:create
bundle exec rake db:schema:load
bin/rake db:create db:schema:load
- name: Run tests
env:
KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC: e3b8800198d2d89b70c7edbdd85f8fd8
KNAPSACK_PRO_CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
@@ -453,10 +440,8 @@ jobs:
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
KNAPSACK_PRO_TEST_FILE_EXCLUDE_PATTERN: "{engines/**/*_spec.rb,spec/models/**/*_spec.rb,spec/controllers/**/*_spec.rb,spec/serializers/**/*_spec.rb,spec/lib/**/*_spec.rb,spec/migrations/**/*_spec.rb,spec/system/**/*_spec.rb}"
run: |
bundle exec rake knapsack_pro:rspec
bin/rake knapsack_pro:rspec
non_knapsack_jest_karma:
runs-on: ubuntu-22.04
@@ -476,11 +461,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Setup redis
uses: supercharge/redis-github-action@1.4.0
with:
redis-version: 6
# Rails is required for the Karma rake script
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
@@ -493,12 +474,8 @@ jobs:
- name: Install JS dependencies
run: yarn install --frozen-lockfile
- name: Set up database
run: |
bundle exec rake db:create
bundle exec rake db:schema:load
- name: Run JS tests
run: bundle exec rake karma:run
run: bin/rake karma:run
- name: Run jest tests
run: yarn jest

1
.rspec Normal file
View File

@@ -0,0 +1 @@
--require base_spec_helper

View File

@@ -5,8 +5,12 @@
# The configuration is split into three files. Look into those files for more details.
#
require:
- rubocop-capybara
- rubocop-factory_bot
- rubocop-rails
- rubocop-rspec
- rubocop-rspec_rails
inherit_from:
# The automatically generated todo list to ignore all current violations.

View File

@@ -1,15 +1,39 @@
# This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 1400 --no-auto-gen-timestamp`
# using RuboCop version 1.62.1.
# using RuboCop version 1.64.1.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Offense count: 2
Lint/DuplicateMethods:
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, DefLikeMacros, AllowAdjacentOneLineDefs, NumberOfEmptyLines.
Layout/EmptyLineBetweenDefs:
Exclude:
- 'lib/discourse/single_sign_on.rb'
- 'app/services/products_renderer.rb'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
Layout/EmptyLines:
Exclude:
- 'app/services/products_renderer.rb'
# Offense count: 6
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: aligned, indented, indented_relative_to_receiver
Layout/MultilineMethodCallIndentation:
Exclude:
- 'app/services/products_renderer.rb'
# Offense count: 2
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: aligned, indented
Layout/MultilineOperationIndentation:
Exclude:
- 'app/services/products_renderer.rb'
# Offense count: 16
# Configuration parameters: AllowComments, AllowEmptyLambdas.
@@ -27,11 +51,10 @@ Lint/EmptyBlock:
- 'spec/jobs/subscription_placement_job_spec.rb'
- 'spec/models/product_import/entry_validator_spec.rb'
# Offense count: 6
# Offense count: 4
# Configuration parameters: AllowComments.
Lint/EmptyClass:
Exclude:
- 'spec/controllers/spree/admin/base_controller_spec.rb'
- 'spec/lib/reports/report_loader_spec.rb'
# Offense count: 1
@@ -85,7 +108,7 @@ Lint/UselessMethodDefinition:
Exclude:
- 'app/models/spree/gateway.rb'
# Offense count: 26
# Offense count: 24
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
Metrics/AbcSize:
Exclude:
@@ -104,11 +127,9 @@ Metrics/AbcSize:
- 'app/models/spree/order/checkout.rb'
- 'app/models/spree/preferences/preferable_class_methods.rb'
- 'app/models/spree/return_authorization.rb'
- 'lib/discourse/single_sign_on.rb'
- 'lib/open_food_network/order_cycle_form_applicator.rb'
- 'lib/open_food_network/order_cycle_permissions.rb'
- 'lib/spree/core/controller_helpers/order.rb'
- 'lib/tasks/enterprises.rake'
- 'spec/services/orders/checkout_restart_service_spec.rb'
# Offense count: 9
@@ -129,7 +150,7 @@ Metrics/BlockNesting:
Exclude:
- 'app/models/spree/payment/processing.rb'
# Offense count: 46
# Offense count: 47
# Configuration parameters: CountComments, Max, CountAsOne.
Metrics/ClassLength:
Exclude:
@@ -164,6 +185,7 @@ Metrics/ClassLength:
- 'app/models/spree/user.rb'
- 'app/models/spree/variant.rb'
- 'app/models/spree/zone.rb'
- 'app/reflexes/admin/orders_reflex.rb'
- 'app/reflexes/products_reflex.rb'
- 'app/serializers/api/cached_enterprise_serializer.rb'
- 'app/serializers/api/enterprise_shopfront_serializer.rb'
@@ -180,7 +202,7 @@ Metrics/ClassLength:
- 'lib/reporting/reports/enterprise_fee_summary/scope.rb'
- 'lib/reporting/reports/xero_invoices/base.rb'
# Offense count: 34
# Offense count: 32
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
Metrics/CyclomaticComplexity:
Exclude:
@@ -190,7 +212,6 @@ Metrics/CyclomaticComplexity:
- 'app/helpers/checkout_helper.rb'
- 'app/helpers/order_cycles_helper.rb'
- 'app/helpers/spree/admin/navigation_helper.rb'
- 'app/models/enterprise.rb'
- 'app/models/enterprise_relationship.rb'
- 'app/models/product_import/entry_validator.rb'
- 'app/models/spree/ability.rb'
@@ -204,7 +225,6 @@ Metrics/CyclomaticComplexity:
- 'app/models/spree/tax_rate.rb'
- 'app/models/spree/variant.rb'
- 'app/models/spree/zone.rb'
- 'lib/discourse/single_sign_on.rb'
- 'lib/open_food_network/enterprise_issue_validator.rb'
- 'lib/reporting/reports/xero_invoices/base.rb'
- 'lib/spree/core/controller_helpers/order.rb'
@@ -212,7 +232,7 @@ Metrics/CyclomaticComplexity:
- 'lib/spree/localized_number.rb'
- 'spec/models/product_importer_spec.rb'
# Offense count: 25
# Offense count: 24
# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns.
Metrics/MethodLength:
Exclude:
@@ -226,7 +246,6 @@ Metrics/MethodLength:
- 'app/models/spree/order/checkout.rb'
- 'app/models/spree/payment/processing.rb'
- 'app/models/spree/preferences/preferable_class_methods.rb'
- 'lib/discourse/single_sign_on.rb'
- 'lib/open_food_network/order_cycle_form_applicator.rb'
- 'lib/open_food_network/order_cycle_permissions.rb'
- 'lib/reporting/reports/enterprise_fee_summary/scope.rb'
@@ -398,7 +417,6 @@ RSpecRails/HaveHttpStatus:
- 'spec/controllers/user_registrations_controller_spec.rb'
- 'spec/requests/admin/images_spec.rb'
- 'spec/requests/api/routes_spec.rb'
- 'spec/requests/checkout/failed_checkout_spec.rb'
- 'spec/requests/checkout/stripe_sca_spec.rb'
- 'spec/requests/home_controller_spec.rb'
- 'spec/requests/omniauth_callbacks_controller_spec.rb'
@@ -414,7 +432,7 @@ RSpecRails/HttpStatus:
- 'spec/controllers/spree/admin/products_controller_spec.rb'
- 'spec/requests/api/orders_spec.rb'
# Offense count: 146
# Offense count: 144
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Inferences.
RSpecRails/InferredSpecType:
@@ -518,7 +536,6 @@ RSpecRails/InferredSpecType:
- 'spec/helpers/navigation_helper_spec.rb'
- 'spec/helpers/order_cycles_helper_spec.rb'
- 'spec/helpers/serializer_helper_spec.rb'
- 'spec/helpers/shared_helper_spec.rb'
- 'spec/helpers/shop_helper_spec.rb'
- 'spec/helpers/spree/admin/base_helper_spec.rb'
- 'spec/helpers/spree/admin/general_settings_helper_spec.rb'
@@ -552,7 +569,6 @@ RSpecRails/InferredSpecType:
- 'spec/requests/api/routes_spec.rb'
- 'spec/requests/api/v1/customers_spec.rb'
- 'spec/requests/api_docs_spec.rb'
- 'spec/requests/checkout/failed_checkout_spec.rb'
- 'spec/requests/checkout/paypal_spec.rb'
- 'spec/requests/checkout/routes_spec.rb'
- 'spec/requests/checkout/stripe_sca_spec.rb'
@@ -565,17 +581,6 @@ RSpecRails/InferredSpecType:
- 'spec/requests/voucher_adjustments_spec.rb'
- 'spec/routing/stripe_spec.rb'
# Offense count: 11
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/HasManyOrHasOneDependent:
Exclude:
- 'app/models/enterprise.rb'
- 'app/models/spree/address.rb'
- 'app/models/spree/stock_item.rb'
- 'app/models/spree/tax_rate.rb'
- 'app/models/spree/variant.rb'
# Offense count: 22
# Configuration parameters: IgnoreScopes, Include.
# Include: app/models/**/*.rb
@@ -660,27 +665,6 @@ Rails/RedundantActiveRecordAllMethod:
- 'app/models/spree/variant.rb'
- 'spec/system/admin/product_import_spec.rb'
# Offense count: 20
# This cop supports unsafe autocorrection (--autocorrect-all).
Rails/RedundantPresenceValidationOnBelongsTo:
Exclude:
- 'app/models/enterprise_fee.rb'
- 'app/models/exchange.rb'
- 'app/models/inventory_item.rb'
- 'app/models/order_cycle.rb'
- 'app/models/spree/address.rb'
- 'app/models/spree/line_item.rb'
- 'app/models/spree/order.rb'
- 'app/models/spree/product_property.rb'
- 'app/models/spree/return_authorization.rb'
- 'app/models/spree/state.rb'
- 'app/models/spree/stock_item.rb'
- 'app/models/spree/stock_movement.rb'
- 'app/models/spree/tax_rate.rb'
- 'app/models/subscription_line_item.rb'
- 'app/models/tag_rule.rb'
- 'app/models/variant_override.rb'
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
Rails/RelativeDateConstant:
@@ -762,17 +746,6 @@ Rails/UnknownEnv:
Exclude:
- 'app/models/spree/app_configuration.rb'
# Offense count: 7
# Configuration parameters: Severity.
Rails/UnusedRenderContent:
Exclude:
- 'app/controllers/admin/bulk_line_items_controller.rb'
- 'app/controllers/admin/tag_rules_controller.rb'
- 'app/controllers/api/v0/enterprise_fees_controller.rb'
- 'app/controllers/api/v0/products_controller.rb'
- 'app/controllers/api/v0/taxons_controller.rb'
- 'app/controllers/api/v0/variants_controller.rb'
# Offense count: 1
Security/Open:
Exclude:
@@ -797,7 +770,7 @@ Style/CaseEquality:
Exclude:
- 'spec/models/spree/payment_spec.rb'
# Offense count: 25
# Offense count: 23
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: nested, compact
@@ -824,10 +797,9 @@ Style/ClassAndModuleChildren:
- 'app/serializers/api/taxon_serializer.rb'
- 'app/serializers/api/variant_serializer.rb'
- 'lib/open_food_network/locking.rb'
- 'spec/controllers/spree/admin/base_controller_spec.rb'
- 'spec/models/spree/payment_method_spec.rb'
# Offense count: 1
# Offense count: 2
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: always, always_true, never
@@ -877,12 +849,6 @@ Style/HashEachMethods:
- 'spec/models/product_importer_spec.rb'
- 'spec/support/cancan_helper.rb'
# Offense count: 1
# Configuration parameters: MinBranchesCount.
Style/HashLikeCase:
Exclude:
- 'app/models/enterprise.rb'
# Offense count: 4
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/MapToHash:
@@ -971,19 +937,6 @@ Style/RedundantInitialize:
Exclude:
- 'spec/models/spree/gateway_spec.rb'
# Offense count: 2
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/RedundantInterpolation:
Exclude:
- 'lib/tasks/karma.rake'
- 'spec/base_spec_helper.rb'
# Offense count: 8
# This cop supports safe autocorrection (--autocorrect).
Style/RedundantLineContinuation:
Exclude:
- 'lib/reporting/reports/enterprise_fee_summary/scope.rb'
# Offense count: 19
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowedMethods, AllowedPatterns.
@@ -1026,36 +979,10 @@ Style/Send:
- 'spec/services/variant_units/option_value_namer_spec.rb'
- 'spec/support/localized_number_helper.rb'
# Offense count: 4
# Offense count: 3
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/SlicingWithRange:
Exclude:
- 'app/helpers/spree/admin/navigation_helper.rb'
- 'app/services/embedded_page_service.rb'
- 'engines/order_management/app/services/order_management/subscriptions/validator.rb'
- 'lib/discourse/single_sign_on.rb'
# Offense count: 25
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Mode.
Style/StringConcatenation:
Exclude:
- 'app/controllers/admin/stripe_connect_settings_controller.rb'
- 'app/helpers/discourse_helper.rb'
- 'app/helpers/enterprises_helper.rb'
- 'app/helpers/spree/admin/base_helper.rb'
- 'app/mailers/spree/user_mailer.rb'
- 'app/models/enterprise.rb'
- 'app/models/spree/credit_card.rb'
- 'app/models/spree/payment_method.rb'
- 'app/serializers/api/cached_enterprise_serializer.rb'
- 'app/serializers/api/enterprise_shopfront_list_serializer.rb'
- 'app/services/embedded_page_service.rb'
- 'app/services/products_renderer.rb'
- 'lib/spree/api/controller_setup.rb'
- 'lib/spree/core/environment_extension.rb'
- 'spec/models/spree/line_item_spec.rb'
- 'spec/models/spree/product_spec.rb'
- 'spec/services/embedded_page_service_spec.rb'
- 'spec/support/features/datepicker_helper.rb'
- 'spec/system/admin/products_spec.rb'

View File

@@ -19,7 +19,6 @@ gem 'angular-rails-templates', '>= 0.3.0'
gem 'awesome_nested_set'
gem 'ransack', '~> 4.1.0'
gem 'responders'
gem 'rexml'
gem 'webpacker', '~> 5'
gem 'i18n'
@@ -103,8 +102,10 @@ gem 'redis'
gem 'sidekiq'
gem 'sidekiq-scheduler'
gem "cable_ready", "5.0.1"
gem "stimulus_reflex", "3.5.0.rc3"
gem "cable_ready"
gem "stimulus_reflex"
gem "turbo-rails"
gem 'combine_pdf'
gem 'wicked_pdf'
@@ -164,7 +165,7 @@ group :test, :development do
gem 'rspec-sql'
gem 'rswag'
gem 'shoulda-matchers'
gem 'stimulus_reflex_testing'
gem 'stimulus_reflex_testing', github: "podia/stimulus_reflex_testing", branch: :main
gem 'timecop'
end

View File

@@ -17,6 +17,14 @@ GIT
sass-rails
thor (>= 0.14)
GIT
remote: https://github.com/podia/stimulus_reflex_testing.git
revision: abac2ee34de347c589795b4d1a8e83e0baafb201
branch: main
specs:
stimulus_reflex_testing (0.3.1)
stimulus_reflex (>= 3.3.0)
PATH
remote: engines/catalog
specs:
@@ -165,17 +173,17 @@ GEM
awesome_nested_set (3.6.0)
activerecord (>= 4.0.0, < 7.2)
aws-eventstream (1.3.0)
aws-partitions (1.914.0)
aws-sdk-core (3.192.0)
aws-partitions (1.929.0)
aws-sdk-core (3.196.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.79.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (1.81.0)
aws-sdk-core (~> 3, >= 3.193.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.147.0)
aws-sdk-core (~> 3, >= 3.192.0)
aws-sdk-s3 (1.151.0)
aws-sdk-core (~> 3, >= 3.194.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
@@ -194,10 +202,11 @@ GEM
bullet (7.1.6)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
cable_ready (5.0.1)
cable_ready (5.0.5)
actionpack (>= 5.2)
actionview (>= 5.2)
activesupport (>= 5.2)
observer (~> 0.1)
railties (>= 5.2)
thread-local (>= 1.1.0)
cancancan (1.15.0)
@@ -230,14 +239,15 @@ GEM
combine_pdf (1.0.26)
matrix
ruby-rc4 (>= 0.1.5)
concurrent-ruby (1.2.3)
concurrent-ruby (1.3.1)
connection_pool (2.4.1)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
css_parser (1.16.0)
css_parser (1.17.1)
addressable
csv (3.3.0)
cuprite (0.15)
capybara (~> 3.0)
ferrum (~> 0.14.0)
@@ -262,14 +272,14 @@ GEM
warden (~> 1.2.3)
devise-encryptable (0.2.0)
devise (>= 2.1.0)
devise-i18n (1.12.0)
devise-i18n (1.12.1)
devise (>= 4.9.0)
devise-token_authenticatable (1.1.0)
devise (>= 4.0.0, < 5.0.0)
diff-lcs (1.5.1)
digest (3.1.1)
docile (1.4.0)
dotenv (3.1.0)
dotenv (3.1.2)
drb (2.2.0)
ruby2_keywords
email_validator (2.2.4)
@@ -297,16 +307,17 @@ GEM
websocket-driver (>= 0.6, < 0.8)
ffaker (2.23.0)
ffi (1.16.3)
flipper (0.26.2)
flipper (1.3.0)
concurrent-ruby (< 2)
flipper-active_record (0.26.2)
flipper-active_record (1.3.0)
activerecord (>= 4.2, < 8)
flipper (~> 0.26.2)
flipper-ui (0.26.2)
flipper (~> 1.3.0)
flipper-ui (1.3.0)
erubi (>= 1.0.0, < 2.0.0)
flipper (~> 0.26.2)
rack (>= 1.4, < 3)
rack-protection (>= 1.5.3, <= 4.0.0)
flipper (~> 1.3.0)
rack (>= 1.4, < 4)
rack-protection (>= 1.5.3, < 5.0.0)
rack-session (>= 1.0.2, < 3.0.0)
sanitize (< 7)
fog-aws (2.0.1)
fog-core (~> 1.38)
@@ -331,7 +342,9 @@ GEM
fuubar (2.5.1)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
geocoder (1.8.2)
geocoder (1.8.3)
base64 (>= 0.1.0)
csv (>= 3.0.0)
globalid (1.2.1)
activesupport (>= 6.1)
gmaps4rails (2.1.2)
@@ -347,7 +360,7 @@ GEM
hashie (5.0.0)
highline (2.0.3)
htmlentities (4.3.4)
i18n (1.14.4)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
i18n-js (3.9.2)
i18n (>= 0.6.6)
@@ -395,7 +408,7 @@ GEM
activesupport (>= 4.2)
jwt (2.8.1)
base64
knapsack_pro (6.0.4)
knapsack_pro (7.4.0)
rake
language_server-protocol (3.17.0.3)
launchy (3.0.0)
@@ -417,7 +430,7 @@ GEM
net-smtp
marcel (1.0.2)
matrix (0.4.2)
method_source (1.0.0)
method_source (1.1.0)
mime-types (3.5.2)
mime-types-data (~> 3.2015)
mime-types-data (3.2023.1205)
@@ -427,7 +440,7 @@ GEM
mini_magick (4.11.0)
mini_mime (1.1.5)
mini_portile2 (2.8.6)
minitest (5.22.3)
minitest (5.23.1)
monetize (1.13.0)
money (~> 6.12)
money (6.16.0)
@@ -447,22 +460,25 @@ GEM
timeout
net-smtp (0.5.0)
net-protocol
newrelic_rpm (9.8.0)
newrelic_rpm (9.9.0)
nio4r (2.7.0)
nokogiri (1.16.4)
nokogiri (1.16.5)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri-html5-inference (0.3.0)
nokogiri (~> 1.14)
oauth2 (1.4.11)
faraday (>= 0.17.3, < 3.0)
jwt (>= 1.0, < 3.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 4)
observer (0.1.2)
omniauth (2.1.2)
hashie (>= 3.4.6)
rack (>= 2.2.3)
rack-protection
omniauth-rails_csrf_protection (1.0.1)
omniauth-rails_csrf_protection (1.0.2)
actionpack (>= 4.2)
omniauth (~> 2.0)
omniauth_openid_connect (0.7.1)
@@ -490,7 +506,7 @@ GEM
parallel (1.24.0)
paranoia (2.6.3)
activerecord (>= 5.1, < 7.2)
parser (3.3.0.5)
parser (3.3.2.0)
ast (~> 2.4.1)
racc
paypal-sdk-core (0.3.4)
@@ -511,14 +527,14 @@ GEM
method_source (~> 1.0)
psych (5.1.2)
stringio
public_suffix (5.0.4)
public_suffix (5.0.5)
puma (6.4.2)
nio4r (~> 2.0)
query_count (1.1.1)
activerecord (>= 4.2)
railties (>= 4.2)
raabro (1.4.0)
racc (1.7.3)
racc (1.8.0)
rack (2.2.9)
rack-mini-profiler (2.3.4)
rack (>= 1.2.0)
@@ -602,9 +618,9 @@ GEM
redcarpet (3.6.0)
redis (5.2.0)
redis-client (>= 0.22.0)
redis-client (0.22.0)
redis-client (0.22.1)
connection_pool
regexp_parser (2.9.0)
regexp_parser (2.9.2)
reline (0.5.0)
io-console (~> 0.5)
request_store (1.5.1)
@@ -612,11 +628,12 @@ GEM
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
rexml (3.2.6)
roadie (5.2.0)
rexml (3.2.8)
strscan (>= 3.0.9)
roadie (5.2.1)
css_parser (~> 1.4)
nokogiri (~> 1.15)
roadie-rails (3.1.0)
roadie-rails (3.2.0)
railties (>= 5.1, < 8.0)
roadie (~> 5.0)
rodf (1.2.0)
@@ -666,7 +683,7 @@ GEM
rswag-ui (2.13.0)
actionpack (>= 3.1, < 7.2)
railties (>= 3.1, < 7.2)
rubocop (1.63.2)
rubocop (1.64.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
@@ -677,8 +694,8 @@ GEM
rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.31.2)
parser (>= 3.3.0.4)
rubocop-ast (1.31.3)
parser (>= 3.3.1.0)
rubocop-capybara (2.20.0)
rubocop (~> 1.41)
rubocop-factory_bot (2.25.1)
@@ -688,12 +705,12 @@ GEM
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (2.29.1)
rubocop-rspec (2.29.2)
rubocop (~> 1.40)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
rubocop-rspec_rails (~> 2.28)
rubocop-rspec_rails (2.28.2)
rubocop-rspec_rails (2.28.3)
rubocop (~> 1.40)
ruby-graphviz (1.2.5)
rexml
@@ -705,7 +722,7 @@ GEM
rubyzip (2.3.2)
rufus-scheduler (3.8.2)
fugit (~> 1.1, >= 1.1.6)
sanitize (6.0.2)
sanitize (6.1.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
sass (3.4.25)
@@ -719,7 +736,7 @@ GEM
semantic_range (3.0.0)
shoulda-matchers (6.2.0)
activesupport (>= 5.2.0)
sidekiq (7.2.2)
sidekiq (7.2.4)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
@@ -737,7 +754,7 @@ GEM
spreadsheet_architect (5.0.0)
caxlsx (>= 3.3.0, < 4)
rodf (>= 1.0.0, < 2)
spring (4.2.0)
spring (4.2.1)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
spring-commands-rubocop (0.4.0)
@@ -756,21 +773,21 @@ GEM
state_machines-activerecord (0.9.0)
activerecord (>= 6.0)
state_machines-activemodel (>= 0.9.0)
stimulus_reflex (3.5.0.rc3)
stimulus_reflex (3.5.1)
actioncable (>= 5.2, < 8)
actionpack (>= 5.2, < 8)
actionview (>= 5.2, < 8)
activesupport (>= 5.2, < 8)
cable_ready (~> 5.0)
nokogiri (~> 1.0)
nokogiri-html5-inference (~> 0.3)
rack (>= 2, < 4)
railties (>= 5.2, < 8)
redis (>= 4.0, < 6.0)
stimulus_reflex_testing (0.3.0)
stimulus_reflex (>= 3.3.0)
stringex (2.8.6)
stringio (3.1.0)
stripe (11.1.0)
strscan (3.1.0)
swd (2.0.3)
activesupport (>= 3)
attr_required (>= 0.0.5)
@@ -783,6 +800,10 @@ GEM
timecop (0.9.8)
timeout (0.4.1)
ttfunk (1.7.0)
turbo-rails (2.0.5)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
@@ -797,7 +818,7 @@ GEM
validates_lengths_from_database (0.8.0)
activerecord (>= 4)
vcr (6.2.0)
view_component (3.12.0)
view_component (3.12.1)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
method_source (~> 1.0)
@@ -818,7 +839,7 @@ GEM
activesupport
faraday (~> 2.0)
faraday-follow_redirects
webmock (3.23.0)
webmock (3.23.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -835,11 +856,11 @@ GEM
chronic (>= 0.6.3)
wicked_pdf (2.6.3)
activesupport
wkhtmltopdf-binary (0.12.6.6)
wkhtmltopdf-binary (0.12.6.7)
xml-simple (1.1.8)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.13)
zeitwerk (2.6.15)
PLATFORMS
ruby
@@ -865,7 +886,7 @@ DEPENDENCIES
bootsnap
bugsnag
bullet
cable_ready (= 5.0.1)
cable_ready
cancancan (~> 1.15.0)
capybara
catalog!
@@ -944,7 +965,6 @@ DEPENDENCIES
redcarpet
redis
responders
rexml
roadie-rails
roo
rspec-rails (>= 3.5.2)
@@ -967,11 +987,12 @@ DEPENDENCIES
spring-commands-rspec
spring-commands-rubocop
state_machines-activerecord
stimulus_reflex (= 3.5.0.rc3)
stimulus_reflex_testing
stimulus_reflex
stimulus_reflex_testing!
stringex (~> 2.8.5)
stripe
timecop
turbo-rails
valid_email2
validates_lengths_from_database
vcr

View File

@@ -31,7 +31,7 @@ angular.module("admin.indexUtils").factory 'Columns', ($rootScope, $http, $injec
savePreferences: (action_name) =>
$http
method: "PUT"
url: "/admin/column_preferences/bulk_update"
url: "/admin/column_preferences/bulk_update.json"
data:
action_name: action_name
column_preferences: (preference for column_name, preference of @columns)

View File

@@ -42,8 +42,17 @@ angular.module('Darkswarm').controller "RegistrationCtrl", ($scope, Registration
$scope.toggleAddressConfirmed = ->
$scope.addressConfirmed = !$scope.addressConfirmed
if $scope.addressConfirmed
$scope.setLatLongIfUsingOpenStreetMap()
$scope.enterprise.address.latitude = $scope.latLong.latitude
$scope.enterprise.address.longitude = $scope.latLong.longitude
else
$scope.enterprise.address.latitude = null
$scope.enterprise.address.longitude = null
# When OpenStreetMaps is enabled the latitude and longitude are calculated via a Stimulus
# controller, so they need to be read from data properties to be accessible here.
$scope.setLatLongIfUsingOpenStreetMap = ->
openStreetMap = document.getElementById("open-street-map")
if !$scope.latLong && openStreetMap && openStreetMap.dataset.latitude && openStreetMap.dataset.longitude
$scope.latLong = { latitude: openStreetMap.dataset.latitude, longitude: openStreetMap.dataset.longitude }

View File

@@ -5,8 +5,9 @@
%div.menu{ 'ng-show' => "expanded" }
.menu_items
.menu_item{ "ng-repeat": "column in columns", "ng-click": "toggle(column);" }
%input.redesigned-input{ type: "checkbox", "ng-checked": "column.visible" }
{{ column.name }}
%input{ type: "checkbox", "ng-checked": "column.visible" }
%span
{{ column.name }}
%hr
%div.menu_item.text-center
%input.fullwidth.orange{ type: "button", "ng-value": "saved() ? 'Saved': 'Saving'", "ng-show": "saved() || saving", "ng-disabled": "saved()" }

View File

@@ -2,6 +2,6 @@
class HelpModalComponent < ModalComponent
def initialize(id:, close_button: true)
super(id:, close_button:)
super
end
end

View File

@@ -9,5 +9,5 @@
%div.menu_items
- @options.each do |option|
%label.menu_item{ "data-multiple-checked-select-target": "option", "data-value": option[1], "data-label": option[0] }
%input.redesigned-input{ type: "checkbox", checked: @selected.include?(option[1]), name: "#{@name}[]", value: option[1] }
= option[0]
%input{ type: "checkbox", checked: @selected.include?(option[1]), name: "#{@name}[]", value: option[1] }
%span= option[0]

View File

@@ -0,0 +1,43 @@
# frozen_string_literal: true
class SearchableDropdownComponent < ViewComponent::Base
REMOVED_SEARCH_PLUGIN = { 'tom-select-options-value': '{ "plugins": [] }' }.freeze
MINIMUM_OPTIONS_FOR_SEARCH_FIELD = 11 # at least 11 options are required for the search field
def initialize(
form:,
name:,
options:,
selected_option:,
placeholder_value:,
include_blank: false,
aria_label: ''
)
@f = form
@name = name
@options = options
@selected_option = selected_option
@placeholder_value = placeholder_value
@include_blank = include_blank
@aria_label = aria_label
end
private
attr_reader :f, :name, :options, :selected_option, :placeholder_value, :include_blank, :aria_label
def classes
"fullwidth #{remove_search_plugin? ? 'no-input' : ''}"
end
def data
{
controller: "tom-select",
'tom-select-placeholder-value': placeholder_value
}.merge(remove_search_plugin? ? REMOVED_SEARCH_PLUGIN : {})
end
def remove_search_plugin?
@remove_search_plugin ||= options.count < MINIMUM_OPTIONS_FOR_SEARCH_FIELD
end
end

View File

@@ -0,0 +1 @@
= f.select name, options_for_select(options, selected_option), { include_blank: }, class: classes, data:, 'aria-label': aria_label

View File

@@ -35,7 +35,7 @@ module Admin
order.with_lock do
if order.contents.update_item(@line_item, line_item_params)
# No Content, does not trigger ng resource auto-update
render body: nil, status: :no_content
head :no_content
else
render json: { errors: @line_item.errors }, status: :precondition_failed
end
@@ -49,7 +49,7 @@ module Admin
authorize! :update, order
order.contents.remove(@line_item.variant)
render body: nil, status: :no_content # No Content, does not trigger ng resource auto-update
head :no_content # No Content, does not trigger ng resource auto-update
end
private

View File

@@ -4,17 +4,25 @@ module Admin
class ColumnPreferencesController < Admin::ResourceController
before_action :load_collection, only: [:bulk_update]
respond_to :json
def bulk_update
@cp_set.collection.each { |cp| authorize! :bulk_update, cp }
if @cp_set.save
render json: @cp_set.collection, each_serializer: Api::Admin::ColumnPreferenceSerializer
elsif @cp_set.errors.present?
render json: { errors: @cp_set.errors }, status: :bad_request
else
render body: nil, status: :internal_server_error
respond_to do |format|
if @cp_set.save
format.json {
render json: @cp_set.collection, each_serializer: Api::Admin::ColumnPreferenceSerializer
}
format.turbo_stream {
flash.now[:success] = t('.success')
render :bulk_update, locals: { action: permitted_params[:action_name] }
}
else
format.json { render json: { errors: @cp_set.errors }, status: :bad_request }
format.turbo_stream {
flash.now[:error] = @cp_set.errors.full_messages.to_sentence
render :bulk_update, locals: { action: permitted_params[:action_name] }
}
end
end
end
@@ -28,11 +36,26 @@ module Admin
end
def load_collection
collection_hash = Hash[permitted_params[:column_preferences].
each_with_index.map { |cp, i| [i, cp] }]
collection_hash.select!{ |_i, cp| cp[:action_name] == permitted_params[:action_name] }
@cp_set = Sets::ColumnPreferenceSet.new(@column_preferences,
collection_attributes: collection_hash)
collection_attributes = nil
respond_to do |format|
format.json do
collection_attributes = Hash[permitted_params[:column_preferences].
each_with_index.map { |cp, i| [i, cp] }]
collection_attributes.select!{ |_i, cp|
cp[:action_name] == permitted_params[:action_name]
}
end
format.all do
# Inject action name and user ID for each column_preference
collection_attributes = permitted_params[:column_preferences].to_h.each_value { |cp|
cp[:action_name] = permitted_params[:action_name]
cp[:user_id] = spree_current_user.id
}
end
end
@cp_set = Sets::ColumnPreferenceSet.new(@column_preferences, collection_attributes:)
end
def collection

View File

@@ -0,0 +1,43 @@
# frozen_string_literal: true
module Admin
class ConnectedAppsController < ApplicationController
def create
authorize! :admin, enterprise
app = ConnectedApp.create!(enterprise_id: enterprise.id)
ConnectAppJob.perform_later(
app, spree_current_user.spree_api_key,
channel: SessionChannel.for_request(request),
)
render_panel
end
def destroy
authorize! :admin, enterprise
app = enterprise.connected_apps.first
app.destroy
WebhookDeliveryJob.perform_later(
app.data["destroy"],
"disconnect-app",
nil
)
render_panel
end
private
def enterprise
@enterprise ||= Enterprise.find(params.require(:enterprise_id))
end
def render_panel
redirect_to "#{edit_admin_enterprise_path(enterprise)}#/connected_apps_panel"
end
end
end

View File

@@ -20,7 +20,7 @@ module Admin
catalog_url = params.require(:catalog_url)
json_catalog = DfcRequest.new(spree_current_user).get(catalog_url)
json_catalog = fetch_catalog(catalog_url)
graph = DfcIo.import(json_catalog)
# * First step: import all products for given enterprise.
@@ -34,6 +34,16 @@ module Admin
private
def fetch_catalog(url)
if url =~ /food-data-collaboration/
fdc_json = FdcRequest.new(spree_current_user).call(url)
fdc_message = JSON.parse(fdc_json)
fdc_message["products"]
else
DfcRequest.new(spree_current_user).call(url)
end
end
# Most of this code is the same as in the DfcProvider::SuppliedProductsController.
def import_product(subject, enterprise)
return unless subject.is_a? DataFoodConsortium::Connector::SuppliedProduct

View File

@@ -1,13 +1,16 @@
# frozen_string_literal: true
# rubocop:disable Metrics/ClassLength
module Admin
class ProductsV3Controller < Spree::Admin::BaseController
helper ProductsHelper
before_action :init_filters_params
before_action :init_pagination_params
def index
fetch_products
render "index", locals: { producers:, categories:, flash: }
render "index", locals: { producers:, categories:, tax_category_options:, flash: }
end
def bulk_update
@@ -24,7 +27,44 @@ module Admin
elsif product_set.errors.present?
@error_counts = { saved: product_set.saved_count, invalid: product_set.invalid.count }
render "index", status: :unprocessable_entity, locals: { producers:, categories:, flash: }
render "index", status: :unprocessable_entity,
locals: { producers:, categories:, tax_category_options:, flash: }
end
end
def destroy
@record = ProductScopeQuery.new(
spree_current_user,
{ id: params[:id] }
).find_product
status = :ok
if @record.destroy
flash.now[:success] = t('.delete_product.success')
else
flash.now[:error] = t('.delete_product.error')
status = :internal_server_error
end
respond_with do |format|
format.turbo_stream { render :destroy_product_variant, status: }
end
end
def destroy_variant
@record = Spree::Variant.active.find(params[:id])
authorize! :delete, @record
status = :ok
if VariantDeleter.new.delete(@record)
flash.now[:success] = t('.delete_variant.success')
else
flash.now[:error] = t('.delete_variant.error')
status = :internal_server_error
end
respond_with do |format|
format.turbo_stream { render :destroy_product_variant, status: }
end
end
@@ -47,6 +87,7 @@ module Admin
# prority is given to element dataset (if present) over url params
@page = params[:page].presence || 1
@per_page = params[:per_page].presence || 15
@q = params.permit(q: {})[:q] || { s: 'name asc' }
end
def producers
@@ -59,6 +100,10 @@ module Admin
Spree::Taxon.order(:name).map { |c| [c.name, c.id] }
end
def tax_category_options
Spree::TaxCategory.order(:name).pluck(:name, :id)
end
def fetch_products
product_query = OpenFoodNetwork::Permissions.new(spree_current_user)
.editable_products.merge(product_scope).ransack(ransack_query).result
@@ -84,6 +129,8 @@ module Admin
query.merge!(Spree::Variant::SEARCH_KEY => @search_term)
end
query.merge!(variants_primary_taxon_id_in: @category_id) if @category_id.present?
query.merge!(@q) if @q
query
end
@@ -137,3 +184,4 @@ module Admin
end
end
end
# rubocop:enable Metrics/ClassLength

View File

@@ -146,7 +146,7 @@ module Admin
return nil if parent_data.blank?
@parent ||= parent_data[:model_class].
public_send("find_by", parent_data[:find_by] => params["#{model_name}_id"])
find_by(parent_data[:find_by] => params["#{model_name}_id"])
instance_variable_set("@#{model_name}", @parent)
end

View File

@@ -37,7 +37,7 @@ module Admin
def obfuscated_secret_key
key = Stripe.api_key
key.first(8) + "****" + key.last(4)
"#{key.first(8)}****#{key.last(4)}"
end
def settings_params

View File

@@ -5,7 +5,7 @@ module Admin
respond_to :json
respond_override destroy: { json: {
success: lambda { render body: nil, status: :no_content }
success: lambda { head :no_content }
} }
def map_by_tag

View File

@@ -9,7 +9,7 @@ module Api
authorize! :destroy, enterprise_fee
if enterprise_fee.destroy
render plain: I18n.t(:successfully_removed), status: :no_content
head :no_content
else
render plain: enterprise_fee.errors.full_messages.first, status: :forbidden
end

View File

@@ -42,8 +42,9 @@ module Api
authorize! :delete, Spree::Product
@product = product_finder.find_product
authorize! :delete, @product
@product.destroyed_by = current_api_user
@product.destroy
render json: @product, serializer: Api::Admin::ProductSerializer, status: :no_content
head :no_content
end
def bulk_products

View File

@@ -55,7 +55,7 @@ module Api
def destroy
authorize! :delete, Spree::Taxon
taxon.destroy
render json: taxon, serializer: Api::TaxonSerializer, status: :no_content
head :no_content
end
private

View File

@@ -44,7 +44,7 @@ module Api
authorize! :delete, @variant
VariantDeleter.new.delete(@variant)
render json: @variant, serializer: Api::VariantSerializer, status: :no_content
head :no_content
end
private

View File

@@ -28,7 +28,6 @@ class ApplicationController < ActionController::Base
helper 'injection'
helper 'markdown'
helper 'footer_links'
helper 'discourse'
helper 'checkout'
helper 'link'
helper 'terms_and_conditions'
@@ -61,7 +60,7 @@ class ApplicationController < ActionController::Base
rescue StandardError
'unknown'
end}")
super(options, response_status)
super
end
def set_checkout_redirect

View File

@@ -1,52 +0,0 @@
# frozen_string_literal: true
require 'discourse/single_sign_on'
class DiscourseSsoController < ApplicationController
include SharedHelper
include DiscourseHelper
before_action :require_config
def login
if require_activation?
redirect_to discourse_url
else
redirect_to discourse_login_url
end
end
def sso
if spree_current_user
begin
redirect_to sso_url
rescue TypeError
render plain: "Bad SingleSignOn request.", status: :bad_request
end
else
redirect_to login_path
end
end
private
def sso_url
secret = discourse_sso_secret!
sso = Discourse::SingleSignOn.parse(request.query_string, secret)
sso.email = spree_current_user.email
sso.username = spree_current_user.login
sso.external_id = spree_current_user.id
sso.sso_secret = secret
sso.admin = admin_user?
sso.require_activation = require_activation?
sso.to_url(discourse_sso_url)
end
def require_config
raise ActionController::RoutingError, 'Not Found' unless discourse_configured?
end
def require_activation?
!admin_user? && !spree_current_user.confirmed?
end
end

View File

@@ -3,6 +3,8 @@
module Spree
module Admin
class ImagesController < ::Admin::ResourceController
helper ::Admin::ProductsHelper
# This will make resource controller redirect correctly after deleting product images.
# This can be removed after upgrading to Spree 2.1.
# See here https://github.com/spree/spree/commit/334a011d2b8e16355e4ae77ae07cd93f7cbc8fd1
@@ -17,7 +19,10 @@ module Spree
def new
@url_filters = ::ProductFilters.new.extract(request.query_parameters)
render layout: !request.xhr?
respond_with do |format|
format.turbo_stream { render :edit }
format.all { render layout: !request.xhr? }
end
end
def edit
@@ -32,13 +37,14 @@ module Spree
if @object.save
flash[:success] = flash_message_for(@object, :successfully_created)
redirect_to location_after_save
respond_to do |format|
format.html { redirect_to location_after_save }
format.turbo_stream { render :update }
end
else
respond_with(@object)
respond_with_error(@object.errors)
end
rescue ActiveStorage::IntegrityError
@object.errors.add :attachment, :integrity_error
respond_with(@object)
end
def update
@@ -47,13 +53,14 @@ module Spree
if @object.update(permitted_resource_params)
flash[:success] = flash_message_for(@object, :successfully_updated)
redirect_to location_after_save
respond_to do |format|
format.html { redirect_to location_after_save }
format.turbo_stream
end
else
respond_with(@object)
respond_with_error(@object.errors)
end
rescue ActiveStorage::IntegrityError
@object.errors.add :attachment, :integrity_error
respond_with(@object)
end
def destroy
@@ -103,6 +110,14 @@ module Spree
:attachment, :viewable_id, :alt
)
end
def respond_with_error(errors)
@errors = errors.map(&:full_message)
respond_to do |format|
format.html { respond_with(@object) }
format.turbo_stream { render :edit }
end
end
end
end
end

View File

@@ -19,8 +19,13 @@ module Spree
def generate
@order = Order.find_by(number: params[:order_id])
authorize! :invoice, @order
::Orders::GenerateInvoiceService.new(@order).generate_or_update_latest_invoice
if @order.distributor.can_invoice?
authorize! :invoice, @order
::Orders::GenerateInvoiceService.new(@order).generate_or_update_latest_invoice
else
flash[:error] = t(:must_have_valid_business_number,
enterprise_name: @order.distributor.name)
end
redirect_back(fallback_location: spree.admin_dashboard_path)
end

View File

@@ -14,6 +14,10 @@ module Admin
producers.size == 1 ? producers.first.id : nil
end
def managed_by_user?(enterprise)
enterprise.in?(spree_current_user.enterprises)
end
def enterprise_side_menu_items(enterprise)
is_shop = enterprise.sells != "none"
show_properties = !!enterprise.is_primary_producer

View File

@@ -11,9 +11,25 @@ module Admin
def order_adjustments_for_display(order)
order.adjustments +
voucher_included_tax_representations(order) +
additional_tax_total_representation(order) +
order.all_adjustments.payment_fee.eligible
end
def additional_tax_total_representation(order)
adjustment = Spree::Adjustment.additional.tax.where(
order_id: order.id, adjustable_type: 'Spree::Adjustment'
).sum(:amount)
return [] unless adjustment != 0
[
AdjustmentData.new(
I18n.t("admin.orders.edit.tax_on_fees"),
adjustment
)
]
end
def voucher_included_tax_representations(order)
return [] unless VoucherAdjustmentsService.new(order).voucher_included_tax.negative?

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
module Admin
module ProductsHelper
def product_image_form_path(product)
if product.image.present?
edit_admin_product_image_path(product.id, product.image.id)
else
new_admin_product_image_path(product.id)
end
end
end
end

View File

@@ -8,6 +8,6 @@ class BulkFormBuilder < ActionView::Helpers::FormBuilder
opts[:class] = "#{opts[:class]} changed".strip
end
super(field, **opts)
super
end
end

View File

@@ -1,27 +0,0 @@
# frozen_string_literal: true
module DiscourseHelper
def discourse_configured?
discourse_url.present?
end
def discourse_url
ENV.fetch('DISCOURSE_URL', nil)
end
def discourse_login_url
discourse_url + '/login'
end
def discourse_sso_url
discourse_url + '/session/sso_login'
end
def discourse_url!
discourse_url || raise('Missing Discourse URL')
end
def discourse_sso_secret!
ENV['DISCOURSE_SSO_SECRET'] || raise('Missing SSO secret')
end
end

View File

@@ -31,7 +31,7 @@ module EnterprisesHelper
def enterprises_options(enterprises)
enterprises.map { |enterprise|
[enterprise.name + ": " + enterprise.address.address1 + ", " + enterprise.address.city,
["#{enterprise.name}: #{enterprise.address.address1}, #{enterprise.address.city}",
enterprise.id.to_i]
}
end

View File

@@ -2,7 +2,9 @@
module MapHelper
def using_google_maps?
ENV["GOOGLE_MAPS_API_KEY"].present? || google_maps_configured_with_geocoder_api_key?
!ContentConfig.open_street_map_enabled && (
ENV["GOOGLE_MAPS_API_KEY"].present? || google_maps_configured_with_geocoder_api_key?
)
end
private

View File

@@ -108,7 +108,7 @@ module Spree
object.preferences.keys.map { |key|
preference_label = form.label("preferred_#{key}",
Spree.t(key.to_s.gsub("_from_list", "")) + ": ")
"#{Spree.t(key.to_s.gsub('_from_list', ''))}: ")
preference_field = preference_field_for(
form,
"preferred_#{key}",
@@ -120,7 +120,7 @@ module Spree
def link_to_add_fields(name, target, options = {})
name = '' if options[:no_text]
css_classes = options[:class] ? options[:class] + " spree_add_fields" : "spree_add_fields"
css_classes = options[:class] ? "#{options[:class]} spree_add_fields" : "spree_add_fields"
link_to_with_icon('icon-plus',
name,
'javascript:',

View File

@@ -34,6 +34,10 @@ module Spree
links
end
def order_shipment_ready?(order)
order.ready_to_ship?
end
private
def complete_order_links(order)

View File

@@ -17,7 +17,7 @@ class ConnectAppJob < ApplicationJob
return unless channel
selector = "#edit_enterprise_#{enterprise.id} #connected-app-discover-regen"
selector = "#connected-app-discover-regen.enterprise_#{enterprise.id}"
html = ApplicationController.render(
partial: "admin/enterprises/form/connected_apps",
locals: { enterprise: },

View File

@@ -25,7 +25,7 @@ module Spree
@user = user
I18n.with_locale valid_locale(@user) do
mail(to: user.email,
subject: t(:welcome_to) + ' ' + Spree::Config[:site_name])
subject: "#{t(:welcome_to)} #{Spree::Config[:site_name]}")
end
end

View File

@@ -22,7 +22,7 @@ module FilePreferences
if has_preference?("#{key}_blob_id")
:file
else
super(key)
super
end
end

View File

@@ -0,0 +1,18 @@
# frozen_string_literal: true
require 'active_support/concern'
module LogDestroyPerformer
extend ActiveSupport::Concern
included do
attr_accessor :destroyed_by
after_destroy :log_who_destroyed
def log_who_destroyed
return if destroyed_by.nil?
Rails.logger.info "#{self.class} #{id} deleted by #{destroyed_by.id}"
end
end
end

View File

@@ -43,25 +43,31 @@ class Enterprise < ApplicationRecord
foreign_key: 'supplier_id',
dependent: :destroy
has_many :supplied_variants, through: :supplied_products, source: :variants
has_many :distributed_orders, class_name: 'Spree::Order', foreign_key: 'distributor_id'
has_many :distributed_orders, class_name: 'Spree::Order',
foreign_key: 'distributor_id',
dependent: :restrict_with_exception
belongs_to :address, class_name: 'Spree::Address'
belongs_to :business_address, optional: true, class_name: 'Spree::Address', dependent: :destroy
has_many :enterprise_fees
has_many :enterprise_fees, dependent: :restrict_with_exception
has_many :enterprise_roles, dependent: :destroy
has_many :users, through: :enterprise_roles
belongs_to :owner, class_name: 'Spree::User',
inverse_of: :owned_enterprises
has_many :distributor_payment_methods,
inverse_of: :distributor, foreign_key: :distributor_id
inverse_of: :distributor,
foreign_key: :distributor_id,
dependent: :restrict_with_exception
has_many :distributor_shipping_methods,
inverse_of: :distributor, foreign_key: :distributor_id
inverse_of: :distributor,
foreign_key: :distributor_id,
dependent: :restrict_with_exception
has_many :payment_methods, through: :distributor_payment_methods
has_many :shipping_methods, through: :distributor_shipping_methods
has_many :customers, dependent: :destroy
has_many :inventory_items, dependent: :destroy
has_many :tag_rules, dependent: :destroy
has_one :stripe_account, dependent: :destroy
has_many :vouchers
has_many :vouchers, dependent: :restrict_with_exception
has_many :connected_apps, dependent: :destroy
has_one :custom_tab, dependent: :destroy
@@ -128,6 +134,7 @@ class Enterprise < ApplicationRecord
after_create :set_default_contact
after_create :relate_to_owners_enterprises
after_rollback :restore_permalink
after_touch :touch_distributors
after_create_commit :send_welcome_email
@@ -240,6 +247,16 @@ class Enterprise < ApplicationRecord
count(distinct: true)
end
# Remove any unsupported HTML.
def long_description
HtmlSanitizer.sanitize(super)
end
# Remove any unsupported HTML.
def long_description=(html)
super(HtmlSanitizer.sanitize(html))
end
def contact
contact = users.where(enterprise_roles: { receives_notifications: true }).first
contact || owner
@@ -359,7 +376,7 @@ class Enterprise < ApplicationRecord
def category
# Make this crazy logic human readable so we can argue about it sanely.
cat = is_primary_producer ? "producer_" : "non_producer_"
cat << ("sells_" + sells)
cat << ("sells_#{sells}")
# Map backend cases to front end cases.
case cat
@@ -493,7 +510,7 @@ class Enterprise < ApplicationRecord
end
def correct_whatsapp_url(phone_number)
phone_number && ("https://wa.me/" + phone_number.tr('+ ', ''))
phone_number && "https://wa.me/#{phone_number.tr('+ ', '')}"
end
def correct_instagram_url(url)

View File

@@ -21,7 +21,6 @@ class EnterpriseFee < ApplicationRecord
validates :fee_type, inclusion: { in: FEE_TYPES }
validates :name, presence: true
validates :enterprise_id, presence: true
before_save :ensure_valid_tax_category_settings

View File

@@ -74,6 +74,16 @@ class EnterpriseGroup < ApplicationRecord
permalink
end
# Remove any unsupported HTML.
def long_description
HtmlSanitizer.sanitize(super)
end
# Remove any unsupported HTML.
def long_description=(html)
super(HtmlSanitizer.sanitize(html))
end
private
def sanitize_permalink

View File

@@ -10,8 +10,6 @@
# shopfront (outgoing products). But the set of shown products can be smaller
# than all incoming products.
class Exchange < ApplicationRecord
self.belongs_to_required_by_default = false
acts_as_taggable
belongs_to :order_cycle
@@ -24,7 +22,6 @@ class Exchange < ApplicationRecord
has_many :exchange_fees, dependent: :destroy
has_many :enterprise_fees, through: :exchange_fees
validates :order_cycle, :sender, :receiver, presence: true
validates :sender_id, uniqueness: { scope: [:order_cycle_id, :receiver_id, :incoming] }
before_destroy :delete_related_exchange_variants, prepend: true

View File

@@ -1,14 +1,10 @@
# frozen_string_literal: true
class InventoryItem < ApplicationRecord
self.belongs_to_required_by_default = false
belongs_to :enterprise
belongs_to :variant, class_name: "Spree::Variant"
validates :variant_id, uniqueness: { scope: :enterprise_id }
validates :enterprise, presence: true
validates :variant, presence: true
validates :visible,
inclusion: { in: [true, false], message: I18n.t(:inventory_item_visibility_error) }

View File

@@ -3,8 +3,6 @@
require 'open_food_network/scope_variant_to_hub'
class OrderCycle < ApplicationRecord
self.belongs_to_required_by_default = false
searchable_attributes :orders_open_at, :orders_close_at, :coordinator_id
searchable_scopes :active, :inactive, :active_or_complete, :upcoming, :closed, :not_closed,
:dated, :undated, :soonest_opening, :soonest_closing, :most_recently_closed
@@ -44,7 +42,7 @@ class OrderCycle < ApplicationRecord
before_update :reset_processed_at, if: :will_save_change_to_orders_close_at?
after_save :sync_subscriptions, if: :opening?
validates :name, :coordinator_id, presence: true
validates :name, presence: true
validate :orders_close_at_after_orders_open_at?
preference :product_selection_from_coordinator_inventory_only, :boolean, default: false
@@ -255,7 +253,7 @@ class OrderCycle < ApplicationRecord
end
def pickup_time_for(distributor)
exchange_for_distributor(distributor)&.pickup_time || distributor.next_collection_at
exchange_for_distributor(distributor)&.pickup_time
end
def pickup_instructions_for(distributor)

View File

@@ -192,7 +192,7 @@ module Spree
OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include? product.supplier
end
can [:admin, :index], :products_v3
can [:admin, :index, :bulk_update, :destroy, :destroy_variant], :products_v3
can [:create], Spree::Variant
can [:admin, :index, :read, :edit,

View File

@@ -4,19 +4,17 @@ module Spree
class Address < ApplicationRecord
include AddressDisplay
self.belongs_to_required_by_default = false
searchable_attributes :firstname, :lastname, :phone, :full_name, :full_name_reversed,
:full_name_with_comma, :full_name_with_comma_reversed
searchable_associations :country, :state
belongs_to :country, class_name: "Spree::Country"
belongs_to :state, class_name: "Spree::State"
belongs_to :state, class_name: "Spree::State", optional: true
has_one :enterprise, dependent: :restrict_with_exception
has_many :shipments
has_many :shipments, dependent: :restrict_with_exception
validates :address1, :city, :country, :phone, presence: true
validates :address1, :city, :phone, presence: true
validates :company, presence: true, unless: -> { first_name.blank? || last_name.blank? }
validates :firstname, :lastname, presence: true, if: -> do
company.blank? || company == 'unused'

View File

@@ -30,7 +30,7 @@ module Spree
def expiry=(expiry)
self[:month], self[:year] = expiry.split(" / ")
self[:year] = "20" + self[:year]
self[:year] = "20#{self[:year]}"
end
def number=(num)

View File

@@ -7,8 +7,6 @@ module Spree
include VariantUnits::VariantAndLineItemNaming
include LineItemStockChanges
self.belongs_to_required_by_default = false
searchable_attributes :price, :quantity, :order_id, :variant_id, :tax_category_id
searchable_associations :order, :order_cycle, :variant, :product, :supplier, :tax_category
searchable_scopes :with_tax, :without_tax
@@ -19,7 +17,7 @@ module Spree
belongs_to :variant, -> { with_deleted }, class_name: "Spree::Variant"
has_one :product, through: :variant
has_one :supplier, through: :product
belongs_to :tax_category, class_name: "Spree::TaxCategory"
belongs_to :tax_category, class_name: "Spree::TaxCategory", optional: true
has_many :adjustments, as: :adjustable, dependent: :destroy
@@ -28,7 +26,6 @@ module Spree
before_validation :copy_tax_category
before_validation :copy_dimensions
validates :variant, presence: true
validates :quantity, numericality: {
only_integer: true,
greater_than: -1,

View File

@@ -8,8 +8,6 @@ module Spree
include Balance
include SetUnusedAddressFields
self.belongs_to_required_by_default = false
searchable_attributes :number, :state, :shipment_state, :payment_state, :distributor_id,
:order_cycle_id, :email, :total, :customer_id
searchable_associations :shipping_method, :bill_address, :distributor
@@ -33,13 +31,13 @@ module Spree
token_resource
belongs_to :user, class_name: "Spree::User"
belongs_to :created_by, class_name: "Spree::User"
belongs_to :user, class_name: "Spree::User", optional: true
belongs_to :created_by, class_name: "Spree::User", optional: true
belongs_to :bill_address, class_name: 'Spree::Address'
belongs_to :bill_address, class_name: 'Spree::Address', optional: true
alias_attribute :billing_address, :bill_address
belongs_to :ship_address, class_name: 'Spree::Address'
belongs_to :ship_address, class_name: 'Spree::Address', optional: true
alias_attribute :shipping_address, :ship_address
has_many :state_changes, as: :stateful, dependent: :destroy
@@ -70,9 +68,9 @@ module Spree
dependent: :destroy
has_many :invoices, dependent: :restrict_with_exception
belongs_to :order_cycle
belongs_to :distributor, class_name: 'Enterprise'
belongs_to :customer
belongs_to :order_cycle, optional: true
belongs_to :distributor, class_name: 'Enterprise', optional: true
belongs_to :customer, optional: true
has_one :proxy_order, dependent: :destroy
has_one :subscription, through: :proxy_order

View File

@@ -155,6 +155,7 @@ module Spree
if adjustment
adjustment.originator = payment_method
adjustment.label = adjustment_label
adjustment.amount = payment_method.compute_amount(self)
adjustment.save
elsif !processing_refund? && payment_method.present?
payment_method.create_adjustment(adjustment_label, self, true)

View File

@@ -118,7 +118,7 @@ module Spree
end
def self.clean_name
i18n_key = "spree.admin.payment_methods.providers." + name.demodulize.downcase
i18n_key = "spree.admin.payment_methods.providers.#{name.demodulize.downcase}"
I18n.t(i18n_key)
end

View File

@@ -25,6 +25,10 @@ module Spree
ActiveMerchant::Billing::Response.new(true, "", {}, {})
end
def payment_source_class
nil
end
def source_required?
false
end

View File

@@ -24,10 +24,6 @@ module Spree
amount
end
def price_changed?
amount_changed?
end
def price=(price)
self[:amount] = parse_price(price)
end

View File

@@ -23,6 +23,7 @@ require 'open_food_network/property_merge'
module Spree
class Product < ApplicationRecord
include ProductStock
include LogDestroyPerformer
self.belongs_to_required_by_default = false
@@ -303,6 +304,16 @@ module Spree
)
end
# Remove any unsupported HTML.
def description
HtmlSanitizer.sanitize(super)
end
# Remove any unsupported HTML.
def description=(html)
super(HtmlSanitizer.sanitize(html))
end
private
def update_units

View File

@@ -2,12 +2,9 @@
module Spree
class ProductProperty < ApplicationRecord
self.belongs_to_required_by_default = false
belongs_to :product, class_name: "Spree::Product", touch: true
belongs_to :property, class_name: 'Spree::Property'
validates :property, presence: true
validates :value, length: { maximum: 255 }
default_scope -> { order("#{table_name}.position") }

View File

@@ -2,8 +2,6 @@
module Spree
class ReturnAuthorization < ApplicationRecord
self.belongs_to_required_by_default = false
acts_as_paranoid
belongs_to :order, class_name: 'Spree::Order', inverse_of: :return_authorizations
@@ -13,7 +11,6 @@ module Spree
before_save :force_positive_amount
before_create :generate_number
validates :order, presence: true
validates :amount, numericality: true
validate :must_have_shipped_units

View File

@@ -2,11 +2,9 @@
module Spree
class State < ApplicationRecord
self.belongs_to_required_by_default = false
belongs_to :country, class_name: 'Spree::Country'
validates :country, :name, presence: true
validates :name, presence: true
def self.find_all_by_name_or_abbr(name_or_abbr)
where('name = ? OR abbr = ?', name_or_abbr, name_or_abbr)

View File

@@ -2,15 +2,12 @@
module Spree
class StockItem < ApplicationRecord
self.belongs_to_required_by_default = false
acts_as_paranoid
belongs_to :stock_location, class_name: 'Spree::StockLocation', inverse_of: :stock_items
belongs_to :variant, -> { with_deleted }, class_name: 'Spree::Variant'
has_many :stock_movements
has_many :stock_movements, dependent: :destroy
validates :stock_location, :variant, presence: true
validates :variant_id, uniqueness: { scope: [:stock_location_id, :deleted_at] }
validates :count_on_hand, numericality: { greater_than_or_equal_to: 0, unless: :backorderable? }

View File

@@ -2,22 +2,15 @@
module Spree
class StockMovement < ApplicationRecord
self.belongs_to_required_by_default = false
belongs_to :stock_item, class_name: 'Spree::StockItem'
belongs_to :originator, polymorphic: true
belongs_to :originator, polymorphic: true, optional: true
after_create :update_stock_item_quantity
validates :stock_item, presence: true
validates :quantity, presence: true
scope :recent, -> { order('created_at DESC') }
def readonly?
!new_record?
end
private
def update_stock_item_quantity

View File

@@ -14,17 +14,14 @@ end
module Spree
class TaxRate < ApplicationRecord
self.belongs_to_required_by_default = false
acts_as_paranoid
include CalculatedAdjustments
belongs_to :zone, class_name: "Spree::Zone", inverse_of: :tax_rates
belongs_to :zone, class_name: "Spree::Zone", inverse_of: :tax_rates, optional: true
belongs_to :tax_category, class_name: "Spree::TaxCategory", inverse_of: :tax_rates
has_many :adjustments, as: :originator
has_many :adjustments, as: :originator, dependent: nil
validates :amount, presence: true, numericality: true
validates :tax_category, presence: true
validates_with DefaultTaxZoneValidator
scope :by_zone, ->(zone) { where(zone_id: zone) }

View File

@@ -32,8 +32,8 @@ module Spree
delegate :name, :name=, :description, :description=, :meta_keywords, to: :product
has_many :inventory_units, inverse_of: :variant
has_many :line_items, inverse_of: :variant
has_many :inventory_units, inverse_of: :variant, dependent: nil
has_many :line_items, inverse_of: :variant, dependent: nil
has_many :stock_items, dependent: :destroy, inverse_of: :variant
has_many :stock_locations, through: :stock_items
@@ -49,11 +49,11 @@ module Spree
has_many :prices,
class_name: 'Spree::Price',
dependent: :destroy
delegate :display_price, :display_amount, :price, :price_changed?, :price=,
delegate :display_price, :display_amount, :price, :price=,
:currency, :currency=,
to: :find_or_build_default_price
has_many :exchange_variants
has_many :exchange_variants, dependent: nil
has_many :exchanges, through: :exchange_variants
has_many :variant_overrides, dependent: :destroy
has_many :inventory_items, dependent: :destroy
@@ -199,6 +199,11 @@ module Spree
price_in(currency).try(:amount)
end
def changed?
# We consider the variant changed if associated price is changed (it is saved after_save)
super || default_price.changed?
end
# can_supply? is implemented in VariantStock
def in_stock?(quantity = 1)
can_supply?(quantity)

View File

@@ -1,13 +1,9 @@
# frozen_string_literal: true
class SubscriptionLineItem < ApplicationRecord
self.belongs_to_required_by_default = false
belongs_to :subscription, inverse_of: :subscription_line_items
belongs_to :variant, -> { with_deleted }, class_name: 'Spree::Variant'
validates :subscription, presence: true
validates :variant, presence: true
validates :quantity, presence: true, numericality: { only_integer: true }
default_scope { order('id ASC') }

View File

@@ -1,14 +1,10 @@
# frozen_string_literal: true
class TagRule < ApplicationRecord
self.belongs_to_required_by_default = false
belongs_to :enterprise
preference :customer_tags, :string, default: ""
validates :enterprise, presence: true
scope :for, ->(enterprise) { where(enterprise_id: enterprise) }
scope :prioritised, -> { order('priority ASC') }

View File

@@ -6,15 +6,11 @@ class VariantOverride < ApplicationRecord
extend Spree::LocalizedNumber
include StockSettingsOverrideValidation
self.belongs_to_required_by_default = false
acts_as_taggable
belongs_to :hub, class_name: 'Enterprise'
belongs_to :variant, class_name: 'Spree::Variant'
validates :hub, presence: true
validates :variant, presence: true
# Default stock can be nil, indicating stock should not be reset or zero, meaning reset to zero.
# Need to ensure this can be set by the user.
validates :default_stock, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true

View File

@@ -1,53 +0,0 @@
# frozen_string_literal: true
module Admin
class ConnectedAppReflex < ApplicationReflex
def create
authorize! :admin, enterprise
app = ConnectedApp.create!(enterprise_id: enterprise.id)
# Avoid race condition by sending before enqueuing job:
broadcast_partial
ConnectAppJob.perform_later(
app, current_user.spree_api_key,
channel: SessionChannel.for_request(request),
)
morph :nothing
end
def destroy
authorize! :admin, enterprise
app = enterprise.connected_apps.first
app.destroy
broadcast_partial
WebhookDeliveryJob.perform_later(
app.data["destroy"],
"disconnect-app",
nil
)
morph :nothing
end
private
def enterprise
@enterprise ||= Enterprise.find(element.dataset.enterprise_id)
end
def broadcast_partial
selector = "#edit_enterprise_#{enterprise.id} #connected-app-discover-regen"
html = ApplicationController.render(
partial: "admin/enterprises/form/connected_apps",
locals: { enterprise: },
)
# Avoid race condition by sending before enqueuing job:
cable_ready.morph(selector:, html:).broadcast
end
end
end

View File

@@ -21,7 +21,8 @@ module Admin
def ship
@order.send_shipment_email = false unless params[:send_shipment_email]
if @order.ship
return set_param_for_controller if request.url.match?('edit')
paths = %w[edit customer payments adjustments invoices return_authorizations].freeze
return set_param_for_controller if Regexp.union(paths).match? request.url
morph dom_id(@order), render(partial: "spree/admin/orders/table_row",
locals: { order: @order.reload, success: true })

View File

@@ -21,46 +21,6 @@ class ProductsReflex < ApplicationReflex
fetch_and_render_products_with_flash
end
def delete_product
id = current_id_from_element(element)
product = product_finder(id).find_product
authorize! :delete, product
if product.destroy
flash[:success] = I18n.t('admin.products_v3.delete_product.success')
else
flash[:error] = I18n.t('admin.products_v3.delete_product.error')
end
fetch_and_render_products_with_flash
end
def delete_variant
id = current_id_from_element(element)
variant = Spree::Variant.active.find(id)
authorize! :delete, variant
if VariantDeleter.new.delete(variant)
flash[:success] = I18n.t('admin.products_v3.delete_variant.success')
else
flash[:error] = I18n.t('admin.products_v3.delete_variant.error')
end
fetch_and_render_products_with_flash
end
def edit_image
id = current_id_from_element(element)
product = product_finder(id).find_product
image = product.image
image = Spree::Image.new(viewable: product) if product.image.blank?
morph "#modal-component",
render(partial: "admin/products_v3/edit_image",
locals: { product:, image:, return_url: url })
end
private
def init_filters_params
@@ -89,8 +49,8 @@ class ProductsReflex < ApplicationReflex
html: render(partial: "admin/products_v3/content",
locals: { products: @products, pagy: @pagy, search_term: @search_term,
producer_options: producers, producer_id: @producer_id,
category_options: categories, category_id: @category_id,
flashes: flash })
category_options: categories, tax_category_options:,
category_id: @category_id, flashes: flash })
)
cable_ready.replace_state(
@@ -125,6 +85,10 @@ class ProductsReflex < ApplicationReflex
Spree::Taxon.order(:name).map { |c| [c.name, c.id] }
end
def tax_category_options
Spree::TaxCategory.order(:name).pluck(:name, :id)
end
def fetch_products
product_query = OpenFoodNetwork::Permissions.new(current_user)
.editable_products.merge(product_scope).ransack(ransack_query).result(distinct: true)
@@ -210,12 +174,4 @@ class ProductsReflex < ApplicationReflex
params.permit(products: ::PermittedAttributes::Product.attributes)
.to_h.with_indifferent_access
end
def product_finder(id)
ProductScopeQuery.new(current_user, { id: })
end
def current_id_from_element(element)
element.dataset.current_id
end
end

View File

@@ -131,7 +131,7 @@ module Api
producer_shop: "map_003-producer-shop.svg",
producer: "map_001-producer-only.svg",
}
"/map_icons/" + (icons[enterprise.category] || "map_001-producer-only.svg")
"/map_icons/#{icons[enterprise.category] || 'map_001-producer-only.svg'}"
end
# Choose regular icon font for enterprises.

View File

@@ -21,7 +21,7 @@ module Api
producer_shop: "map_003-producer-shop.svg",
producer: "map_001-producer-only.svg",
}
"/map_icons/" + (icons[enterprise.category] || "map_001-producer-only.svg")
"/map_icons/#{icons[enterprise.category] || 'map_001-producer-only.svg'}"
end
def icon_font

View File

@@ -59,7 +59,7 @@ class EmbeddedPageService
def set_logout_redirect
return unless enterprise_slug
@session[:shopfront_redirect] = '/' + enterprise_slug + '/shop?embedded_shopfront=true'
@session[:shopfront_redirect] = "/#{enterprise_slug}/shop?embedded_shopfront=true"
end
def enterprise_slug

View File

@@ -0,0 +1,21 @@
# frozen_string_literal: true
# Keeps only allowed HTML.
#
# We store some rich text as HTML in attributes of models like Enterprise.
# We offer an editor which supports certain tags but you can't insert just any
# HTML, which would be dangerous.
class HtmlSanitizer
# div is required by Trix editor
ALLOWED_TAGS = %w[h1 h2 h3 h4 div p br b i u a strong em del pre blockquote ul ol li hr
figure].freeze
ALLOWED_ATTRIBUTES = %w[href target].freeze
ALLOWED_TRIX_DATA_ATTRIBUTES = %w[data-trix-attachment].freeze
def self.sanitize(html)
@sanitizer ||= Rails::HTML5::SafeListSanitizer.new
@sanitizer.sanitize(
html, tags: ALLOWED_TAGS, attributes: (ALLOWED_ATTRIBUTES + ALLOWED_TRIX_DATA_ATTRIBUTES)
)
end
end

View File

@@ -68,23 +68,26 @@ class ProductsRenderer
end
def products_order
if (distributor.preferred_shopfront_product_sorting_method == "by_producer") &&
distributor.preferred_shopfront_producer_order.present?
distributor
.preferred_shopfront_producer_order
.split(",").map { |id| "spree_products.supplier_id=#{id} DESC" }
.join(", ") + ", spree_products.name ASC, spree_products.id ASC"
if distributor.preferred_shopfront_product_sorting_method == "by_producer" &&
distributor.preferred_shopfront_producer_order.present?
order_by_producer = distributor
.preferred_shopfront_producer_order
.split(",").map { |id| "spree_products.supplier_id=#{id} DESC" }
.join(", ")
"#{order_by_producer}, spree_products.name ASC, spree_products.id ASC"
elsif distributor.preferred_shopfront_product_sorting_method == "by_category" &&
distributor.preferred_shopfront_taxon_order.present?
distributor
.preferred_shopfront_taxon_order
.split(",").map { |id| "first_variant.primary_taxon_id=#{id} DESC" }
.join(", ") + ", spree_products.name ASC, spree_products.id ASC"
distributor.preferred_shopfront_taxon_order.present?
order_by_category = distributor
.preferred_shopfront_taxon_order
.split(",").map { |id| "first_variant.primary_taxon_id=#{id} DESC" }
.join(", ")
"#{order_by_category}, spree_products.name ASC, spree_products.id ASC"
else
"spree_products.name ASC, spree_products.id"
end
end
def variants_for_shop
@variants_for_shop ||= begin
scoper = OpenFoodNetwork::ScopeVariantToHub.new(distributor)

View File

@@ -67,11 +67,12 @@ module Sets
product.assign_attributes(product_related_attrs)
return true unless product.changed?
validate_presence_of_unit_value_in_product(product)
changed = product.changed?
success = product.errors.empty? && product.save
count_result(success && changed)
count_result(success)
success
end
@@ -104,7 +105,8 @@ module Sets
def create_or_update_variant(product, variant_attributes)
variant = find_model(product.variants, variant_attributes[:id])
if variant.present?
variant.update(variant_attributes.except(:id))
variant.assign_attributes(variant_attributes.except(:id))
variant.save if variant.changed?
else
variant = create_variant(product, variant_attributes)
end

View File

@@ -0,0 +1,23 @@
= form_with url: bulk_update_admin_column_preferences_path, method: :put,
id: :bulk_admin_column_preferences_form, class: "column-preferences",
html: { 'data-controller': "column-preferences" } do |f|
= hidden_field_tag :action_name, action
/ DC: this makes my Chrome DevTools crash when inspecting the <details> element. If problem continues, we need to use a different method.
%details.ofn-drop-down.ofn-drop-down-v2.right{ 'data-controller': "dropdown" }
%summary.ofn-drop-down-label
= t('admin.columns')
%span.icon-caret
.menu
.menu_items
- ColumnPreference.for(spree_current_user, action).each_with_index do |column_preference, index|
= f.fields_for("column_preferences", column_preference, index:) do |cp_form|
= cp_form.hidden_field :id
= cp_form.hidden_field :column_name
%label.menu_item
= cp_form.check_box :visible, 'data-column-name': column_preference.column_name
%span= t("admin.products_page.columns." + column_preference.column_name)
.actions
= f.submit t('admin.column_save_as_default'), class: "secondary fullwidth"

View File

@@ -0,0 +1,3 @@
= turbo_stream.replace "bulk_admin_column_preferences_form" do
= render partial: "admin/shared/flashes", locals: { flashes: flash } if defined? flash
= render partial: 'form', locals: { action: }

View File

@@ -9,6 +9,11 @@
%fieldset.alpha.no-border-bottom{ id: "#{item[:name]}_panel", data: { "tabs-and-panels-target": "panel" }}
%legend= t(".#{ item[:name] }.legend")
- when 'connected_apps'
-# Don't render this item here in the main form.
-# The panel contains its own form and we can't nest forms in forms.
-# Otherwise we add multiple authenticity tokens and Rails denies updates.
- else
%fieldset.alpha.no-border-bottom{ id: "#{item[:name]}_panel", data: { "tabs-and-panels-target": "panel" }}
%legend= t(".#{ item[:form_name] || item[:name] }.legend")

View File

@@ -11,11 +11,4 @@
%input.red{ type: "button", value: t(:update), "ng-click": "submit()", "ng-disabled": "!enterprise_form.$dirty" }
%input{ type: "button", "ng-value": "enterprise_form.$dirty ? '#{t(:cancel)}' : '#{t(:close)}'", "ng-click": "cancel('#{main_app.admin_enterprises_path}')" }
.row{ data: {
controller: "tabs-and-panels", "tabs-and-panels-class-name-value": "selected" }}
.sixteen.columns.alpha
.four.columns.alpha
= render 'admin/shared/side_menu'
.one.column &nbsp;
.eleven.columns.omega.fullwidth_inputs
= render 'form', f: f
= render 'form', f: f

View File

@@ -13,4 +13,14 @@
= render 'admin/enterprises/form_data'
= render 'admin/enterprises/ng_form', action: 'edit'
.row{ data: { controller: "tabs-and-panels", "tabs-and-panels-class-name-value": "selected" }}
.sixteen.columns.alpha
.four.columns.alpha
= render 'admin/shared/side_menu'
.one.column &nbsp;
.eleven.columns.omega.fullwidth_inputs
= render 'admin/enterprises/ng_form', action: 'edit'
%fieldset.alpha.no-border-bottom{ id: "connected_apps_panel", data: { "tabs-and-panels-target": "panel" }}
%legend= t("admin.enterprises.form.connected_apps.legend")
= render "admin/enterprises/form/connected_apps", enterprise: @enterprise

View File

@@ -1,21 +1,20 @@
- enterprise ||= f.object
#connected-app-discover-regen
%div{ id: "connected-app-discover-regen", class: "enterprise_#{enterprise.id}" }
.connected-app__head
%div
%h3= t ".title"
%p= t ".tagline"
%div
- if enterprise.connected_apps.empty?
%button{ data: {reflex: "click->Admin::ConnectedApp#create", enterprise_id: enterprise.id} }
= t ".enable"
= button_to t(".enable"), admin_enterprise_connected_apps_path(enterprise.id), method: :post, disabled: !managed_by_user?(enterprise)
-# This is only seen by super-admins:
%em= t(".need_to_be_manager") unless managed_by_user?(enterprise)
- elsif enterprise.connected_apps.connecting.present?
%button{ disabled: true }
%i.spinner.fa.fa-spin.fa-circle-o-notch
&nbsp;
= t ".loading"
- else
%button{ data: {reflex: "click->Admin::ConnectedApp#destroy", enterprise_id: enterprise.id} }
= t ".disable"
= button_to t(".disable"), admin_enterprise_connected_app_path(0, enterprise_id: enterprise.id), method: :delete
.connected-app__connection
- if enterprise.connected_apps.ready.present?

View File

@@ -15,7 +15,7 @@
.container.results
.sixteen.columns
= render partial: 'sort', locals: { pagy: pagy, search_term: search_term, producer_id: producer_id, category_id: category_id }
= render partial: 'table', locals: { products: products }
= render partial: 'table', locals: { products:, producer_options:, category_options:, tax_category_options: }
- if pagy.present? && pagy.pages > 1
= render partial: 'admin/shared/stimulus_pagination', locals: { pagy: pagy }
- else

View File

@@ -5,8 +5,8 @@
cancel_button_text: t("#{base_translation_key}.cancellation_text"),
confirm_button_class: :red,
actions_alignment_class: 'justify-end',
confirm_reflexes: "click->products#delete_#{object_type}",
confirm_actions: "click->modal#close",
controller: "products",
confirm_actions: "click->products#delete_#{object_type} click->modal#close",
)
= render delete_modal do
%h2.margin-bottom-20.black-text

View File

@@ -1,17 +0,0 @@
= render ModalComponent.new id: "#modal_edit_product_image_#{image.id}", instant: true, close_button: false, modal_class: :fit do
%h2= t(".title")
-# Display image in the same way it appears in the shopfront popup
%p= image_tag image.persisted? ? image.url(:large) : Spree::Image.default_image_url(:large), width: 433, height: 433
-# Submit to controller, because StimulusReflex doesn't support file uploads
= form_for [:admin, product, image],
html: { multipart: true }, data: { controller: "form" } do |f|
%input{ type: :hidden, name: :return_url, value: return_url}
= f.hidden_field :viewable_id, value: product.id
.modal-actions.justify-end
%input{ class: "secondary relaxed", type: 'button', value: t('.close'), "data-action": "click->modal#close" }
-# label.button provides a handy shortcut to open the file selector on click. Unfortunately this trick isn't keyboard accessible though..
= f.label :attachment, t(".upload"), class: "button primary relaxed icon-upload-alt"
= f.file_field :attachment, accept: "image/*", style: "display: none", "data-action": "change->form#submit"

View File

@@ -1,6 +1,7 @@
= form_with url: admin_products_path, id: "filters", method: :get, data: { "search-target": "form", 'turbo-frame': "_self" } do
= hidden_field_tag :page, nil, class: "page"
= hidden_field_tag :per_page, nil, class: "per-page"
= hidden_field_tag '[q][s]', params.dig(:q, :s) || 'name asc', class: 'sort', 'data-default': 'name asc'
.query
.search-input

View File

@@ -0,0 +1,3 @@
%a.image-field{ href: product_image_form_path(product), 'data-turbo-stream': true }
= image_tag product.image&.url(:mini) || Spree::Image.default_image_url(:mini), width: 40, height: 40
.button.secondary.mini= t('admin.products_page.image.edit')

View File

@@ -1,15 +1,13 @@
%td.with-image
%a.image-field{ href: admin_product_images_path(product), data: { controller: "modal", reflex: "click->products#edit_image", "current-id": product.id} }
= image_tag product.image&.url(:mini) || Spree::Image.default_image_url(:mini), width: 40, height: 40
.button.secondary.mini= t('admin.products_page.image.edit')
%td.field.align-left.header.naked_inputs
%td.col-image.with-image{ id: "image-#{product.id}" }
= render partial: "product_image", locals: { product: }
%td.col-name.field.align-left.header.naked_inputs
= f.hidden_field :id
= f.text_field :name, 'aria-label': t('admin.products_page.columns.name')
= error_message_on product, :name
%td.field.naked_inputs
%td.col-sku.field.naked_inputs
= f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
= error_message_on product, :sku
%td.multi-field.naked_inputs{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' }
%td.col-unit_scale.field.naked_inputs{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' }
= f.hidden_field :variant_unit
= f.hidden_field :variant_unit_scale
= f.select :variant_unit_with_scale,
@@ -21,18 +19,23 @@
.field
= f.text_field :variant_unit_name, 'aria-label': t('items'), 'data-toggle-control-target': 'control', style: (product.variant_unit == "items" ? "" : "display: none")
= error_message_on product, :variant_unit_name, 'data-toggle-control-target': 'control'
%td.align-right
%td.col-unit.align-right
-# empty
%td.align-right
%td.col-price.align-right
-# empty
%td.align-right
%td.col-on_hand.align-right
-# empty
%td.align-left
.content= product.supplier&.name
%td.align-left
%td.col-producer.naked_inputs
= render(SearchableDropdownComponent.new(form: f,
name: :supplier_id,
aria_label: t('.producer_field_name'),
options: producer_options,
selected_option: product.supplier_id,
placeholder_value: t('admin.products_v3.filters.search_for_producers')))
%td.col-category.align-left
-# empty
%td.align-left
%td.align-left
%td.col-tax_category.align-left
%td.col-inherits_properties.align-left
.content= product.inherits_properties ? 'YES' : 'NO' #TODO: consider using https://github.com/RST-J/human_attribute_values, else use I18n.t (also below)
%td.align-right
= render(VerticalEllipsisMenu::Component.new) do
@@ -40,5 +43,5 @@
= link_to t('admin.products_page.actions.clone'), clone_admin_product_path(product), 'data-turbo': false
%a{ "data-controller": "modal-link", "data-action": "click->modal-link#setModalDataSetOnConfirm click->modal-link#open",
"data-modal-link-target-value": "product-delete-modal", "class": "delete",
"data-modal-link-modal-dataset-value": {'data-current-id': product.id}.to_json }
"data-modal-link-modal-dataset-value": {'data-delete-path': admin_product_destroy_path(product)}.to_json }
= t('admin.products_page.actions.delete')

View File

@@ -1,5 +1,5 @@
#sort
%div
%div.pagination-description
- if pagy.present?
= t(".pagination.total_html", total: pagy.count, from: pagy.from, to: pagy.to)
@@ -13,3 +13,6 @@
options_for_select([15, 25, 50, 100].collect{|i| [t('.pagination.per_page.per_page', num: i), i]}, pagy&.items),
class: "no-input per-page",
data: { controller: "tom-select search", action: "change->search#changePerPage", "tom-select-options-value": '{ "plugins": [] }'}
/ Columns dropdown
= render partial: "admin/column_preferences/form", locals: { action: "products_v3_index" }

View File

@@ -13,20 +13,21 @@
= hidden_field_tag :producer_id, @producer_id
= hidden_field_tag :category_id, @category_id
%table.products
%table.products{ 'data-column-preferences-target': "table" }
%colgroup
%col{ width:"56" }= # Img (size + padding)
%col= # (grow to fill) Name
%col{ width:"5%"}
%col{ width:"8%"}
%col{ width:"8%"}
%col{ width:"5%"}
%col{ width:"10%"}
%col{ width:"15%"}= # Producer
%col{ width:"8%"}
%col{ width:"8%"}
%col{ width:"8%"}
%col{ width:"8%"}= # Actions
-# The `min-width` property works in Chrome but not Firefox so is considered progressive enhancement.
%col.col-image{ width:"56px" }= # (image size + padding)
%col.col-name{ style:"min-width: 6em" }= # (grow to fill)
%col.col-sku{ width:"8%", style:"min-width: 6em" }
%col.col-unit_scale{ width:"8%" }
%col.col-unit{ width:"8%" }
%col.col-price{ width:"5%", style:"min-width: 5em" }
%col.col-on_hand{ width:"10%"}
%col.col-producer{ style:"min-width: 6em" }= # (grow to fill)
%col.col-category{ width:"8%" }
%col.col-tax_category{ width:"8%" }
%col.col-inherits_properties{ width:"5%" }
%col{ width:"5%", style:"min-width: 3em"}= # Actions
%thead
%tr
%td.form-actions-wrapper{ colspan: 12 }
@@ -48,35 +49,36 @@
= t('.reset')
= form.submit t('.save'), class: "medium"
%tr
%th.align-left= # image
%th.align-left.with-input= t('admin.products_page.columns.name')
%th.align-left.with-input= t('admin.products_page.columns.sku')
%th.align-left.with-input= t('admin.products_page.columns.unit_scale')
%th.align-left.with-input= t('admin.products_page.columns.unit')
%th.align-left.with-input= t('admin.products_page.columns.price')
%th.align-left.with-input= t('admin.products_page.columns.on_hand')
%th.align-left= t('admin.products_page.columns.producer')
%th.align-left= t('admin.products_page.columns.category')
%th.align-left= t('admin.products_page.columns.tax_category')
%th.align-left= t('admin.products_page.columns.inherits_properties')
%th.col-image.align-left= # image
= render partial: 'spree/admin/shared/stimulus_sortable_header',
locals: { column: :name, sorted: params.dig(:q, :s), default: 'name asc' }
%th.align-left.col-sku.with-input= t('admin.products_page.columns.sku')
%th.align-left.col-unit_scale.with-input= t('admin.products_page.columns.unit_scale')
%th.align-left.col-unit.with-input= t('admin.products_page.columns.unit')
%th.align-left.col-price.with-input= t('admin.products_page.columns.price')
%th.align-left.col-on_hand.with-input= t('admin.products_page.columns.on_hand')
%th.align-left.col-producer= t('admin.products_page.columns.producer')
%th.align-left.col-category= t('admin.products_page.columns.category')
%th.align-left.col-tax_category= t('admin.products_page.columns.tax_category')
%th.align-left.col-inherits_properties= t('admin.products_page.columns.inherits_properties')
%th.align-right= t('admin.products_page.columns.actions')
- products.each_with_index do |product, product_index|
= form.fields_for("products", product, index: product_index) do |product_form|
%tbody.relaxed{ data: { 'record-id': product_form.object.id,
%tbody.relaxed{ id: dom_id(product), data: { 'record-id': product_form.object.id,
controller: "nested-form product",
action: 'rails-nested-form:add->bulk-form#registerElements' } }
action: 'rails-nested-form:add->bulk-form#registerElements rails-nested-form:remove->bulk-form#toggleFormChanged' } }
%tr
= render partial: 'product_row', locals: { product:, f: product_form }
= render partial: 'product_row', locals: { f: product_form, product:, producer_options: }
- product.variants.each_with_index do |variant, variant_index|
= form.fields_for("products][#{product_index}][variants_attributes][", variant, index: variant_index) do |variant_form|
%tr.condensed{ 'data-controller': "variant" }
= render partial: 'variant_row', locals: { variant:, f: variant_form }
%tr.condensed{ id: dom_id(variant), 'data-controller': "variant", 'class': "nested-form-wrapper", 'data-new-record': variant.new_record? ? "true" : false }
= render partial: 'variant_row', locals: { variant:, f: variant_form, category_options:, tax_category_options: }
= form.fields_for("products][#{product_index}][variants_attributes][NEW_RECORD", product.variants.build) do |new_variant_form|
%template{ 'data-nested-form-target': "template" }
%tr.condensed{ 'data-controller': "variant" }
= render partial: 'variant_row', locals: { variant: new_variant_form.object, f: new_variant_form }
%tr.condensed{ 'data-controller': "variant", 'class': "nested-form-wrapper", 'data-new-record': "true" }
= render partial: 'variant_row', locals: { variant: new_variant_form.object, f: new_variant_form, category_options:, tax_category_options: }
%tr{ 'data-nested-form-target': "target" }
%tr.condensed

View File

@@ -1,15 +1,15 @@
%td
%td.col-image
-# empty
%td.field.naked_inputs
%td.col-name.field.naked_inputs
= f.hidden_field :id
= f.text_field :display_name, 'aria-label': t('admin.products_page.columns.name'), placeholder: variant.product.name
= error_message_on variant, :display_name
%td.field.naked_inputs
%td.col-sku.field.naked_inputs
= f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
= error_message_on variant, :sku
%td
%td.col-unit_scale
-# empty
%td.field.popout{'data-controller': "popout", 'data-popout-update-display-value': "false"}
%td.col-unit.field.popout{'data-controller': "popout", 'data-popout-update-display-value': "false"}
= f.button :unit_to_display, class: "popout__button", 'aria-label': t('admin.products_page.columns.unit'), 'data-popout-target': "button" do
= variant.unit_to_display # Show the generated summary of unit values
%div.popout__container{ style: 'display: none;', 'data-controller': 'toggle-control', 'data-popout-target': "dialog" }
@@ -25,10 +25,10 @@
= f.label :display_as, t('admin.products_page.columns.display_as')
= f.text_field :display_as, placeholder: VariantUnits::OptionValueNamer.new(variant).name
= error_message_on variant, :unit_value
%td.field.naked_inputs
%td.col-price.field.naked_inputs
= f.text_field :price, 'aria-label': t('admin.products_page.columns.price'), value: number_to_currency(variant.price, unit: '')&.strip # TODO: add a spec to prove that this formatting is necessary. If so, it should be in a shared form helper for currency inputs
= error_message_on variant, :price
%td.field.popout{'data-controller': "popout"}
%td.col-on_hand.field.popout{'data-controller': "popout"}
%button.popout__button{'data-popout-target': "button", 'aria-label': t('admin.products_page.columns.on_hand')}
= variant.on_demand ? t(:on_demand) : variant.on_hand
%div.popout__container{ style: 'display: none;', 'data-controller': 'toggle-control', 'data-popout-target': "dialog" }
@@ -39,20 +39,35 @@
= f.label :on_demand do
= f.check_box :on_demand, 'data-action': 'change->toggle-control#disableIfPresent change->popout#closeIfChecked'
= t(:on_demand)
%td.align-left
.content= variant.product.supplier&.name # same as product
%td.align-left
.content= variant.primary_taxon&.name
%td.align-left
.content= (variant.tax_category_id ? variant.tax_category&.name : t('.none_tax_category')) # TODO: convert to dropdown
%td.align-left
%td.col-producer.align-left
-# empty producer name
%td.col-category.field.naked_inputs
= render(SearchableDropdownComponent.new(form: f,
name: :primary_taxon_id,
options: category_options,
selected_option: variant.primary_taxon_id,
aria_label: t('.category_field_name'),
placeholder_value: t('admin.products_v3.filters.search_for_categories')))
%td.col-tax_category.field.naked_inputs
= render(SearchableDropdownComponent.new(form: f,
name: :tax_category_id,
options: tax_category_options,
selected_option: variant.tax_category_id,
include_blank: t('.none_tax_category'),
aria_label: t('.tax_category_field_name'),
placeholder_value: t('.search_for_tax_categories')))
= error_message_on variant, :tax_category
%td.col-inherits_properties.align-left
-# empty
%td.align-right
- if variant.persisted?
= render(VerticalEllipsisMenu::Component.new) do
= render(VerticalEllipsisMenu::Component.new) do
- if variant.persisted?
= link_to t('admin.products_page.actions.edit'), edit_admin_product_variant_path(variant.product, variant)
- if variant.product.variants.size > 1
%a{ "data-controller": "modal-link", "data-action": "click->modal-link#setModalDataSetOnConfirm click->modal-link#open",
"data-modal-link-target-value": "variant-delete-modal", "class": "delete",
"data-modal-link-modal-dataset-value": {'data-current-id': variant.id}.to_json }
"data-modal-link-modal-dataset-value": {'data-delete-path': admin_destroy_variant_path(variant)}.to_json }
= t('admin.products_page.actions.delete')
- else
%a{ 'data-action': "nested-form#remove", class: 'delete' }
= t('admin.products_page.actions.remove')

View File

@@ -0,0 +1,5 @@
- # @record can either be Product or Variant
- unless flash[:error]
= turbo_stream.remove(dom_id(@record))
= turbo_stream.append "flashes" do
= render(partial: 'admin/shared/flashes', locals: { flashes: flash })

View File

@@ -15,8 +15,8 @@
= render partial: "content", locals: { products: @products, pagy: @pagy, search_term: @search_term,
producer_options: producers, producer_id: @producer_id,
category_options: categories, category_id: @category_id,
flashes: flash }
tax_category_options:, flashes: flash }
- %w[product variant].each do |object_type|
= render partial: 'delete_modal', locals: { object_type: }
#modal-component
#edit_image_modal

View File

@@ -7,7 +7,8 @@
.row.date-range-filter
.alpha.two.columns= label_tag nil, t(:date_range)
.omega.fourteen.columns
= f.text_field "#{field}_gt", :class => 'datetimepicker datepicker-from', :placeholder => t(:start), data: { controller: "flatpickr", "flatpickr-enable-time-value": true, "flatpickr-default-date-value": "startOfDay" }, value: start_date
%span.range-divider
%i.icon-arrow-right
= f.text_field "#{field}_lt", :class => 'datetimepicker datepicker-to', :placeholder => t(:stop), data: { controller: "flatpickr", "flatpickr-enable-time-value": true, "flatpickr-default-date-value": "endOfDay" }, value: end_date
.field-block.omega.four.columns
.date-range-fields{ data: { controller: "flatpickr", "flatpickr-mode-value": "range", "flatpickr-enable-time-value": true , "flatpickr-default-hour": 0 } }
= text_field_tag nil, nil, class: "datepicker fullwidth", data: { "flatpickr-target": "instance", action: "flatpickr_clear@window->flatpickr#clear" }
= text_field_tag "q[#{field}_gt]", nil, data: { "flatpickr-target": "start" }, style: "display: none", value: start_date
= text_field_tag "q[#{field}_lt]", nil, data: { "flatpickr-target": "end" }, style: "display: none", value: end_date

View File

@@ -95,12 +95,7 @@
%div.checkout-input{"data-shippingmethod-target": "shippingMethodDescription", "data-shippingmethodid": shipping_method.id , style: "display: #{ship_method_is_selected ? 'block' : 'none'}" }
#distributor_address.panel
- if shipping_method.description.present?
%span #{shipping_method.description}
%br/
%br/
- if @order.order_cycle.pickup_time_for(@order.distributor)
= t :checkout_ready_for
= @order.order_cycle.pickup_time_for(@order.distributor)
= simple_format(html_escape(shipping_method.description))
= f.error_message_on :shipping_method, standalone: true

Some files were not shown because too many files have changed in this diff Show More