Packages
phoenix_kit
1.7.69
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/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
form_slug =
form
|> Map.get("slug")
|> case do
nil -> nil
slug -> String.trim(to_string(slug))
end
cond do
form_slug && form_slug != "" ->
form_slug
Map.get(post, :slug) && post.slug != "" ->
post.slug
Map.get(post, "slug") && post["slug"] != "" ->
post["slug"]
metadata_value(post, :slug) ->
metadata_value(post, :slug)
metadata_value(post, "slug") ->
metadata_value(post, "slug")
true ->
""
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
case Publishing.read_post(group_slug, identifier) do
{:ok, db_post} ->
# Merge DB metadata as base, with preview metadata on top.
# This preserves non-form fields (description, created_at, version_created_at,
# previous_url_slugs, etc.) that aren't carried in the preview token,
# preventing silent data loss if the user saves after returning from preview.
preview_meta_values =
post.metadata |> Enum.reject(fn {_k, v} -> is_nil(v) end) |> Map.new()
merged_metadata = Map.merge(db_post.metadata, preview_meta_values)
enriched =
post
|> Map.put(:metadata, merged_metadata)
|> 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))
|> Map.put(:primary_language, Map.get(db_post, :primary_language))
{enriched, db_post}
{:error, reason} ->
Logger.debug("Preview enrich_from_db failed for #{identifier}: #{inspect(reason)}")
fallback_status = Map.get(post.metadata, :status, "draft")
enriched = Map.put(post, :language_statuses, %{post.language => fallback_status})
{enriched, nil}
end
else
fallback_status = Map.get(post.metadata, :status, "draft")
enriched = Map.put(post, :language_statuses, %{post.language => fallback_status})
{enriched, nil}
end
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