From 31fabf167cb3bd88a7a0fea98e70d539a55f23d4 Mon Sep 17 00:00:00 2001 From: Thomas von Deyen Date: Fri, 27 Mar 2026 16:56:47 +0100 Subject: [PATCH] Fix ingredient editor nested attributes index collision The form_field_counter method used the ingredient's position in the YAML element definition to generate the nested attributes index. This caused collisions where two ingredients could end up with the same index, corrupting form data on save. Use the ingredient's database ID as the nested attributes key instead. Rails accepts_nested_attributes_for does not require sequential indices, it only needs unique keys to group fields for each record. Since each ingredient has a unique ID, this eliminates any possibility of collision. Signed-off-by: Thomas von Deyen --- app/components/alchemy/ingredients/base_editor.rb | 2 +- app/decorators/alchemy/ingredient_editor.rb | 2 +- spec/components/alchemy/ingredients/base_editor_spec.rb | 4 ++-- spec/decorators/alchemy/ingredient_editor_spec.rb | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/components/alchemy/ingredients/base_editor.rb b/app/components/alchemy/ingredients/base_editor.rb index 074d579c7e..158917f0c0 100644 --- a/app/components/alchemy/ingredients/base_editor.rb +++ b/app/components/alchemy/ingredients/base_editor.rb @@ -161,7 +161,7 @@ def presence_validation? end def form_field_counter - element.definition.ingredients.index { |i| i.role == role } + ingredient.id end # Renders the translated role of ingredient. diff --git a/app/decorators/alchemy/ingredient_editor.rb b/app/decorators/alchemy/ingredient_editor.rb index 55af8c76c3..5d59d09977 100644 --- a/app/decorators/alchemy/ingredient_editor.rb +++ b/app/decorators/alchemy/ingredient_editor.rb @@ -164,7 +164,7 @@ def presence_validation? private def form_field_counter - element.ingredient_definitions.index { _1.role == role } + id end end end diff --git a/spec/components/alchemy/ingredients/base_editor_spec.rb b/spec/components/alchemy/ingredients/base_editor_spec.rb index 52d6e855dd..5279a2f239 100644 --- a/spec/components/alchemy/ingredients/base_editor_spec.rb +++ b/spec/components/alchemy/ingredients/base_editor_spec.rb @@ -88,12 +88,12 @@ describe "#form_field_name" do it "returns a name for form fields with value as default" do - expect(ingredient_editor.form_field_name).to eq("element[ingredients_attributes][1][value]") + expect(ingredient_editor.form_field_name).to eq("element[ingredients_attributes][#{ingredient.id}][value]") end context "with a value given" do it "returns a name for form fields for that column" do - expect(ingredient_editor.form_field_name(:link_title)).to eq("element[ingredients_attributes][1][link_title]") + expect(ingredient_editor.form_field_name(:link_title)).to eq("element[ingredients_attributes][#{ingredient.id}][link_title]") end end end diff --git a/spec/decorators/alchemy/ingredient_editor_spec.rb b/spec/decorators/alchemy/ingredient_editor_spec.rb index 08c8fe9925..17cba85609 100644 --- a/spec/decorators/alchemy/ingredient_editor_spec.rb +++ b/spec/decorators/alchemy/ingredient_editor_spec.rb @@ -4,7 +4,7 @@ RSpec.describe Alchemy::IngredientEditor, :silence_deprecations do let(:element) { build(:alchemy_element, name: "article") } - let(:ingredient) { Alchemy::Ingredients::Text.new(role: "headline", element: element) } + let(:ingredient) { Alchemy::Ingredients::Text.new(id: 123, role: "headline", element: element) } let(:ingredient_editor) { described_class.new(ingredient) } describe "#ingredient" do @@ -99,12 +99,12 @@ describe "#form_field_name" do it "returns a name for form fields with value as default" do - expect(ingredient_editor.form_field_name).to eq("element[ingredients_attributes][1][value]") + expect(ingredient_editor.form_field_name).to eq("element[ingredients_attributes][123][value]") end context "with a value given" do it "returns a name for form fields for that column" do - expect(ingredient_editor.form_field_name(:link_title)).to eq("element[ingredients_attributes][1][link_title]") + expect(ingredient_editor.form_field_name(:link_title)).to eq("element[ingredients_attributes][123][link_title]") end end end