Current section
Files
Jump to
Current section
Files
lib/phoenix_kit_publishing/web/editor/versions.ex
defmodule PhoenixKit.Modules.Publishing.Web.Editor.Versions do
@moduledoc """
Version management functionality for the publishing editor.
Handles version switching, creation, migration, and
version-related UI state management.
"""
use Gettext, backend: PhoenixKitWeb.Gettext
alias PhoenixKit.Modules.Publishing
alias PhoenixKit.Modules.Publishing.LanguageHelpers
alias PhoenixKit.Modules.Publishing.PubSub, as: PublishingPubSub
alias PhoenixKit.Modules.Publishing.Shared
alias PhoenixKit.Modules.Publishing.Web.Editor.Helpers
# ============================================================================
# Version Reading
# ============================================================================
@doc """
Reads a specific version of a post.
"""
def read_version_post(socket, version) do
post = socket.assigns.post
language = socket.assigns.current_language
primary_language = LanguageHelpers.get_primary_language()
read_fn = fn lang -> Publishing.read_post_by_uuid(post.uuid, lang, version) end
# Try current language first, fall back to primary if different
case read_fn.(language) do
{:ok, _} = result -> result
{:error, _} when language != primary_language -> read_fn.(primary_language)
error -> error
end
end
# ============================================================================
# Version Switching
# ============================================================================
@doc """
Applies a version switch to the socket.
"""
def apply_version_switch(socket, version, version_post, form_builder_fn) do
group_slug = socket.assigns.group_slug
form = form_builder_fn.(group_slug, version_post, version)
is_published = form["status"] == "published"
actual_language = version_post.language
new_form_key = PublishingPubSub.generate_form_key(group_slug, version_post, :edit)
# Save old form_key and post slug BEFORE assigning new one (for presence cleanup)
old_form_key = socket.assigns[:form_key]
old_post_slug = socket.assigns[:post] && PublishingPubSub.broadcast_id(socket.assigns.post)
socket =
socket
|> Phoenix.Component.assign(:post, %{version_post | group: group_slug})
|> Phoenix.Component.assign(:form, form)
|> Phoenix.Component.assign(:content, version_post.content)
|> Phoenix.Component.assign(:current_version, version)
|> Phoenix.Component.assign(:available_versions, version_post.available_versions)
|> Phoenix.Component.assign(:version_statuses, version_post.version_statuses)
|> Phoenix.Component.assign(:version_dates, Map.get(version_post, :version_dates, %{}))
|> Phoenix.Component.assign(:available_languages, version_post.available_languages)
|> Phoenix.Component.assign(:editing_published_version, is_published)
|> Phoenix.Component.assign(:viewing_older_version, false)
|> Phoenix.Component.assign(:has_pending_changes, false)
|> Phoenix.Component.assign(:form_key, new_form_key)
|> Phoenix.Component.assign(:saved_status, form["status"])
|> Phoenix.LiveView.push_event("changes-status", %{has_changes: false})
|> Phoenix.LiveView.push_event("set-content", %{content: version_post.content})
# Return socket with cleanup info for the caller to handle collaborative editing
{socket, old_form_key, old_post_slug, new_form_key, actual_language}
end
# ============================================================================
# Version Creation
# ============================================================================
@doc """
Creates a new version from a source version.
Returns {:ok, socket} or {:error, socket} for use in handle_event.
"""
def create_version_from_source(socket) do
group_slug = socket.assigns.group_slug
post = socket.assigns.post
source_version = socket.assigns.new_version_source
scope = socket.assigns[:phoenix_kit_current_scope]
post_identifier = post[:uuid] || post.slug
# Set just_created_version BEFORE calling create_version_from to prevent race condition
# where the PubSub broadcast is received before this assign happens
socket = Phoenix.Component.assign(socket, :just_created_version, true)
case Publishing.create_version_from(group_slug, post_identifier, source_version, %{},
scope: scope,
actor_uuid: Shared.actor_uuid_from_socket(socket)
) do
{:ok, new_version_post} ->
flash_msg =
if source_version do
gettext("Created new version %{version} from v%{source}",
version: new_version_post.version,
source: source_version
)
else
gettext("Created new blank version %{version}", version: new_version_post.version)
end
socket =
socket
|> Phoenix.Component.assign(:show_new_version_modal, false)
|> Phoenix.Component.assign(:new_version_source, nil)
|> Phoenix.LiveView.put_flash(:info, flash_msg)
|> Phoenix.LiveView.push_navigate(
to:
Helpers.build_edit_url(group_slug, new_version_post,
version: new_version_post.version
)
)
{:ok, socket}
{:error, reason} ->
socket =
socket
|> Phoenix.Component.assign(:show_new_version_modal, false)
|> Phoenix.LiveView.put_flash(
:error,
gettext("Failed to create new version: %{reason}", reason: inspect(reason))
)
{:error, socket}
end
end
# ============================================================================
# Version Migration
# ============================================================================
# ============================================================================
# Version Deletion Handling
# ============================================================================
@doc """
Handles when a version is deleted by another editor.
"""
def handle_version_deleted(socket, deleted_version) do
available_versions = socket.assigns[:available_versions] || []
updated_versions = Enum.reject(available_versions, &(&1 == deleted_version))
current_version = socket.assigns[:current_version]
if current_version == deleted_version do
switch_to_surviving_version(socket, updated_versions)
else
# We weren't viewing the deleted version, just update the list
socket
|> Phoenix.Component.assign(:available_versions, updated_versions)
|> Phoenix.Component.assign(
:post,
Map.put(socket.assigns.post, :available_versions, updated_versions)
)
end
end
defp switch_to_surviving_version(socket, [surviving_version | _] = versions) do
current_language = editor_language(socket.assigns)
post_uuid = socket.assigns.post.uuid
case Publishing.read_post_by_uuid(post_uuid, current_language, surviving_version) do
{:ok, fresh_post} ->
group_slug = socket.assigns.group_slug
apply_surviving_version(socket, group_slug, fresh_post, versions, surviving_version)
{:error, _} ->
socket
|> Phoenix.Component.assign(:readonly?, true)
|> Phoenix.LiveView.put_flash(
:error,
gettext("The version you were editing was deleted and no other versions are available.")
)
end
end
defp switch_to_surviving_version(socket, []) do
# No versions left - this post is effectively deleted
socket
|> Phoenix.Component.assign(:readonly?, true)
|> Phoenix.Component.assign(:current_version, nil)
|> Phoenix.Component.assign(:available_versions, [])
|> Phoenix.Component.assign(
:post,
Map.merge(socket.assigns.post, %{current_version: nil})
)
|> Phoenix.Component.assign(:has_pending_changes, false)
|> Phoenix.LiveView.put_flash(
:error,
gettext("All versions of this post have been deleted. Please navigate away.")
)
end
defp apply_surviving_version(
socket,
group_slug,
fresh_post,
updated_versions,
surviving_version
) do
socket
|> Phoenix.Component.assign(:post, %{fresh_post | group: group_slug})
|> Phoenix.Component.assign(:available_versions, updated_versions)
|> Phoenix.Component.assign(:current_version, surviving_version)
|> Phoenix.Component.assign(:content, fresh_post.content)
|> Phoenix.Component.assign(:has_pending_changes, false)
|> Phoenix.LiveView.push_event("changes-status", %{has_changes: false})
|> Phoenix.LiveView.put_flash(
:warning,
gettext("The version you were editing was deleted. Switched to version %{version}.",
version: surviving_version
)
)
end
# ============================================================================
# Helpers
# ============================================================================
@doc """
With variant versioning, all versions are editable since they're independent attempts.
This function always returns false - no version locking.
"""
def viewing_older_version?(_current_version, _available_versions, _current_language), do: false
defp editor_language(assigns), do: Helpers.editor_language(assigns)
end