berrypod/lib/berrypod_web/live/admin/pages/editor.ex

981 lines
29 KiB
Elixir
Raw Normal View History

defmodule BerrypodWeb.Admin.Pages.Editor do
use BerrypodWeb, :live_view
alias Berrypod.{Media, Pages}
alias Berrypod.Pages.{BlockTypes, Defaults}
alias Berrypod.Products.ProductImage
alias Berrypod.Theme.{Fonts, PreviewData}
@impl true
def mount(%{"slug" => slug}, _session, socket) do
page = Pages.get_page(slug)
allowed_blocks = BlockTypes.allowed_for(slug)
preview_data = %{
products: PreviewData.products(),
categories: PreviewData.categories(),
cart_items: PreviewData.cart_items()
}
{:ok,
socket
|> assign(:page_title, page.title)
|> assign(:slug, slug)
|> assign(:page_data, page)
|> assign(:blocks, page.blocks)
|> assign(:allowed_blocks, allowed_blocks)
|> assign(:dirty, false)
|> assign(:show_picker, false)
|> assign(:picker_filter, "")
|> assign(:expanded, MapSet.new())
|> assign(:live_region_message, nil)
|> assign(:show_preview, false)
|> assign(:preview_data, preview_data)
|> assign(:logo_image, Media.get_logo())
|> assign(:header_image, Media.get_header())}
end
@impl true
def handle_event("move_up", %{"id" => block_id}, socket) do
blocks = socket.assigns.blocks
idx = Enum.find_index(blocks, &(&1["id"] == block_id))
if idx && idx > 0 do
block = Enum.at(blocks, idx)
block_name = block_display_name(block)
new_pos = idx
new_blocks =
blocks
|> List.delete_at(idx)
|> List.insert_at(idx - 1, block)
{:noreply,
socket
|> assign(:blocks, new_blocks)
|> assign(:dirty, true)
|> assign(:live_region_message, "#{block_name} moved to position #{new_pos}")}
else
{:noreply, socket}
end
end
def handle_event("move_down", %{"id" => block_id}, socket) do
blocks = socket.assigns.blocks
idx = Enum.find_index(blocks, &(&1["id"] == block_id))
if idx && idx < length(blocks) - 1 do
block = Enum.at(blocks, idx)
block_name = block_display_name(block)
new_pos = idx + 2
new_blocks =
blocks
|> List.delete_at(idx)
|> List.insert_at(idx + 1, block)
{:noreply,
socket
|> assign(:blocks, new_blocks)
|> assign(:dirty, true)
|> assign(:live_region_message, "#{block_name} moved to position #{new_pos}")}
else
{:noreply, socket}
end
end
def handle_event("remove_block", %{"id" => block_id}, socket) do
block = Enum.find(socket.assigns.blocks, &(&1["id"] == block_id))
block_name = block_display_name(block)
new_blocks = Enum.reject(socket.assigns.blocks, &(&1["id"] == block_id))
{:noreply,
socket
|> assign(:blocks, new_blocks)
|> assign(:dirty, true)
|> assign(:live_region_message, "#{block_name} removed")}
end
def handle_event("duplicate_block", %{"id" => block_id}, socket) do
blocks = socket.assigns.blocks
idx = Enum.find_index(blocks, &(&1["id"] == block_id))
if idx do
original = Enum.at(blocks, idx)
copy = %{
"id" => Defaults.generate_block_id(),
"type" => original["type"],
"settings" => original["settings"] || %{}
}
block_name = block_display_name(original)
new_blocks = List.insert_at(blocks, idx + 1, copy)
{:noreply,
socket
|> assign(:blocks, new_blocks)
|> assign(:dirty, true)
|> assign(:live_region_message, "#{block_name} duplicated")}
else
{:noreply, socket}
end
end
def handle_event("toggle_expand", %{"id" => block_id}, socket) do
expanded = socket.assigns.expanded
block = Enum.find(socket.assigns.blocks, &(&1["id"] == block_id))
block_name = block_display_name(block)
{new_expanded, action} =
if MapSet.member?(expanded, block_id) do
{MapSet.delete(expanded, block_id), "collapsed"}
else
{MapSet.put(expanded, block_id), "expanded"}
end
{:noreply,
socket
|> assign(:expanded, new_expanded)
|> assign(:live_region_message, "#{block_name} settings #{action}")}
end
def handle_event("update_block_settings", params, socket) do
block_id = params["block_id"]
new_settings = params["block_settings"] || %{}
# Find the block and its schema to coerce types
block = Enum.find(socket.assigns.blocks, &(&1["id"] == block_id))
if block do
schema =
case BlockTypes.get(block["type"]) do
%{settings_schema: s} -> s
_ -> []
end
coerced = coerce_settings(new_settings, schema)
new_blocks =
Enum.map(socket.assigns.blocks, fn b ->
if b["id"] == block_id do
Map.put(b, "settings", Map.merge(b["settings"] || %{}, coerced))
else
b
end
end)
{:noreply,
socket
|> assign(:blocks, new_blocks)
|> assign(:dirty, true)}
else
{:noreply, socket}
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
|> assign(:show_picker, true)
|> assign(:picker_filter, "")}
end
def handle_event("hide_picker", _params, socket) do
{:noreply, assign(socket, :show_picker, false)}
end
def handle_event("filter_picker", %{"value" => value}, socket) do
{:noreply, assign(socket, :picker_filter, value)}
end
def handle_event("add_block", %{"type" => type}, socket) do
block_def = BlockTypes.get(type)
if block_def do
# Build default settings from schema
default_settings =
block_def
|> Map.get(:settings_schema, [])
|> Enum.into(%{}, fn field -> {field.key, field.default} end)
new_block = %{
"id" => Defaults.generate_block_id(),
"type" => type,
"settings" => default_settings
}
{:noreply,
socket
|> assign(:blocks, socket.assigns.blocks ++ [new_block])
|> assign(:dirty, true)
|> assign(:show_picker, false)
|> assign(:live_region_message, "#{block_def.name} added")}
else
{:noreply, socket}
end
end
def handle_event("save", _params, socket) do
%{slug: slug, blocks: blocks, page_data: page_data} = socket.assigns
case Pages.save_page(slug, %{title: page_data.title, blocks: blocks}) do
{:ok, _page} ->
{:noreply,
socket
|> assign(:dirty, false)
|> put_flash(:info, "Page saved")}
{:error, _changeset} ->
{:noreply, put_flash(socket, :error, "Failed to save page")}
end
end
def handle_event("reset_defaults", _params, socket) do
slug = socket.assigns.slug
:ok = Pages.reset_page(slug)
page = Pages.get_page(slug)
{:noreply,
socket
|> assign(:blocks, page.blocks)
|> assign(:dirty, false)
|> put_flash(:info, "Page reset to defaults")}
end
def handle_event("toggle_preview", _params, socket) do
{:noreply, assign(socket, :show_preview, !socket.assigns.show_preview)}
end
@impl true
def render(assigns) do
~H"""
<div id="page-editor" phx-hook="DirtyGuard" data-dirty={to_string(@dirty)}>
<.link
navigate={~p"/admin/pages"}
class="text-sm font-normal text-base-content/60 hover:underline"
>
&larr; Pages
</.link>
<.header>
{@page_data.title}
<:actions>
<button
phx-click="toggle_preview"
class="admin-btn admin-btn-sm admin-btn-ghost page-editor-toggle-preview"
>
<.icon
name={if @show_preview, do: "hero-pencil-square", else: "hero-eye"}
class="size-4"
/>
{if @show_preview, do: "Edit", else: "Preview"}
</button>
<button
phx-click="reset_defaults"
data-confirm="Reset this page to its default layout? Your changes will be lost."
class="admin-btn admin-btn-sm admin-btn-ghost"
>
Reset to defaults
</button>
<button
phx-click="save"
class={["admin-btn admin-btn-sm admin-btn-primary", !@dirty && "opacity-50"]}
disabled={!@dirty}
>
Save
</button>
</:actions>
</.header>
<%!-- ARIA live region for screen reader announcements --%>
<div aria-live="polite" aria-atomic="true" class="sr-only">
{if @live_region_message, do: @live_region_message}
</div>
<%!-- Unsaved changes indicator --%>
<p :if={@dirty} class="admin-badge admin-badge-warning mt-4">
Unsaved changes
</p>
<div class="page-editor-container">
<%!-- Editor pane --%>
<div class={[
"page-editor-pane",
@show_preview && "page-editor-pane-hidden-mobile"
]}>
<%!-- Block list --%>
<div class="block-list" role="list" aria-label="Page blocks">
<.block_card
:for={{block, idx} <- Enum.with_index(@blocks)}
block={block}
idx={idx}
total={length(@blocks)}
expanded={@expanded}
/>
<div :if={@blocks == []} class="block-list-empty">
<p>No blocks on this page yet.</p>
</div>
</div>
<%!-- Add block button --%>
<div class="block-actions">
<button phx-click="show_picker" class="admin-btn admin-btn-outline block-add-btn">
<.icon name="hero-plus" class="size-4" /> Add block
</button>
</div>
</div>
<%!-- Preview pane --%>
<div class={[
"page-editor-preview-pane",
!@show_preview && "page-editor-preview-hidden-mobile"
]}>
<.preview_pane
slug={@slug}
blocks={@blocks}
page_data={@page_data}
preview_data={@preview_data}
theme_settings={@theme_settings}
generated_css={@generated_css}
logo_image={@logo_image}
header_image={@header_image}
/>
</div>
</div>
<%!-- Block picker modal --%>
<.block_picker
:if={@show_picker}
allowed_blocks={@allowed_blocks}
filter={@picker_filter}
/>
</div>
"""
end
# ── Preview ───────────────────────────────────────────────────────
defp preview_pane(assigns) do
# Build a temporary page struct from working state
page = %{slug: assigns.slug, title: assigns.page_data.title, blocks: assigns.blocks}
preview =
assigns
|> assign(:page, page)
|> assign(:mode, :preview)
|> assign(:products, assigns.preview_data.products)
|> assign(:categories, assigns.preview_data.categories)
|> assign(:cart_items, PreviewData.cart_drawer_items())
|> assign(:cart_count, 2)
|> assign(:cart_subtotal, "£72.00")
|> assign(:cart_drawer_open, false)
|> preview_page_context(assigns.slug)
extra = Pages.load_block_data(page.blocks, preview)
assigns = assign(assigns, :preview, assign(preview, extra))
~H"""
<div
class="page-editor-preview themed"
data-mood={@theme_settings.mood}
data-typography={@theme_settings.typography}
data-shape={@theme_settings.shape}
data-density={@theme_settings.density}
data-grid={@theme_settings.grid_columns}
data-header={@theme_settings.header_layout}
data-sticky={to_string(@theme_settings.sticky_header)}
data-layout={@theme_settings.layout_width}
data-shadow={@theme_settings.card_shadow}
data-button-style={@theme_settings.button_style}
>
<style>
<%= Phoenix.HTML.raw(Fonts.generate_all_font_faces(&BerrypodWeb.Endpoint.static_path/1)) %>
<%= Phoenix.HTML.raw(@generated_css) %>
</style>
<BerrypodWeb.PageRenderer.render_page {@preview} />
</div>
"""
end
defp preview_page_context(assigns, "pdp") do
product = List.first(assigns.preview_data.products)
option_types = Map.get(product, :option_types) || []
variants = Map.get(product, :variants) || []
{selected_options, selected_variant} =
case variants do
[first | _] -> {first.options, first}
[] -> {%{}, nil}
end
available_options =
Enum.reduce(option_types, %{}, fn opt, acc ->
values = Enum.map(opt.values, & &1.title)
Map.put(acc, opt.name, values)
end)
display_price =
if selected_variant, do: selected_variant.price, else: product.cheapest_price
assigns
|> assign(:product, product)
|> assign(:gallery_images, build_gallery_images(product))
|> assign(:option_types, option_types)
|> assign(:selected_options, selected_options)
|> assign(:available_options, available_options)
|> assign(:display_price, display_price)
|> assign(:quantity, 1)
|> assign(:option_urls, %{})
end
defp preview_page_context(assigns, "cart") do
cart_items = assigns.preview_data.cart_items
subtotal =
Enum.reduce(cart_items, 0, fn item, acc ->
acc + item.product.cheapest_price * item.quantity
end)
assigns
|> assign(:cart_page_items, cart_items)
|> assign(:cart_page_subtotal, subtotal)
end
defp preview_page_context(assigns, "about"),
do: assign(assigns, :content_blocks, PreviewData.about_content())
defp preview_page_context(assigns, "delivery"),
do: assign(assigns, :content_blocks, PreviewData.delivery_content())
defp preview_page_context(assigns, "privacy"),
do: assign(assigns, :content_blocks, PreviewData.privacy_content())
defp preview_page_context(assigns, "terms"),
do: assign(assigns, :content_blocks, PreviewData.terms_content())
defp preview_page_context(assigns, "error") do
assign(assigns, %{
error_code: "404",
error_title: "Page Not Found",
error_description:
"Sorry, we couldn't find the page you're looking for. Perhaps you've mistyped the URL or the page has been moved."
})
end
defp preview_page_context(assigns, _slug), do: assigns
defp build_gallery_images(product) do
(Map.get(product, :images) || [])
|> Enum.sort_by(& &1.position)
|> Enum.map(fn img -> ProductImage.url(img, 1200) end)
|> Enum.reject(&is_nil/1)
end
# ── Block card ─────────────────────────────────────────────────────
defp block_card(assigns) do
block_type = BlockTypes.get(assigns.block["type"])
has_settings = has_settings?(assigns.block)
expanded = MapSet.member?(assigns.expanded, assigns.block["id"])
assigns =
assigns
|> assign(:block_type, block_type)
|> assign(:has_settings, has_settings)
|> assign(:is_expanded, expanded)
~H"""
<div
class={["block-card", @is_expanded && "block-card-expanded"]}
role="listitem"
aria-label={"#{@block_type && @block_type.name || @block["type"]}, position #{@idx + 1} of #{@total}"}
id={"block-#{@block["id"]}"}
>
<div class="block-card-header">
<span class="block-card-position">{@idx + 1}</span>
<span class="block-card-icon">
<.icon name={(@block_type && @block_type.icon) || "hero-puzzle-piece"} class="size-5" />
</span>
<span class="block-card-name">
{(@block_type && @block_type.name) || @block["type"]}
</span>
<span class="block-card-controls">
<button
:if={@has_settings}
phx-click="toggle_expand"
phx-value-id={@block["id"]}
class={[
"admin-btn admin-btn-ghost admin-btn-icon admin-btn-sm",
@is_expanded && "block-edit-btn-active"
]}
aria-label={"Edit #{@block_type && @block_type.name} settings"}
aria-expanded={to_string(@is_expanded)}
aria-controls={"block-settings-#{@block["id"]}"}
id={"block-edit-btn-#{@block["id"]}"}
>
<.icon name="hero-cog-6-tooth-mini" class="size-4" />
</button>
<button
phx-click="move_up"
phx-value-id={@block["id"]}
class="admin-btn admin-btn-ghost admin-btn-icon admin-btn-sm"
aria-label={"Move #{@block_type && @block_type.name} up"}
disabled={@idx == 0}
>
<.icon name="hero-chevron-up-mini" class="size-4" />
</button>
<button
phx-click="move_down"
phx-value-id={@block["id"]}
class="admin-btn admin-btn-ghost admin-btn-icon admin-btn-sm"
aria-label={"Move #{@block_type && @block_type.name} down"}
disabled={@idx == @total - 1}
>
<.icon name="hero-chevron-down-mini" class="size-4" />
</button>
<button
phx-click="duplicate_block"
phx-value-id={@block["id"]}
class="admin-btn admin-btn-ghost admin-btn-icon admin-btn-sm"
aria-label={"Duplicate #{@block_type && @block_type.name}"}
>
<.icon name="hero-document-duplicate-mini" class="size-4" />
</button>
<button
phx-click="remove_block"
phx-value-id={@block["id"]}
class="admin-btn admin-btn-ghost admin-btn-icon admin-btn-sm block-remove-btn"
aria-label={"Remove #{@block_type && @block_type.name}"}
data-confirm={"Remove #{@block_type && @block_type.name}?"}
>
<.icon name="hero-trash-mini" class="size-4" />
</button>
</span>
</div>
<.block_settings_form
:if={@is_expanded}
block={@block}
schema={@block_type.settings_schema}
/>
</div>
"""
end
defp block_settings_form(assigns) do
settings = settings_with_defaults(assigns.block)
assigns = assign(assigns, :settings, settings)
~H"""
<div class="block-card-settings" id={"block-settings-#{@block["id"]}"}>
<form phx-change="update_block_settings">
<input type="hidden" name="block_id" value={@block["id"]} />
<div class="block-settings-fields">
<.block_field
:for={field <- @schema}
field={field}
value={@settings[field.key]}
block_id={@block["id"]}
/>
</div>
</form>
</div>
"""
end
defp block_field(%{field: %{type: :select}} = assigns) do
~H"""
<div class="admin-fieldset">
<label>
<span class="admin-label">{@field.label}</span>
<select
name={"block_settings[#{@field.key}]"}
id={"block-#{@block_id}-#{@field.key}"}
class="admin-select"
phx-debounce="blur"
>
{Phoenix.HTML.Form.options_for_select(@field.options, to_string(@value))}
</select>
</label>
</div>
"""
end
defp block_field(%{field: %{type: :textarea}} = assigns) do
~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"
rows="3"
phx-debounce="300"
>{@value}</textarea>
</label>
</div>
"""
end
defp block_field(%{field: %{type: :number}} = assigns) do
~H"""
<div class="admin-fieldset">
<label>
<span class="admin-label">{@field.label}</span>
<input
type="number"
name={"block_settings[#{@field.key}]"}
id={"block-#{@block_id}-#{@field.key}"}
value={@value}
class="admin-input"
phx-debounce="blur"
/>
</label>
</div>
"""
end
defp block_field(%{field: %{type: :repeater}} = assigns) do
items = if is_list(assigns.value), do: assigns.value, else: []
item_count = length(items)
assigns =
assigns
|> assign(:items, items)
|> assign(:item_count, item_count)
~H"""
<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">
<label>
<span class="admin-label">{@field.label}</span>
<input
type="text"
name={"block_settings[#{@field.key}]"}
id={"block-#{@block_id}-#{@field.key}"}
value={@value}
class="admin-input"
phx-debounce="300"
/>
</label>
</div>
"""
end
defp block_picker(assigns) do
filter = String.downcase(assigns.filter)
filtered =
assigns.allowed_blocks
|> Enum.filter(fn {_type, def} ->
filter == "" or String.contains?(String.downcase(def.name), filter)
end)
|> Enum.sort_by(fn {_type, def} -> def.name end)
assigns = assign(assigns, :filtered_blocks, filtered)
~H"""
<div class="block-picker-overlay" phx-click="hide_picker">
<div class="block-picker" phx-click-away="hide_picker">
<div class="block-picker-header">
<h3>Add a block</h3>
<button
phx-click="hide_picker"
class="admin-btn admin-btn-ghost admin-btn-icon admin-btn-sm"
aria-label="Close"
>
<.icon name="hero-x-mark" class="size-5" />
</button>
</div>
<input
type="text"
placeholder="Filter blocks..."
value={@filter}
phx-keyup="filter_picker"
phx-key=""
class="admin-input block-picker-search"
autofocus
/>
<div class="block-picker-grid">
<button
:for={{type, def} <- @filtered_blocks}
phx-click="add_block"
phx-value-type={type}
class="block-picker-item"
>
<.icon name={def.icon} class="size-5" />
<span>{def.name}</span>
</button>
<p :if={@filtered_blocks == []} class="block-picker-empty">
No matching blocks.
</p>
</div>
</div>
</div>
"""
end
defp block_display_name(nil), do: "Block"
defp block_display_name(block) do
case BlockTypes.get(block["type"]) do
%{name: name} -> name
_ -> block["type"]
end
end
defp coerce_settings(params, schema) do
type_map = Map.new(schema, fn field -> {field.key, field} end)
Map.new(params, fn {key, value} ->
case type_map[key] 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
_ -> default
end
end
defp parse_number(value, _default) when is_integer(value), do: value
defp parse_number(_value, default), do: default
defp has_settings?(block) do
case BlockTypes.get(block["type"]) do
%{settings_schema: [_ | _]} -> true
_ -> false
end
end
defp settings_with_defaults(block) do
schema =
case BlockTypes.get(block["type"]) do
%{settings_schema: s} -> s
_ -> []
end
defaults = Map.new(schema, fn field -> {field.key, field.default} end)
Map.merge(defaults, block["settings"] || %{})
end
end