Current section
Files
Jump to
Current section
Files
lib/phoenix_kit_publishing/web/editor/preview.ex
defmodule PhoenixKit.Modules.Publishing.Web.Editor.Preview do
@moduledoc """
Preview functionality for the publishing editor.
Handles preview payload building, preview mode initialization,
and preview-related state management.
"""
require Logger
alias PhoenixKit.Modules.Publishing
alias PhoenixKit.Modules.Publishing.Web.Editor.Forms
alias PhoenixKit.Modules.Publishing.Web.Editor.Helpers
alias PhoenixKit.Utils.Routes
# ============================================================================
# Preview Payload Building
# ============================================================================
@doc """
Builds the preview payload from the current socket state.
"""
def build_preview_payload(socket) do
form = socket.assigns.form || %{}
post = socket.assigns.post
metadata = %{
status: map_get_with_fallback(form, "status", metadata_value(post, :status), "draft"),
published_at:
map_get_with_fallback(
form,
"published_at",
metadata_value(post, :published_at),
""
),
slug: preview_slug(form, post),
featured_image_uuid: Map.get(form, "featured_image_uuid", ""),
url_slug: Map.get(form, "url_slug", "")
}
%{
group_slug: socket.assigns.group_slug,
mode: Map.get(post, :mode) || Map.get(post, "mode") || infer_mode(socket),
language: socket.assigns.current_language,
available_languages: post.available_languages || [],
metadata: metadata,
content: socket.assigns.content || "",
is_new_post:
Map.get(socket.assigns, :is_new_post, false) ||
is_nil(post[:uuid])
}
end
defp map_get_with_fallback(map, key, fallback, default) do
case Map.get(map, key) do
nil -> fallback || default
value -> value
end
end
defp preview_slug(form, post) do
extract_form_slug(form) ||
extract_post_slug(post) ||
metadata_value(post, :slug) ||
metadata_value(post, "slug") ||
""
end
defp extract_form_slug(form) do
case Map.get(form, "slug") do
nil ->
nil
slug ->
trimmed = String.trim(to_string(slug))
if trimmed != "", do: trimmed
end
end
defp extract_post_slug(post) do
cond do
Map.get(post, :slug) && post.slug != "" -> post.slug
Map.get(post, "slug") && post["slug"] != "" -> post["slug"]
true -> nil
end
end
defp metadata_value(post, key) do
metadata = Map.get(post, :metadata) || %{}
cond do
is_atom(key) ->
Map.get(metadata, key) || Map.get(metadata, Atom.to_string(key))
is_binary(key) ->
Map.get(metadata, key) ||
try do
Map.get(metadata, String.to_existing_atom(key))
rescue
ArgumentError -> nil
end
true ->
nil
end
end
defp infer_mode(socket) do
case socket.assigns[:group_mode] do
"slug" -> :slug
:slug -> :slug
_ -> :timestamp
end
end
# ============================================================================
# Preview URL Building
# ============================================================================
@doc """
Builds the preview URL query params.
"""
def build_preview_query_params(_preview_payload, token) do
%{"preview_token" => token}
end
@doc """
Builds the editor path for preview mode.
"""
def preview_editor_path(socket, data, token, _params) do
group_slug = data[:group_slug] || socket.assigns.group_slug
query_params =
%{"preview_token" => token}
|> maybe_put_new_flag(data[:is_new_post])
query =
case URI.encode_query(query_params) do
"" -> ""
encoded -> "?" <> encoded
end
Routes.path("/admin/publishing/#{group_slug}/edit#{query}")
end
defp maybe_put_new_flag(params, true), do: Map.put(params, "new", "true")
defp maybe_put_new_flag(params, _), do: params
# ============================================================================
# Preview State Application
# ============================================================================
@doc """
Applies preview payload data to the socket.
"""
def apply_preview_payload(socket, data) do
group_slug = data[:group_slug] || socket.assigns.group_slug
mode = data[:mode] || :timestamp
language = data[:language] || socket.assigns.current_language || "en"
metadata = normalize_preview_metadata(data[:metadata] || %{}, mode)
post = build_preview_post(data, group_slug, mode, language, metadata)
{post, db_post} = enrich_from_db(post, group_slug)
form = build_preview_form(metadata, mode, db_post)
apply_preview_assigns(socket, post, form, group_slug, mode, data, db_post)
end
defp build_preview_post(data, group_slug, mode, language, metadata) do
{date, time} = derive_datetime_fields(mode, metadata[:published_at])
available_languages = data[:available_languages] || []
available_languages =
[language | available_languages] |> Enum.reject(&is_nil/1) |> Enum.uniq()
%{
group: group_slug,
slug: metadata[:slug],
date: date,
time: time,
metadata: metadata,
content: data[:content] || "",
language: language,
available_languages: available_languages,
mode: mode
}
end
defp build_preview_form(metadata, mode, db_post) do
%{
"title" => metadata[:title] || "",
"status" => metadata[:status] || "draft",
"published_at" => metadata[:published_at] || "",
"featured_image_uuid" => metadata[:featured_image_uuid] || "",
"url_slug" => metadata[:url_slug] || ""
}
|> maybe_put_form_slug(metadata[:slug], mode)
|> supplement_form_from_db(db_post)
|> Forms.normalize_form()
end
# Fill in any form fields that are empty with DB-stored values
defp supplement_form_from_db(form, nil), do: form
defp supplement_form_from_db(form, db_post) do
Enum.reduce(
[
{"featured_image_uuid", Map.get(db_post.metadata, :featured_image_uuid)},
{"url_slug", Map.get(db_post.metadata, :url_slug) || Map.get(db_post, :url_slug)}
],
form,
fn {key, db_value}, acc ->
current = Map.get(acc, key, "")
if current in [nil, ""] and db_value not in [nil, ""] do
Map.put(acc, key, to_string(db_value))
else
acc
end
end
)
end
defp apply_preview_assigns(socket, post, form, group_slug, mode, data, db_post) do
language = post.language
has_changes =
case db_post do
nil -> true
dp -> Forms.dirty?(dp, form, data[:content] || "")
end
# Derive saved status for save logic (saved_status tracks what's actually stored)
{saved_status, editing_published} =
case db_post do
nil ->
{"draft", false}
dp ->
status = Map.get(dp.metadata, :status, "draft")
{status, status == "published"}
end
socket
|> Phoenix.Component.assign(:group_mode, mode_to_string(mode))
|> Phoenix.Component.assign(:group_slug, group_slug)
|> Phoenix.Component.assign(:post, post)
|> Forms.assign_form_with_tracking(form, slug_manually_set: false)
|> Phoenix.Component.assign(:content, data[:content] || "")
|> Phoenix.Component.assign(:available_languages, post.available_languages)
|> Phoenix.Component.assign(:all_enabled_languages, Publishing.enabled_language_codes())
|> Helpers.assign_current_language(language)
|> Phoenix.Component.assign(:has_pending_changes, has_changes)
|> Phoenix.Component.assign(:is_new_post, data[:is_new_post] || false)
|> Phoenix.Component.assign(:public_url, Helpers.build_public_url(post, language))
|> Phoenix.Component.assign(:group_name, Publishing.group_name(group_slug) || group_slug)
|> Phoenix.Component.assign(:current_version, Map.get(post, :version))
|> Phoenix.Component.assign(:available_versions, Map.get(post, :available_versions, []))
|> Phoenix.Component.assign(:version_statuses, Map.get(post, :version_statuses, %{}))
|> Phoenix.Component.assign(:version_dates, Map.get(post, :version_dates, %{}))
|> Phoenix.Component.assign(:editing_published_version, editing_published)
|> Phoenix.Component.assign(:viewing_older_version, false)
|> Phoenix.Component.assign(:saved_status, saved_status)
end
defp enrich_from_db(post, group_slug) do
identifier = post[:uuid] || post[:slug]
if identifier do
do_enrich_from_db(post, group_slug, identifier)
else
enrich_fallback(post)
end
end
defp do_enrich_from_db(post, group_slug, identifier) do
case Publishing.read_post(group_slug, identifier) do
{:ok, db_post} ->
preview_meta_values =
post.metadata |> Enum.reject(fn {_k, v} -> is_nil(v) end) |> Map.new()
enriched =
post
|> Map.put(:metadata, Map.merge(db_post.metadata, preview_meta_values))
|> Map.put(:language_statuses, Map.get(db_post, :language_statuses, %{}))
|> Map.put(:available_versions, Map.get(db_post, :available_versions, []))
|> Map.put(:version_statuses, Map.get(db_post, :version_statuses, %{}))
|> Map.put(:version_dates, Map.get(db_post, :version_dates, %{}))
|> Map.put(:version, Map.get(db_post, :version))
{enriched, db_post}
{:error, reason} ->
Logger.debug("Preview enrich_from_db failed for #{identifier}: #{inspect(reason)}")
enrich_fallback(post)
end
end
defp enrich_fallback(post) do
fallback_status = Map.get(post.metadata, :status, "draft")
enriched = Map.put(post, :language_statuses, %{post.language => fallback_status})
{enriched, nil}
end
defp normalize_preview_metadata(metadata, mode) do
metadata_map =
Enum.reduce(metadata, %{}, fn
{key, value}, acc
when key in [:status, :published_at, :slug, :featured_image_uuid, :url_slug] ->
Map.put(acc, key, value)
{"status", value}, acc ->
Map.put(acc, :status, value)
{"published_at", value}, acc ->
Map.put(acc, :published_at, value)
{"slug", value}, acc ->
Map.put(acc, :slug, value)
{"featured_image_uuid", value}, acc ->
Map.put(acc, :featured_image_uuid, value)
{"url_slug", value}, acc ->
Map.put(acc, :url_slug, value)
_, acc ->
acc
end)
defaults =
case mode do
:slug -> %{status: "draft", published_at: "", slug: ""}
_ -> %{status: "draft", published_at: "", slug: nil}
end
Map.merge(defaults, metadata_map)
end
defp derive_datetime_fields(:timestamp, published_at) do
with value when is_binary(value) and value != "" <- published_at,
{:ok, dt, _offset} <- DateTime.from_iso8601(value) do
floored = Forms.floor_datetime_to_minute(dt)
{DateTime.to_date(floored), DateTime.to_time(floored)}
else
_ -> {nil, nil}
end
end
defp derive_datetime_fields(_, _), do: {nil, nil}
defp maybe_put_form_slug(form, slug, :slug) do
Map.put(form, "slug", slug || "")
end
defp maybe_put_form_slug(form, _slug, _mode), do: form
defp mode_to_string(:slug), do: "slug"
defp mode_to_string(_), do: "timestamp"
end