add SettingsField struct and repeater field type for block settings
All checks were successful
deploy / deploy (push) Successful in 1m23s
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:
parent
3f97742c0b
commit
6fbd654d57
@ -1346,16 +1346,55 @@
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.block-settings-json {
|
||||
font-family: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
/* Repeater fields */
|
||||
|
||||
.repeater-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.block-settings-hint {
|
||||
font-size: 0.75rem;
|
||||
color: color-mix(in oklch, var(--t-text-primary) 50%, transparent);
|
||||
margin-top: 0.25rem;
|
||||
.repeater-items {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.repeater-item {
|
||||
border: 1px solid var(--t-border-default);
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.5rem;
|
||||
|
||||
& fieldset {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.repeater-item-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.repeater-item-controls {
|
||||
display: flex;
|
||||
gap: 0.125rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
|
||||
.repeater-remove-btn {
|
||||
color: var(--t-danger);
|
||||
}
|
||||
|
||||
.repeater-add-btn {
|
||||
align-self: flex-start;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
} /* @layer admin */
|
||||
|
||||
@ -7,6 +7,7 @@ defmodule Berrypod.Pages.BlockTypes do
|
||||
is present on a page.
|
||||
"""
|
||||
|
||||
alias Berrypod.Pages.SettingsField
|
||||
alias Berrypod.Products
|
||||
alias Berrypod.Theme.PreviewData
|
||||
|
||||
@ -18,13 +19,23 @@ defmodule Berrypod.Pages.BlockTypes do
|
||||
icon: "hero-megaphone",
|
||||
allowed_on: :all,
|
||||
settings_schema: [
|
||||
%{key: "title", label: "Title", type: :text, default: ""},
|
||||
%{key: "description", label: "Description", type: :textarea, default: ""},
|
||||
%{key: "cta_text", label: "Button text", type: :text, default: ""},
|
||||
%{key: "cta_href", label: "Button link", type: :text, default: ""},
|
||||
%{key: "secondary_cta_text", label: "Secondary button text", type: :text, default: ""},
|
||||
%{key: "secondary_cta_href", label: "Secondary button link", type: :text, default: ""},
|
||||
%{
|
||||
%SettingsField{key: "title", label: "Title", type: :text, default: ""},
|
||||
%SettingsField{key: "description", label: "Description", type: :textarea, default: ""},
|
||||
%SettingsField{key: "cta_text", label: "Button text", type: :text, default: ""},
|
||||
%SettingsField{key: "cta_href", label: "Button link", type: :text, default: ""},
|
||||
%SettingsField{
|
||||
key: "secondary_cta_text",
|
||||
label: "Secondary button text",
|
||||
type: :text,
|
||||
default: ""
|
||||
},
|
||||
%SettingsField{
|
||||
key: "secondary_cta_href",
|
||||
label: "Secondary button link",
|
||||
type: :text,
|
||||
default: ""
|
||||
},
|
||||
%SettingsField{
|
||||
key: "variant",
|
||||
label: "Style",
|
||||
type: :select,
|
||||
@ -38,23 +49,33 @@ defmodule Berrypod.Pages.BlockTypes do
|
||||
icon: "hero-star",
|
||||
allowed_on: :all,
|
||||
settings_schema: [
|
||||
%{key: "title", label: "Title", type: :text, default: "Featured products"},
|
||||
%{key: "product_count", label: "Number of products", type: :number, default: 8},
|
||||
%{
|
||||
%SettingsField{
|
||||
key: "title",
|
||||
label: "Title",
|
||||
type: :text,
|
||||
default: "Featured products"
|
||||
},
|
||||
%SettingsField{
|
||||
key: "product_count",
|
||||
label: "Number of products",
|
||||
type: :number,
|
||||
default: 8
|
||||
},
|
||||
%SettingsField{
|
||||
key: "layout",
|
||||
label: "Layout",
|
||||
type: :select,
|
||||
options: ~w(section grid),
|
||||
default: "section"
|
||||
},
|
||||
%{
|
||||
%SettingsField{
|
||||
key: "card_variant",
|
||||
label: "Card style",
|
||||
type: :select,
|
||||
options: ~w(featured default minimal compact),
|
||||
default: "featured"
|
||||
},
|
||||
%{
|
||||
%SettingsField{
|
||||
key: "columns",
|
||||
label: "Columns",
|
||||
type: :select,
|
||||
@ -69,11 +90,11 @@ defmodule Berrypod.Pages.BlockTypes do
|
||||
icon: "hero-photo",
|
||||
allowed_on: :all,
|
||||
settings_schema: [
|
||||
%{key: "title", label: "Title", type: :text, default: ""},
|
||||
%{key: "description", label: "Description", type: :textarea, default: ""},
|
||||
%{key: "image_url", label: "Image URL", type: :text, default: ""},
|
||||
%{key: "link_text", label: "Link text", type: :text, default: ""},
|
||||
%{key: "link_href", label: "Link URL", type: :text, default: ""}
|
||||
%SettingsField{key: "title", label: "Title", type: :text, default: ""},
|
||||
%SettingsField{key: "description", label: "Description", type: :textarea, default: ""},
|
||||
%SettingsField{key: "image_url", label: "Image URL", type: :text, default: ""},
|
||||
%SettingsField{key: "link_text", label: "Link text", type: :text, default: ""},
|
||||
%SettingsField{key: "link_href", label: "Link URL", type: :text, default: ""}
|
||||
]
|
||||
},
|
||||
"category_nav" => %{
|
||||
@ -99,8 +120,17 @@ defmodule Berrypod.Pages.BlockTypes do
|
||||
icon: "hero-information-circle",
|
||||
allowed_on: :all,
|
||||
settings_schema: [
|
||||
%{key: "title", label: "Title", type: :text, default: ""},
|
||||
%{key: "items", label: "Items", type: :json, default: []}
|
||||
%SettingsField{key: "title", label: "Title", type: :text, default: ""},
|
||||
%SettingsField{
|
||||
key: "items",
|
||||
label: "Items",
|
||||
type: :repeater,
|
||||
default: [],
|
||||
item_schema: [
|
||||
%SettingsField{key: "label", label: "Label", type: :text, default: ""},
|
||||
%SettingsField{key: "value", label: "Value", type: :text, default: ""}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"trust_badges" => %{
|
||||
@ -188,7 +218,7 @@ defmodule Berrypod.Pages.BlockTypes do
|
||||
icon: "hero-envelope",
|
||||
allowed_on: ["contact"],
|
||||
settings_schema: [
|
||||
%{key: "email", label: "Email", type: :text, default: "hello@example.com"}
|
||||
%SettingsField{key: "email", label: "Email", type: :text, default: "hello@example.com"}
|
||||
]
|
||||
},
|
||||
"order_tracking_card" => %{
|
||||
@ -205,8 +235,8 @@ defmodule Berrypod.Pages.BlockTypes do
|
||||
icon: "hero-document-text",
|
||||
allowed_on: ["about", "delivery", "privacy", "terms"],
|
||||
settings_schema: [
|
||||
%{key: "image_src", label: "Image", type: :text, default: ""},
|
||||
%{key: "image_alt", label: "Image alt text", type: :text, default: ""}
|
||||
%SettingsField{key: "image_src", label: "Image", type: :text, default: ""},
|
||||
%SettingsField{key: "image_alt", label: "Image alt text", type: :text, default: ""}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
16
lib/berrypod/pages/settings_field.ex
Normal file
16
lib/berrypod/pages/settings_field.ex
Normal file
@ -0,0 +1,16 @@
|
||||
defmodule Berrypod.Pages.SettingsField do
|
||||
@moduledoc "Defines a single field in a block's settings schema."
|
||||
|
||||
@type field_type :: :text | :textarea | :number | :select | :repeater
|
||||
@type t :: %__MODULE__{
|
||||
key: String.t(),
|
||||
label: String.t(),
|
||||
type: field_type(),
|
||||
default: term(),
|
||||
options: [String.t()] | nil,
|
||||
item_schema: [t()] | nil
|
||||
}
|
||||
|
||||
@enforce_keys [:key, :label, :type]
|
||||
defstruct [:key, :label, :type, default: nil, options: nil, item_schema: nil]
|
||||
end
|
||||
@ -2,7 +2,7 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
||||
use BerrypodWeb, :live_view
|
||||
|
||||
alias Berrypod.Pages
|
||||
alias Berrypod.Pages.{BlockTypes, Defaults}
|
||||
alias Berrypod.Pages.{BlockTypes, Defaults, SettingsField}
|
||||
|
||||
@impl true
|
||||
def mount(%{"slug" => slug}, _session, socket) do
|
||||
@ -162,6 +162,88 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("repeater_add", %{"block-id" => block_id, "field" => field_key}, socket) do
|
||||
block = Enum.find(socket.assigns.blocks, &(&1["id"] == block_id))
|
||||
|
||||
if block do
|
||||
schema = BlockTypes.get(block["type"])
|
||||
field = Enum.find(schema.settings_schema, &(&1.key == field_key))
|
||||
empty_item = Map.new(field.item_schema, fn f -> {f.key, f.default} end)
|
||||
|
||||
items = (block["settings"] || %{})[field_key] || []
|
||||
new_items = items ++ [empty_item]
|
||||
|
||||
new_blocks = update_block_field(socket.assigns.blocks, block_id, field_key, new_items)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:blocks, new_blocks)
|
||||
|> assign(:dirty, true)
|
||||
|> assign(:live_region_message, "Item added")}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
"repeater_remove",
|
||||
%{"block-id" => block_id, "field" => field_key, "index" => index_str},
|
||||
socket
|
||||
) do
|
||||
block = Enum.find(socket.assigns.blocks, &(&1["id"] == block_id))
|
||||
|
||||
if block do
|
||||
index = String.to_integer(index_str)
|
||||
items = (block["settings"] || %{})[field_key] || []
|
||||
new_items = List.delete_at(items, index)
|
||||
|
||||
new_blocks = update_block_field(socket.assigns.blocks, block_id, field_key, new_items)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:blocks, new_blocks)
|
||||
|> assign(:dirty, true)
|
||||
|> assign(:live_region_message, "Item removed")}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
"repeater_move",
|
||||
%{"block-id" => block_id, "field" => field_key, "index" => index_str, "dir" => dir},
|
||||
socket
|
||||
) do
|
||||
block = Enum.find(socket.assigns.blocks, &(&1["id"] == block_id))
|
||||
|
||||
if block do
|
||||
index = String.to_integer(index_str)
|
||||
items = (block["settings"] || %{})[field_key] || []
|
||||
target = if dir == "up", do: index - 1, else: index + 1
|
||||
|
||||
if target >= 0 and target < length(items) do
|
||||
item = Enum.at(items, index)
|
||||
|
||||
new_items =
|
||||
items
|
||||
|> List.delete_at(index)
|
||||
|> List.insert_at(target, item)
|
||||
|
||||
new_blocks = update_block_field(socket.assigns.blocks, block_id, field_key, new_items)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:blocks, new_blocks)
|
||||
|> assign(:dirty, true)
|
||||
|> assign(:live_region_message, "Item moved #{dir}")}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("show_picker", _params, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
@ -470,34 +552,114 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
||||
"""
|
||||
end
|
||||
|
||||
defp block_field(%{field: %{type: :json}} = assigns) do
|
||||
json_str =
|
||||
case assigns.value do
|
||||
val when is_list(val) or is_map(val) -> Jason.encode!(val, pretty: true)
|
||||
val when is_binary(val) -> val
|
||||
_ -> "[]"
|
||||
end
|
||||
defp block_field(%{field: %{type: :repeater}} = assigns) do
|
||||
items = if is_list(assigns.value), do: assigns.value, else: []
|
||||
item_count = length(items)
|
||||
|
||||
assigns = assign(assigns, :json_str, json_str)
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:items, items)
|
||||
|> assign(:item_count, item_count)
|
||||
|
||||
~H"""
|
||||
<div class="admin-fieldset">
|
||||
<label>
|
||||
<span class="admin-label">{@field.label}</span>
|
||||
<textarea
|
||||
name={"block_settings[#{@field.key}]"}
|
||||
id={"block-#{@block_id}-#{@field.key}"}
|
||||
class="admin-textarea block-settings-json"
|
||||
rows="4"
|
||||
readonly
|
||||
aria-readonly="true"
|
||||
>{@json_str}</textarea>
|
||||
<p class="block-settings-hint">JSON data — editing coming soon</p>
|
||||
</label>
|
||||
<div class="repeater-field">
|
||||
<span class="admin-label">{@field.label}</span>
|
||||
|
||||
<ol class="repeater-items" aria-label={@field.label}>
|
||||
<li :for={{item, idx} <- Enum.with_index(@items)} class="repeater-item">
|
||||
<fieldset>
|
||||
<legend class="sr-only">
|
||||
Item {idx + 1}: {item["label"] || item[:label] || "New item"}
|
||||
</legend>
|
||||
|
||||
<div class="repeater-item-fields">
|
||||
<.repeater_item_field
|
||||
:for={sub_field <- @field.item_schema}
|
||||
sub_field={sub_field}
|
||||
item={item}
|
||||
field_key={@field.key}
|
||||
block_id={@block_id}
|
||||
index={idx}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="repeater-item-controls">
|
||||
<button
|
||||
type="button"
|
||||
phx-click="repeater_move"
|
||||
phx-value-block-id={@block_id}
|
||||
phx-value-field={@field.key}
|
||||
phx-value-index={idx}
|
||||
phx-value-dir="up"
|
||||
class="admin-btn admin-btn-ghost admin-btn-icon admin-btn-xs"
|
||||
aria-label={"Move item #{idx + 1} up"}
|
||||
disabled={idx == 0}
|
||||
>
|
||||
<.icon name="hero-chevron-up-mini" class="size-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="repeater_move"
|
||||
phx-value-block-id={@block_id}
|
||||
phx-value-field={@field.key}
|
||||
phx-value-index={idx}
|
||||
phx-value-dir="down"
|
||||
class="admin-btn admin-btn-ghost admin-btn-icon admin-btn-xs"
|
||||
aria-label={"Move item #{idx + 1} down"}
|
||||
disabled={idx == @item_count - 1}
|
||||
>
|
||||
<.icon name="hero-chevron-down-mini" class="size-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="repeater_remove"
|
||||
phx-value-block-id={@block_id}
|
||||
phx-value-field={@field.key}
|
||||
phx-value-index={idx}
|
||||
class="admin-btn admin-btn-ghost admin-btn-icon admin-btn-xs repeater-remove-btn"
|
||||
aria-label={"Remove item #{idx + 1}"}
|
||||
>
|
||||
<.icon name="hero-x-mark-mini" class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
phx-click="repeater_add"
|
||||
phx-value-block-id={@block_id}
|
||||
phx-value-field={@field.key}
|
||||
class="admin-btn admin-btn-outline admin-btn-xs repeater-add-btn"
|
||||
>
|
||||
<.icon name="hero-plus-mini" class="size-3.5" /> Add item
|
||||
</button>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp repeater_item_field(assigns) do
|
||||
value = assigns.item[assigns.sub_field.key] || ""
|
||||
|
||||
assigns = assign(assigns, :value, value)
|
||||
|
||||
~H"""
|
||||
<label class="repeater-sub-field">
|
||||
<span class="sr-only">{@sub_field.label}</span>
|
||||
<input
|
||||
type="text"
|
||||
name={"block_settings[#{@field_key}][#{@index}][#{@sub_field.key}]"}
|
||||
id={"block-#{@block_id}-#{@field_key}-#{@index}-#{@sub_field.key}"}
|
||||
value={@value}
|
||||
placeholder={@sub_field.label}
|
||||
class="admin-input admin-input-sm"
|
||||
phx-debounce="300"
|
||||
/>
|
||||
</label>
|
||||
"""
|
||||
end
|
||||
|
||||
defp block_field(assigns) do
|
||||
~H"""
|
||||
<div class="admin-fieldset">
|
||||
@ -589,12 +751,34 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
||||
%{type: :number, default: default} ->
|
||||
{key, parse_number(value, default)}
|
||||
|
||||
%{type: :repeater} ->
|
||||
{key, indexed_map_to_list(value)}
|
||||
|
||||
_ ->
|
||||
{key, value}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp indexed_map_to_list(map) when is_map(map) do
|
||||
map
|
||||
|> Enum.sort_by(fn {k, _v} -> String.to_integer(k) end)
|
||||
|> Enum.map(fn {_k, v} -> v end)
|
||||
end
|
||||
|
||||
defp indexed_map_to_list(value), do: value
|
||||
|
||||
defp update_block_field(blocks, block_id, field_key, new_value) do
|
||||
Enum.map(blocks, fn b ->
|
||||
if b["id"] == block_id do
|
||||
settings = Map.put(b["settings"] || %{}, field_key, new_value)
|
||||
Map.put(b, "settings", settings)
|
||||
else
|
||||
b
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp parse_number(value, default) when is_binary(value) do
|
||||
case Integer.parse(value) do
|
||||
{n, ""} -> n
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user