From eba2fbcc30a61b2fc21d1044dc737c58a3b0ddf8 Mon Sep 17 00:00:00 2001 From: David Cook Date: Mon, 9 Feb 2026 17:07:26 +1100 Subject: [PATCH] Create source variants --- .../admin/products_v3_controller.rb | 33 +++++++++++++++ .../_product_variant_row.html.haml | 2 +- .../admin/products_v3/_variant_row.html.haml | 4 +- .../create_sourced_variant.turbo_stream.haml | 16 ++++++++ config/locales/en.yml | 2 + config/routes/admin.rb | 1 + spec/requests/admin/products_v3_spec.rb | 41 +++++++++++++++++++ spec/system/admin/products_v3/actions_spec.rb | 17 ++++++-- 8 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 app/views/admin/products_v3/create_sourced_variant.turbo_stream.haml diff --git a/app/controllers/admin/products_v3_controller.rb b/app/controllers/admin/products_v3_controller.rb index d5c3f5383c..ba7151f05b 100644 --- a/app/controllers/admin/products_v3_controller.rb +++ b/app/controllers/admin/products_v3_controller.rb @@ -107,6 +107,39 @@ module Admin end end + # Clone a variant, retaining a link to the "source" + def create_sourced_variant + source_variant = Spree::Variant.find(params[:variant_id]) + product_index = params[:product_index] + authorize! :create_sourced_variant, source_variant + status = :ok + + begin + variant = source_variant.dup #may need a VariantDuplicator like producs? + variant.price = source_variant.price + variant.save! + variant.on_demand = source_variant.on_demand + variant.on_hand = source_variant.on_hand + variant.save! + #todo: create link to source + + flash.now[:success] = t('.success') + variant_index = "-#{variant.id}" + rescue ActiveRecord::RecordInvalid + flash.now[:error] = variant.errors.full_messages.to_sentence + status = :unprocessable_entity + variant_index = "-1" # Create a unique-enough index + end + + respond_with do |format| + format.turbo_stream { + locals = { source_variant:, variant:, product_index:, variant_index:, + producer_options:, category_options: categories, tax_category_options: } + render :create_sourced_variant, status:, locals: + } + end + end + def index_url(params) "/admin/products?#{params.to_query}" # todo: fix routing so this can be automaticly generated end diff --git a/app/views/admin/products_v3/_product_variant_row.html.haml b/app/views/admin/products_v3/_product_variant_row.html.haml index 71ab6e6301..deb0c467d0 100644 --- a/app/views/admin/products_v3/_product_variant_row.html.haml +++ b/app/views/admin/products_v3/_product_variant_row.html.haml @@ -13,7 +13,7 @@ - next if variant.supplier.present? && !allowed_producers.include?(variant.supplier) = form.fields_for("products][#{product_index}][variants_attributes", variant, index: variant_index) do |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:, producer_options: } + = render partial: 'variant_row', locals: { variant:, f: variant_form, product_index:, category_options:, tax_category_options:, producer_options: } = form.fields_for("products][#{product_index}][variants_attributes][NEW_RECORD", prepare_new_variant(product, producer_options)) do |new_variant_form| %template{ 'data-nested-form-target': "template" } diff --git a/app/views/admin/products_v3/_variant_row.html.haml b/app/views/admin/products_v3/_variant_row.html.haml index 79340b0f3e..d8edcaecd6 100644 --- a/app/views/admin/products_v3/_variant_row.html.haml +++ b/app/views/admin/products_v3/_variant_row.html.haml @@ -1,4 +1,4 @@ --# locals: (variant:, f:, category_options:, tax_category_options:, producer_options:) +-# locals: (variant:, f:, product_index: nil, category_options:, tax_category_options:, producer_options:) - method_on_demand, method_on_hand = variant.new_record? ? [:on_demand_desired, :on_hand_desired ]: [:on_demand, :on_hand] %td.col-image -# empty @@ -89,7 +89,7 @@ - if variant.persisted? = link_to t('admin.products_page.actions.edit'), edit_admin_product_variant_path(variant.product, variant) / TODO: only show if have permission. need to load permissions efficiently please. maybe the PErmissions object can cache result for each enterprise. maybe we preload it with the product query. - = link_to t('admin.products_page.actions.create_sourced_variant'), "TODO" # see next commit + = link_to t('admin.products_page.actions.create_sourced_variant'), admin_create_sourced_variant_path(variant_id: variant.id, product_index:), 'data-turbo-method': :post - 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", diff --git a/app/views/admin/products_v3/create_sourced_variant.turbo_stream.haml b/app/views/admin/products_v3/create_sourced_variant.turbo_stream.haml new file mode 100644 index 0000000000..da025462c3 --- /dev/null +++ b/app/views/admin/products_v3/create_sourced_variant.turbo_stream.haml @@ -0,0 +1,16 @@ +-# locals: (variant:, source_variant:, product_index:, variant_index:, producer_options:, category_options:, tax_category_options:) +-# Pre-render the form, because you can't do it inside turbo stream block +- variant_row = nil +- fields_for("products][#{product_index}][variants_attributes", variant, index: variant_index) do |f| + - variant_row = render(partial: 'variant_row', formats: :html, + locals: { f:, + variant:, + producer_options:, + category_options:, + tax_category_options:}) += turbo_stream.after dom_id(source_variant) do + %tr.condensed{ id: dom_id(variant), 'data-controller': "variant", 'class': "nested-form-wrapper slide-in" } + = variant_row + += turbo_stream.append "flashes" do + = render(partial: 'admin/shared/flashes', locals: { flashes: flash }) diff --git a/config/locales/en.yml b/config/locales/en.yml index 352e0875de..e38653bf95 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1099,6 +1099,8 @@ en: clone: success: Successfully cloned the product error: Unable to clone the product + create_sourced_variant: + success: "Successfully created sourced variant" tag_rules: rules_per_tag: one: "%{tag} has 1 rule" diff --git a/config/routes/admin.rb b/config/routes/admin.rb index e36ae10545..079dfee13c 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -82,6 +82,7 @@ Openfoodnetwork::Application.routes.draw do delete 'products_v3/:id', to: 'products_v3#destroy', as: 'product_destroy' delete 'products_v3/destroy_variant/:id', to: 'products_v3#destroy_variant', as: 'destroy_variant' post 'clone/:id', to: 'products_v3#clone', as: 'clone_product' + post 'products/create_sourced_variant', to: 'products_v3#create_sourced_variant', as: 'create_sourced_variant' resources :product_preview, only: [:show] resources :variant_overrides do diff --git a/spec/requests/admin/products_v3_spec.rb b/spec/requests/admin/products_v3_spec.rb index ff1a6850bb..a18f0c9f74 100644 --- a/spec/requests/admin/products_v3_spec.rb +++ b/spec/requests/admin/products_v3_spec.rb @@ -59,4 +59,45 @@ RSpec.describe "Admin::ProductsV3" do expect(response).to redirect_to('/unauthorized') end end + + describe "POST /admin/products/create_sourced_variant" do + let(:enterprise) { create(:supplier_enterprise) } + let(:user) { create(:user, enterprises: [enterprise]) } + + let(:supplier) { create(:supplier_enterprise) } + let(:variant) { create(:variant, display_name: "Original variant", supplier: supplier) } + + before do + sign_in user + end + + it "checks for permission" do + params = { variant_id: variant.id, product_index: 1 } + + expect { + post(admin_create_sourced_variant_path, as: :turbo_stream, params:) + expect(response).to redirect_to('/unauthorized') + }.not_to change { variant.product.variants.count } + end + + context "With create_sourced_variants permissions on supplier" do + let!(:enterprise_relationship) { + create(:enterprise_relationship, + parent: supplier, + child: enterprise, + permissions_list: [:create_sourced_variants]) + } + + it "creates a clone of the variant, retaining link as source" do + params = { variant_id: variant.id, product_index: 1 } + + expect { + post(admin_create_sourced_variant_path, as: :turbo_stream, params:) + + expect(response).to have_http_status(:ok) + expect(response.body).to match "Original variant" # cloned variant name + }.to change { variant.product.variants.count }.by(1) + end + end + end end diff --git a/spec/system/admin/products_v3/actions_spec.rb b/spec/system/admin/products_v3/actions_spec.rb index 006bce3132..07eff652e1 100644 --- a/spec/system/admin/products_v3/actions_spec.rb +++ b/spec/system/admin/products_v3/actions_spec.rb @@ -291,19 +291,30 @@ RSpec.describe 'As an enterprise user, I can perform actions on the products scr end context "with create_sourced_variants permission for my, and other's variants" do - it "shows an option to create sourced variant" do + it "creates a sourced variant" do create(:enterprise_relationship, parent: producer, child: producer, permissions_list: [:create_sourced_variants]) enterprise_relationship.permissions.create! name: :create_sourced_variants + # Check my own variant within row_containing_name("My box") do page.find(".vertical-ellipsis-menu").click - expect(page).to have_link "Create sourced variant" # , href: admin_clone_product_path(product_a) + + expect(page).to have_link "Create sourced variant" end + # Create variant sourced from my friend within row_containing_name("My friends box") do page.find(".vertical-ellipsis-menu").click - expect(page).to have_link "Create sourced variant" # , href: admin_clone_product_path(product_a) + + click_link "Create sourced variant" + end + + expect(page).to have_content "Successfully created sourced variant" + + within "table.products" do + # There are now two copies + expect(all_input_values).to match /My friends box.*My friends box/ end end end