361 lines
12 KiB
Elixir
361 lines
12 KiB
Elixir
|
|
defmodule BerrypodWeb.PageEditorHook do
|
||
|
|
@moduledoc """
|
||
|
|
LiveView on_mount hook for the live page editor sidebar.
|
||
|
|
|
||
|
|
Mounted in the public_shop live_session. When an admin visits any shop
|
||
|
|
page with `?edit=true` in the URL, this hook activates editing mode:
|
||
|
|
loads a working copy of the page's blocks, attaches event handlers for
|
||
|
|
block manipulation, and sets assigns that trigger the editor sidebar
|
||
|
|
in `PageRenderer.render_page/1`.
|
||
|
|
|
||
|
|
Non-admin users are unaffected — the hook just assigns `editing: false`.
|
||
|
|
|
||
|
|
## Actions
|
||
|
|
|
||
|
|
- `:mount_page_editor` — sets up editing assigns and attaches hooks
|
||
|
|
"""
|
||
|
|
|
||
|
|
import Phoenix.Component, only: [assign: 3]
|
||
|
|
import Phoenix.LiveView, only: [attach_hook: 4, put_flash: 3, push_navigate: 2]
|
||
|
|
|
||
|
|
alias Berrypod.Pages
|
||
|
|
alias Berrypod.Pages.{BlockEditor, BlockTypes}
|
||
|
|
|
||
|
|
def on_mount(:mount_page_editor, _params, _session, socket) do
|
||
|
|
socket =
|
||
|
|
socket
|
||
|
|
|> assign(:editing, false)
|
||
|
|
|> assign(:editing_blocks, nil)
|
||
|
|
|> assign(:editor_dirty, false)
|
||
|
|
|> assign(:editor_expanded, MapSet.new())
|
||
|
|
|> assign(:editor_show_picker, false)
|
||
|
|
|> assign(:editor_picker_filter, "")
|
||
|
|
|> assign(:editor_allowed_blocks, nil)
|
||
|
|
|> assign(:editor_live_region_message, nil)
|
||
|
|
|> assign(:editor_current_path, nil)
|
||
|
|
|> assign(:editor_sidebar_open, true)
|
||
|
|
|> attach_hook(:editor_params, :handle_params, &handle_editor_params/3)
|
||
|
|
|> attach_hook(:editor_events, :handle_event, &handle_editor_event/3)
|
||
|
|
|> attach_hook(:editor_info, :handle_info, &handle_editor_info/2)
|
||
|
|
|
||
|
|
{:cont, socket}
|
||
|
|
end
|
||
|
|
|
||
|
|
# ── handle_params: detect ?edit=true ─────────────────────────────
|
||
|
|
|
||
|
|
defp handle_editor_params(_params, uri, socket) do
|
||
|
|
parsed = URI.parse(uri)
|
||
|
|
query = URI.decode_query(parsed.query || "")
|
||
|
|
wants_edit = query["edit"] == "true"
|
||
|
|
|
||
|
|
# Always store the current path for the edit button and "done" navigation
|
||
|
|
socket = assign(socket, :editor_current_path, parsed.path)
|
||
|
|
|
||
|
|
cond do
|
||
|
|
wants_edit and socket.assigns.is_admin and socket.assigns[:page] ->
|
||
|
|
# Page already loaded — enter edit mode and halt (no need for module handle_params)
|
||
|
|
{:halt, enter_edit_mode(socket)}
|
||
|
|
|
||
|
|
wants_edit and socket.assigns.is_admin ->
|
||
|
|
# Page not loaded yet (e.g. Shop.Content loads in handle_params),
|
||
|
|
# defer initialisation until after the LiveView sets @page
|
||
|
|
send(self(), :editor_deferred_init)
|
||
|
|
{:cont, assign(socket, :editing, true)}
|
||
|
|
|
||
|
|
socket.assigns.editing and not wants_edit ->
|
||
|
|
# Exiting edit mode — halt since we've handled the transition
|
||
|
|
{:halt, exit_edit_mode(socket)}
|
||
|
|
|
||
|
|
true ->
|
||
|
|
{:cont, socket}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
# ── handle_info: deferred init ───────────────────────────────────
|
||
|
|
|
||
|
|
defp handle_editor_info(:editor_deferred_init, socket) do
|
||
|
|
if socket.assigns.editing and is_nil(socket.assigns.editing_blocks) and socket.assigns[:page] do
|
||
|
|
{:halt, enter_edit_mode(socket)}
|
||
|
|
else
|
||
|
|
{:cont, socket}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
defp handle_editor_info(_msg, socket), do: {:cont, socket}
|
||
|
|
|
||
|
|
# ── handle_event: editor_* events ────────────────────────────────
|
||
|
|
|
||
|
|
defp handle_editor_event("editor_" <> action, params, socket) do
|
||
|
|
if socket.assigns.editing do
|
||
|
|
handle_editor_action(action, params, socket)
|
||
|
|
else
|
||
|
|
{:cont, socket}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
defp handle_editor_event(_event, _params, socket), do: {:cont, socket}
|
||
|
|
|
||
|
|
# ── Block manipulation actions ───────────────────────────────────
|
||
|
|
|
||
|
|
defp handle_editor_action("move_up", %{"id" => id}, socket) do
|
||
|
|
case BlockEditor.move_up(socket.assigns.editing_blocks, id) do
|
||
|
|
{:ok, new_blocks, message} ->
|
||
|
|
{:halt, apply_mutation(socket, new_blocks, message, :reorder)}
|
||
|
|
|
||
|
|
:noop ->
|
||
|
|
{:halt, socket}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
defp handle_editor_action("move_down", %{"id" => id}, socket) do
|
||
|
|
case BlockEditor.move_down(socket.assigns.editing_blocks, id) do
|
||
|
|
{:ok, new_blocks, message} ->
|
||
|
|
{:halt, apply_mutation(socket, new_blocks, message, :reorder)}
|
||
|
|
|
||
|
|
:noop ->
|
||
|
|
{:halt, socket}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
defp handle_editor_action("remove_block", %{"id" => id}, socket) do
|
||
|
|
{:ok, new_blocks, message} = BlockEditor.remove_block(socket.assigns.editing_blocks, id)
|
||
|
|
{:halt, apply_mutation(socket, new_blocks, message, :content)}
|
||
|
|
end
|
||
|
|
|
||
|
|
defp handle_editor_action("duplicate_block", %{"id" => id}, socket) do
|
||
|
|
case BlockEditor.duplicate_block(socket.assigns.editing_blocks, id) do
|
||
|
|
{:ok, new_blocks, message} ->
|
||
|
|
{:halt, apply_mutation(socket, new_blocks, message, :content)}
|
||
|
|
|
||
|
|
:noop ->
|
||
|
|
{:halt, socket}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
defp handle_editor_action("add_block", %{"type" => type}, socket) do
|
||
|
|
case BlockEditor.add_block(socket.assigns.editing_blocks, type) do
|
||
|
|
{:ok, new_blocks, message} ->
|
||
|
|
socket =
|
||
|
|
socket
|
||
|
|
|> assign(:editing_blocks, new_blocks)
|
||
|
|
|> assign(:editor_dirty, true)
|
||
|
|
|> assign(:editor_show_picker, false)
|
||
|
|
|> assign(:editor_live_region_message, message)
|
||
|
|
|> reload_block_data(new_blocks)
|
||
|
|
|
||
|
|
{:halt, socket}
|
||
|
|
|
||
|
|
:noop ->
|
||
|
|
{:halt, socket}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
defp handle_editor_action("update_block_settings", params, socket) do
|
||
|
|
block_id = params["block_id"]
|
||
|
|
new_settings = params["block_settings"] || %{}
|
||
|
|
|
||
|
|
case BlockEditor.update_settings(socket.assigns.editing_blocks, block_id, new_settings) do
|
||
|
|
{:ok, new_blocks} ->
|
||
|
|
socket =
|
||
|
|
socket
|
||
|
|
|> assign(:editing_blocks, new_blocks)
|
||
|
|
|> assign(:editor_dirty, true)
|
||
|
|
|> reload_block_data(new_blocks)
|
||
|
|
|
||
|
|
{:halt, socket}
|
||
|
|
|
||
|
|
:noop ->
|
||
|
|
{:halt, socket}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
# ── Repeater actions ─────────────────────────────────────────────
|
||
|
|
|
||
|
|
defp handle_editor_action(
|
||
|
|
"repeater_add",
|
||
|
|
%{"block-id" => block_id, "field" => field_key},
|
||
|
|
socket
|
||
|
|
) do
|
||
|
|
case BlockEditor.repeater_add(socket.assigns.editing_blocks, block_id, field_key) do
|
||
|
|
{:ok, new_blocks, message} ->
|
||
|
|
{:halt, apply_mutation(socket, new_blocks, message, :content)}
|
||
|
|
|
||
|
|
:noop ->
|
||
|
|
{:halt, socket}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
defp handle_editor_action(
|
||
|
|
"repeater_remove",
|
||
|
|
%{"block-id" => block_id, "field" => field_key, "index" => index_str},
|
||
|
|
socket
|
||
|
|
) do
|
||
|
|
index = String.to_integer(index_str)
|
||
|
|
|
||
|
|
case BlockEditor.repeater_remove(socket.assigns.editing_blocks, block_id, field_key, index) do
|
||
|
|
{:ok, new_blocks, message} ->
|
||
|
|
{:halt, apply_mutation(socket, new_blocks, message, :content)}
|
||
|
|
|
||
|
|
:noop ->
|
||
|
|
{:halt, socket}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
defp handle_editor_action(
|
||
|
|
"repeater_move",
|
||
|
|
%{"block-id" => block_id, "field" => field_key, "index" => index_str, "dir" => dir},
|
||
|
|
socket
|
||
|
|
) do
|
||
|
|
index = String.to_integer(index_str)
|
||
|
|
|
||
|
|
case BlockEditor.repeater_move(
|
||
|
|
socket.assigns.editing_blocks,
|
||
|
|
block_id,
|
||
|
|
field_key,
|
||
|
|
index,
|
||
|
|
dir
|
||
|
|
) do
|
||
|
|
{:ok, new_blocks, message} ->
|
||
|
|
{:halt, apply_mutation(socket, new_blocks, message, :reorder)}
|
||
|
|
|
||
|
|
:noop ->
|
||
|
|
{:halt, socket}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
# ── UI state actions ─────────────────────────────────────────────
|
||
|
|
|
||
|
|
defp handle_editor_action("toggle_expand", %{"id" => block_id}, socket) do
|
||
|
|
expanded = socket.assigns.editor_expanded
|
||
|
|
block = Enum.find(socket.assigns.editing_blocks, &(&1["id"] == block_id))
|
||
|
|
block_name = BlockEditor.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
|
||
|
|
|
||
|
|
{:halt,
|
||
|
|
socket
|
||
|
|
|> assign(:editor_expanded, new_expanded)
|
||
|
|
|> assign(:editor_live_region_message, "#{block_name} settings #{action}")}
|
||
|
|
end
|
||
|
|
|
||
|
|
defp handle_editor_action("toggle_sidebar", _params, socket) do
|
||
|
|
{:halt, assign(socket, :editor_sidebar_open, !socket.assigns.editor_sidebar_open)}
|
||
|
|
end
|
||
|
|
|
||
|
|
defp handle_editor_action("show_picker", _params, socket) do
|
||
|
|
{:halt,
|
||
|
|
socket
|
||
|
|
|> assign(:editor_show_picker, true)
|
||
|
|
|> assign(:editor_picker_filter, "")}
|
||
|
|
end
|
||
|
|
|
||
|
|
defp handle_editor_action("hide_picker", _params, socket) do
|
||
|
|
{:halt, assign(socket, :editor_show_picker, false)}
|
||
|
|
end
|
||
|
|
|
||
|
|
defp handle_editor_action("filter_picker", %{"value" => value}, socket) do
|
||
|
|
{:halt, assign(socket, :editor_picker_filter, value)}
|
||
|
|
end
|
||
|
|
|
||
|
|
# ── Page actions ─────────────────────────────────────────────────
|
||
|
|
|
||
|
|
defp handle_editor_action("save", _params, socket) do
|
||
|
|
%{page: page, editing_blocks: blocks} = socket.assigns
|
||
|
|
|
||
|
|
case Pages.save_page(page.slug, %{title: page.title, blocks: blocks}) do
|
||
|
|
{:ok, _saved_page} ->
|
||
|
|
updated_page = Pages.get_page(page.slug)
|
||
|
|
|
||
|
|
socket =
|
||
|
|
socket
|
||
|
|
|> assign(:page, updated_page)
|
||
|
|
|> assign(:editing_blocks, updated_page.blocks)
|
||
|
|
|> assign(:editor_dirty, false)
|
||
|
|
|> put_flash(:info, "Page saved")
|
||
|
|
|
||
|
|
{:halt, socket}
|
||
|
|
|
||
|
|
{:error, _changeset} ->
|
||
|
|
{:halt, put_flash(socket, :error, "Failed to save page")}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
defp handle_editor_action("reset_defaults", _params, socket) do
|
||
|
|
slug = socket.assigns.page.slug
|
||
|
|
:ok = Pages.reset_page(slug)
|
||
|
|
page = Pages.get_page(slug)
|
||
|
|
|
||
|
|
socket =
|
||
|
|
socket
|
||
|
|
|> assign(:page, page)
|
||
|
|
|> assign(:editing_blocks, page.blocks)
|
||
|
|
|> assign(:editor_dirty, false)
|
||
|
|
|> reload_block_data(page.blocks)
|
||
|
|
|> put_flash(:info, "Page reset to defaults")
|
||
|
|
|
||
|
|
{:halt, socket}
|
||
|
|
end
|
||
|
|
|
||
|
|
defp handle_editor_action("done", _params, socket) do
|
||
|
|
path = socket.assigns.editor_current_path || "/"
|
||
|
|
{:halt, push_navigate(socket, to: path)}
|
||
|
|
end
|
||
|
|
|
||
|
|
# Catch-all for unknown editor actions
|
||
|
|
defp handle_editor_action(_action, _params, socket), do: {:halt, socket}
|
||
|
|
|
||
|
|
# ── Private helpers ──────────────────────────────────────────────
|
||
|
|
|
||
|
|
defp enter_edit_mode(socket) do
|
||
|
|
page = socket.assigns.page
|
||
|
|
allowed = BlockTypes.allowed_for(page.slug)
|
||
|
|
|
||
|
|
socket
|
||
|
|
|> assign(:editing, true)
|
||
|
|
|> assign(:editing_blocks, page.blocks)
|
||
|
|
|> assign(:editor_dirty, false)
|
||
|
|
|> assign(:editor_expanded, MapSet.new())
|
||
|
|
|> assign(:editor_show_picker, false)
|
||
|
|
|> assign(:editor_picker_filter, "")
|
||
|
|
|> assign(:editor_allowed_blocks, allowed)
|
||
|
|
|> assign(:editor_live_region_message, nil)
|
||
|
|
|> assign(:editor_sidebar_open, true)
|
||
|
|
end
|
||
|
|
|
||
|
|
defp exit_edit_mode(socket) do
|
||
|
|
socket
|
||
|
|
|> assign(:editing, false)
|
||
|
|
|> assign(:editing_blocks, nil)
|
||
|
|
|> assign(:editor_dirty, false)
|
||
|
|
|> assign(:editor_expanded, MapSet.new())
|
||
|
|
|> assign(:editor_show_picker, false)
|
||
|
|
|> assign(:editor_picker_filter, "")
|
||
|
|
|> assign(:editor_allowed_blocks, nil)
|
||
|
|
|> assign(:editor_live_region_message, nil)
|
||
|
|
|> assign(:editor_sidebar_open, true)
|
||
|
|
end
|
||
|
|
|
||
|
|
defp apply_mutation(socket, new_blocks, message, type) do
|
||
|
|
socket =
|
||
|
|
socket
|
||
|
|
|> assign(:editing_blocks, new_blocks)
|
||
|
|
|> assign(:editor_dirty, true)
|
||
|
|
|> assign(:editor_live_region_message, message)
|
||
|
|
|
||
|
|
case type do
|
||
|
|
:content -> reload_block_data(socket, new_blocks)
|
||
|
|
:reorder -> socket
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
defp reload_block_data(socket, blocks) do
|
||
|
|
extra = Pages.load_block_data(blocks, socket.assigns)
|
||
|
|
Enum.reduce(extra, socket, fn {key, value}, acc -> assign(acc, key, value) end)
|
||
|
|
end
|
||
|
|
end
|