Packages
phoenix_kit
1.7.49
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/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.PubSub, as: PublishingPubSub
alias PhoenixKit.Modules.Publishing.Storage
alias PhoenixKit.Modules.Publishing.Web.Editor.Helpers
# ============================================================================
# Version Reading
# ============================================================================
@doc """
Reads a specific version of a post.
"""
def read_version_post(socket, version) do
group_slug = socket.assigns.group_slug
post = socket.assigns.post
language = socket.assigns.current_language
# Use the post's stored primary language for fallback, not global
primary_language = post[:primary_language] || Storage.get_primary_language()
read_fn =
if socket.assigns.group_mode == "slug" do
fn lang -> Publishing.read_post(group_slug, post.slug, lang, version) end
else
fn lang -> read_timestamp_version(group_slug, post, lang, version) end
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
defp read_timestamp_version(group_slug, post, language, version) do
# Extract timestamp identifier from current post path
timestamp_id = extract_timestamp_identifier(post.path)
# Use Publishing.read_post which has DB fallback for imported groups
Publishing.read_post(group_slug, timestamp_id, language, version)
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] && socket.assigns.post[:slug]
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, version_post.path}
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 =
case post.mode do
:timestamp ->
extract_timestamp_identifier(post.path)
_ ->
post.slug
end
# 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
) 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, group_slug, post_slug, 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, group_slug, post_slug, 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,
group_slug,
post_slug,
[surviving_version | _] = versions
) do
current_language = editor_language(socket.assigns)
case Publishing.read_post(group_slug, post_slug, current_language, surviving_version) do
{:ok, fresh_post} ->
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, _group_slug, _post_slug, []) 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,
%{socket.assigns.post | path: nil, 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 """
Extract timestamp identifier (YYYY-MM-DD/HH:MM) from a post path.
"""
def extract_timestamp_identifier(path) when is_binary(path) do
case Regex.run(~r/(\d{4}-\d{2}-\d{2}\/\d{2}:\d{2})/, path) do
[_, timestamp] -> timestamp
nil -> nil
end
end
def extract_timestamp_identifier(_), do: nil
@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
assigns[:current_language] ||
assigns |> Map.get(:post, %{}) |> Map.get(:language) ||
hd(Storage.enabled_language_codes())
end
end