add SettingsField struct and repeater field type for block settings
All checks were successful
deploy / deploy (push) Successful in 1m23s

Introduces typed settings schema with SettingsField struct, replaces
the read-only JSON textarea with a full repeater UI for info_card items.
Supports add, remove, reorder and inline editing of repeater items.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-27 00:54:13 +00:00
parent 3f97742c0b
commit 6fbd654d57
5 changed files with 470 additions and 52 deletions

View File

@@ -478,4 +478,153 @@ defmodule BerrypodWeb.Admin.PagesTest do
assert has_element?(view, "#block-#{hero["id"]}.block-card-expanded")
end
end
describe "repeater fields" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "repeater items render with input fields", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/contact")
page = Pages.get_page("contact")
info_card = Enum.find(page.blocks, &(&1["type"] == "info_card"))
render_click(view, "toggle_expand", %{"id" => info_card["id"]})
# Should render 3 default items with label/value inputs
html = render(view)
assert html =~ "Printing"
assert html =~ "Delivery"
assert html =~ "Issues"
end
test "editing a repeater item field sets dirty flag", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/contact")
page = Pages.get_page("contact")
info_card = Enum.find(page.blocks, &(&1["type"] == "info_card"))
render_click(view, "toggle_expand", %{"id" => info_card["id"]})
render_change(view, "update_block_settings", %{
"block_id" => info_card["id"],
"block_settings" => %{
"title" => "Handy to know",
"items" => %{
"0" => %{"label" => "Shipping", "value" => "1-3 business days"},
"1" => %{
"label" => "Delivery",
"value" => "Example: 3-7 business days after printing"
},
"2" => %{"label" => "Issues", "value" => "Example: Reprints for any defects"}
}
}
})
assert has_element?(view, ".admin-badge-warning", "Unsaved changes")
end
test "adding a repeater item appends an empty item", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/contact")
page = Pages.get_page("contact")
info_card = Enum.find(page.blocks, &(&1["type"] == "info_card"))
render_click(view, "toggle_expand", %{"id" => info_card["id"]})
# Should have 3 items initially
assert render(view) |> count_repeater_items() == 3
render_click(view, "repeater_add", %{
"block-id" => info_card["id"],
"field" => "items"
})
# Now 4 items
assert render(view) |> count_repeater_items() == 4
assert has_element?(view, ".admin-badge-warning", "Unsaved changes")
end
test "removing a repeater item removes it from the list", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/contact")
page = Pages.get_page("contact")
info_card = Enum.find(page.blocks, &(&1["type"] == "info_card"))
render_click(view, "toggle_expand", %{"id" => info_card["id"]})
render_click(view, "repeater_remove", %{
"block-id" => info_card["id"],
"field" => "items",
"index" => "0"
})
html = render(view)
assert count_repeater_items(html) == 2
# "Printing" was item 0, should be gone
refute html =~ "Printing"
assert html =~ "Delivery"
end
test "moving a repeater item changes order", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/contact")
page = Pages.get_page("contact")
info_card = Enum.find(page.blocks, &(&1["type"] == "info_card"))
render_click(view, "toggle_expand", %{"id" => info_card["id"]})
render_click(view, "repeater_move", %{
"block-id" => info_card["id"],
"field" => "items",
"index" => "0",
"dir" => "down"
})
# Save and check the persisted order
render_click(view, "save")
saved = Pages.get_page("contact")
saved_info = Enum.find(saved.blocks, &(&1["type"] == "info_card"))
labels = Enum.map(saved_info["settings"]["items"], & &1["label"])
# "Printing" moved from 0 to 1
assert labels == ["Delivery", "Printing", "Issues"]
end
test "repeater changes persist after save", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/contact")
page = Pages.get_page("contact")
info_card = Enum.find(page.blocks, &(&1["type"] == "info_card"))
render_click(view, "toggle_expand", %{"id" => info_card["id"]})
# Edit item via phx-change
render_change(view, "update_block_settings", %{
"block_id" => info_card["id"],
"block_settings" => %{
"title" => "Good to know",
"items" => %{
"0" => %{"label" => "Shipping", "value" => "Fast shipping"},
"1" => %{"label" => "Returns", "value" => "Easy returns"}
}
}
})
render_click(view, "save")
saved = Pages.get_page("contact")
saved_info = Enum.find(saved.blocks, &(&1["type"] == "info_card"))
assert saved_info["settings"]["title"] == "Good to know"
assert length(saved_info["settings"]["items"]) == 2
assert Enum.at(saved_info["settings"]["items"], 0)["label"] == "Shipping"
assert Enum.at(saved_info["settings"]["items"], 0)["value"] == "Fast shipping"
end
end
defp count_repeater_items(html) do
Regex.scan(~r/class="repeater-item"/, html) |> length()
end
end