Packages
phoenix_kit
1.7.39
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/entities/web/entities_settings.ex
defmodule PhoenixKit.Modules.Entities.Web.EntitiesSettings do
@moduledoc """
LiveView for managing entities system settings and configuration.
Provides interface for enabling/disabling entities module and viewing statistics.
"""
use PhoenixKitWeb, :live_view
on_mount PhoenixKit.Modules.Entities.Web.Hooks
alias PhoenixKit.Modules.Entities
alias PhoenixKit.Modules.Entities.EntityData
alias PhoenixKit.Modules.Entities.Events
alias PhoenixKit.Modules.Entities.Mirror.{Exporter, Importer, Storage}
alias PhoenixKit.Settings
def mount(_params, _session, socket) do
# Set locale for LiveView process
project_title = Settings.get_project_title()
# Load current entities settings
settings = %{
entities_enabled: Entities.enabled?(),
auto_generate_slugs: Settings.get_setting("entities_auto_generate_slugs", "true"),
default_status: Settings.get_setting("entities_default_status", "draft"),
require_approval: Settings.get_setting("entities_require_approval", "false"),
max_entities_per_user: Settings.get_setting("entities_max_per_user", "100"),
data_retention_days: Settings.get_setting("entities_data_retention_days", "365"),
enable_revisions: Settings.get_setting("entities_enable_revisions", "false"),
enable_comments: Settings.get_setting("entities_enable_comments", "false")
}
changeset = build_changeset(settings)
socket =
socket
|> assign(:page_title, gettext("Entities Settings"))
|> assign(:project_title, project_title)
|> assign(:settings, settings)
|> assign(:changeset, changeset)
|> assign(:entities_stats, get_entities_stats())
# Per-entity mirror settings
|> assign(:entities_list, Entities.list_entities_with_mirror_status())
|> assign(:mirror_path, Storage.root_path())
|> assign(:export_stats, Storage.get_stats())
|> assign(:import_preview, nil)
|> assign(:import_selections, %{})
|> assign(:import_active_tab, nil)
|> assign(:show_import_modal, false)
|> assign(:importing, false)
|> assign(:exporting, false)
if connected?(socket) do
Events.subscribe_to_all_data()
end
{:ok, socket}
end
def handle_event("validate", %{"settings" => settings_params}, socket) do
changeset = build_changeset(settings_params, :validate)
{:noreply, assign(socket, :changeset, changeset)}
end
def handle_event("save", %{"settings" => settings_params}, socket) do
changeset = build_changeset(settings_params, :save)
if changeset.valid? do
case save_settings(settings_params) do
:ok ->
# Refresh settings and stats
new_settings = %{
entities_enabled: Entities.enabled?(),
auto_generate_slugs: Settings.get_setting("entities_auto_generate_slugs", "true"),
default_status: Settings.get_setting("entities_default_status", "draft"),
require_approval: Settings.get_setting("entities_require_approval", "false"),
max_entities_per_user: Settings.get_setting("entities_max_per_user", "100"),
data_retention_days: Settings.get_setting("entities_data_retention_days", "365"),
enable_revisions: Settings.get_setting("entities_enable_revisions", "false"),
enable_comments: Settings.get_setting("entities_enable_comments", "false")
}
socket =
socket
|> assign(:settings, new_settings)
|> assign(:changeset, build_changeset(new_settings))
|> assign(:entities_stats, get_entities_stats())
|> put_flash(:info, gettext("Entities settings saved successfully"))
{:noreply, socket}
{:error, reason} ->
socket =
put_flash(
socket,
:error,
gettext("Failed to save settings: %{reason}", reason: reason)
)
{:noreply, socket}
end
else
{:noreply, assign(socket, :changeset, changeset)}
end
end
def handle_event("enable_entities", _params, socket) do
case Entities.enable_system() do
{:ok, _setting} ->
settings = Map.put(socket.assigns.settings, :entities_enabled, true)
socket =
socket
|> assign(:settings, settings)
|> assign(:changeset, build_changeset(settings))
|> assign(:entities_stats, get_entities_stats())
|> put_flash(:info, gettext("Entities system enabled successfully"))
{:noreply, socket}
{:error, reason} ->
socket =
put_flash(
socket,
:error,
gettext("Failed to enable entities: %{reason}", reason: reason)
)
{:noreply, socket}
end
end
def handle_event("disable_entities", _params, socket) do
case Entities.disable_system() do
{:ok, _setting} ->
settings = Map.put(socket.assigns.settings, :entities_enabled, false)
socket =
socket
|> assign(:settings, settings)
|> assign(:changeset, build_changeset(settings))
|> assign(:entities_stats, get_entities_stats())
|> put_flash(:info, gettext("Entities system disabled successfully"))
{:noreply, socket}
{:error, reason} ->
socket =
put_flash(
socket,
:error,
gettext("Failed to disable entities: %{reason}", reason: reason)
)
{:noreply, socket}
end
end
def handle_event("reset_to_defaults", _params, socket) do
default_settings = %{
entities_enabled: true,
auto_generate_slugs: "true",
default_status: "draft",
require_approval: "false",
max_entities_per_user: "unlimited",
data_retention_days: "365",
enable_revisions: "false",
enable_comments: "false"
}
changeset = build_changeset(default_settings)
socket =
socket
|> assign(:settings, default_settings)
|> assign(:changeset, changeset)
|> put_flash(:info, gettext("Settings reset to defaults (not saved yet)"))
{:noreply, socket}
end
## Per-Entity Mirror Events
def handle_event("toggle_entity_definitions", %{"uuid" => entity_uuid}, socket) do
with {:ok, entity} <- fetch_entity(entity_uuid),
{:ok, updated_entity} <- toggle_definitions_setting(entity) do
maybe_export_entity(updated_entity, Entities.mirror_definitions_enabled?(updated_entity))
{:noreply, refresh_entities_list(socket)}
else
{:error, :not_found} ->
{:noreply, put_flash(socket, :error, gettext("Entity not found"))}
{:error, _changeset} ->
{:noreply, put_flash(socket, :error, gettext("Failed to update mirror settings"))}
end
end
def handle_event("toggle_entity_data", %{"uuid" => entity_uuid}, socket) do
with {:ok, entity} <- fetch_entity(entity_uuid),
{:ok, updated_entity} <- toggle_data_setting(entity) do
maybe_export_entity(updated_entity, Entities.mirror_data_enabled?(updated_entity))
{:noreply, refresh_entities_list(socket)}
else
{:error, :not_found} ->
{:noreply, put_flash(socket, :error, gettext("Entity not found"))}
{:error, _changeset} ->
{:noreply, put_flash(socket, :error, gettext("Failed to update mirror settings"))}
end
end
def handle_event("export_entity_now", %{"uuid" => entity_uuid}, socket) do
case Entities.get_entity(entity_uuid) do
nil ->
{:noreply, put_flash(socket, :error, gettext("Entity not found"))}
entity ->
message =
case Exporter.export_entity(entity) do
{:ok, _path, :with_data} ->
gettext("Exported %{name} (definition + records)", name: entity.display_name)
{:ok, _path, :definition_only} ->
gettext("Exported %{name} (definition only)", name: entity.display_name)
{:error, _reason} ->
nil
end
socket =
socket
|> assign(:export_stats, Storage.get_stats())
|> then(fn s ->
if message,
do: put_flash(s, :info, message),
else: put_flash(s, :error, gettext("Export failed"))
end)
{:noreply, socket}
end
end
## Bulk Mirror Actions
def handle_event("enable_all_definitions", _params, socket) do
{:ok, count} = Entities.enable_all_definitions_mirror()
# Export all entities
socket = assign(socket, :exporting, true)
send(self(), :do_full_export)
socket =
socket
|> assign(:entities_list, Entities.list_entities_with_mirror_status())
|> put_flash(:info, gettext("Enabled definition sync for %{count} entities", count: count))
{:noreply, socket}
end
def handle_event("disable_all_definitions", _params, socket) do
# Disabling definitions also disables data
{:ok, _} = Entities.disable_all_data_mirror()
{:ok, count} = Entities.disable_all_definitions_mirror()
socket =
socket
|> assign(:entities_list, Entities.list_entities_with_mirror_status())
|> put_flash(:info, gettext("Disabled definition sync for %{count} entities", count: count))
{:noreply, socket}
end
def handle_event("enable_all_data", _params, socket) do
# Enabling data also requires definitions to be enabled
{:ok, _} = Entities.enable_all_definitions_mirror()
{:ok, count} = Entities.enable_all_data_mirror()
# Export all entities with data
socket = assign(socket, :exporting, true)
send(self(), :do_full_export)
socket =
socket
|> assign(:entities_list, Entities.list_entities_with_mirror_status())
|> put_flash(:info, gettext("Enabled data sync for %{count} entities", count: count))
{:noreply, socket}
end
def handle_event("disable_all_data", _params, socket) do
{:ok, count} = Entities.disable_all_data_mirror()
socket =
socket
|> assign(:entities_list, Entities.list_entities_with_mirror_status())
|> put_flash(:info, gettext("Disabled data sync for %{count} entities", count: count))
{:noreply, socket}
end
def handle_event("export_now", _params, socket) do
socket = assign(socket, :exporting, true)
send(self(), :do_full_export)
{:noreply, socket}
end
def handle_event("show_import_modal", _params, socket) do
preview = Importer.preview_import()
# Initialize selections based on preview - default to appropriate action
selections = build_default_selections(preview)
first_entity = List.first(preview.entities)
socket =
socket
|> assign(:import_preview, preview)
|> assign(:import_selections, selections)
|> assign(:import_active_tab, first_entity && first_entity.name)
|> assign(:show_import_modal, true)
{:noreply, socket}
end
def handle_event("hide_import_modal", _params, socket) do
socket =
socket
|> assign(:show_import_modal, false)
|> assign(:import_preview, nil)
|> assign(:import_selections, %{})
|> assign(:import_active_tab, nil)
{:noreply, socket}
end
def handle_event("set_import_tab", %{"entity" => entity_name}, socket) do
{:noreply, assign(socket, :import_active_tab, entity_name)}
end
def handle_event(
"set_definition_action",
%{"entity" => entity_name, "action" => action},
socket
) do
action_atom = String.to_existing_atom(action)
selections = put_in(socket.assigns.import_selections, [entity_name, :definition], action_atom)
{:noreply, assign(socket, :import_selections, selections)}
end
def handle_event(
"set_record_action",
%{"entity" => entity_name, "slug" => slug, "action" => action},
socket
) do
action_atom = String.to_existing_atom(action)
selections = put_in(socket.assigns.import_selections, [entity_name, :data, slug], action_atom)
{:noreply, assign(socket, :import_selections, selections)}
end
def handle_event(
"set_all_records_action",
%{"entity" => entity_name, "action" => action},
socket
) do
action_atom = String.to_existing_atom(action)
# Find the entity in preview to get all slugs
entity = Enum.find(socket.assigns.import_preview.entities, &(&1.name == entity_name))
if entity do
new_data_selections =
entity.data
|> Enum.map(fn record -> {record.slug, action_atom} end)
|> Map.new()
selections =
put_in(socket.assigns.import_selections, [entity_name, :data], new_data_selections)
{:noreply, assign(socket, :import_selections, selections)}
else
{:noreply, socket}
end
end
def handle_event("do_import_entity", %{"entity" => entity_name}, socket) do
# Only import selections for the specified entity
entity_selections = Map.get(socket.assigns.import_selections, entity_name, %{})
filtered_selections = %{entity_name => entity_selections}
socket =
socket
|> assign(:importing, true)
|> assign(:show_import_modal, false)
send(self(), {:do_import, filtered_selections})
{:noreply, socket}
end
def handle_event("do_import", _params, socket) do
socket =
socket
|> assign(:importing, true)
|> assign(:show_import_modal, false)
send(self(), {:do_import, socket.assigns.import_selections})
{:noreply, socket}
end
def handle_event("refresh_export_stats", _params, socket) do
socket =
socket
|> assign(:export_stats, Storage.get_stats())
{:noreply, socket}
end
## Per-entity mirror helpers
defp fetch_entity(entity_id) do
case Entities.get_entity(entity_id) do
nil -> {:error, :not_found}
entity -> {:ok, entity}
end
end
defp toggle_definitions_setting(entity) do
new_value = !Entities.mirror_definitions_enabled?(entity)
new_settings =
if new_value,
do: %{"mirror_definitions" => true},
else: %{"mirror_definitions" => false, "mirror_data" => false}
Entities.update_mirror_settings(entity, new_settings)
end
defp toggle_data_setting(entity) do
new_value = !Entities.mirror_data_enabled?(entity)
Entities.update_mirror_settings(entity, %{"mirror_data" => new_value})
end
defp maybe_export_entity(entity, true), do: Task.start(fn -> Exporter.export_entity(entity) end)
defp maybe_export_entity(_entity, false), do: :ok
defp refresh_entities_list(socket) do
socket
|> assign(:entities_list, Entities.list_entities_with_mirror_status())
|> assign(:export_stats, Storage.get_stats())
end
## Live updates
def handle_info({event, _entity_id}, socket)
when event in [:entity_created, :entity_updated, :entity_deleted] do
socket =
socket
|> assign(:entities_stats, get_entities_stats())
|> assign(:entities_list, Entities.list_entities_with_mirror_status())
|> assign(:export_stats, Storage.get_stats())
{:noreply, socket}
end
def handle_info({event, _entity_id, _data_id}, socket)
when event in [:data_created, :data_updated, :data_deleted] do
socket =
socket
|> assign(:entities_stats, get_entities_stats())
|> assign(:entities_list, Entities.list_entities_with_mirror_status())
|> assign(:export_stats, Storage.get_stats())
{:noreply, socket}
end
## Mirror background operations
def handle_info(:do_full_export, socket) do
{:ok, %{definitions: def_count, data: data_count}} = Exporter.export_all()
socket =
socket
|> assign(:exporting, false)
|> assign(:export_stats, Storage.get_stats())
|> assign(:entities_list, Entities.list_entities_with_mirror_status())
|> put_flash(
:info,
gettext("Export complete. %{defs} definitions, %{data} records.",
defs: def_count,
data: data_count
)
)
{:noreply, socket}
end
def handle_info({:do_import, selections}, socket) do
{:ok, %{definitions: def_results, data: data_results}} = Importer.import_selected(selections)
def_created = Enum.count(def_results, &match?({:ok, :created, _}, &1))
def_updated = Enum.count(def_results, &match?({:ok, :updated, _}, &1))
def_skipped = Enum.count(def_results, &match?({:ok, :skipped, _}, &1))
data_created = Enum.count(data_results, &match?({:ok, :created, _}, &1))
data_updated = Enum.count(data_results, &match?({:ok, :updated, _}, &1))
data_skipped = Enum.count(data_results, &match?({:ok, :skipped, _}, &1))
socket =
socket
|> assign(:importing, false)
|> assign(:import_preview, nil)
|> assign(:import_selections, %{})
|> assign(:export_stats, Storage.get_stats())
|> assign(:entities_stats, get_entities_stats())
|> put_flash(
:info,
gettext(
"Import complete. Definitions: %{dc} created, %{du} updated, %{ds} skipped. Data: %{rc} created, %{ru} updated, %{rs} skipped.",
dc: def_created,
du: def_updated,
ds: def_skipped,
rc: data_created,
ru: data_updated,
rs: data_skipped
)
)
{:noreply, socket}
end
# Private Functions
defp build_changeset(settings, action \\ nil) do
types = %{
entities_enabled: :boolean,
auto_generate_slugs: :string,
default_status: :string,
require_approval: :string,
max_entities_per_user: :string,
data_retention_days: :string,
enable_revisions: :string,
enable_comments: :string
}
required = [:auto_generate_slugs, :default_status]
changeset =
{settings, types}
|> Ecto.Changeset.cast(settings, Map.keys(types))
|> Ecto.Changeset.validate_required(required)
|> Ecto.Changeset.validate_inclusion(:default_status, ["draft", "published", "archived"])
|> Ecto.Changeset.validate_inclusion(:auto_generate_slugs, ["true", "false"])
|> Ecto.Changeset.validate_inclusion(:require_approval, ["true", "false"])
|> Ecto.Changeset.validate_inclusion(:enable_revisions, ["true", "false"])
|> Ecto.Changeset.validate_inclusion(:enable_comments, ["true", "false"])
|> validate_max_entities_per_user()
|> validate_data_retention_days()
if action do
Map.put(changeset, :action, action)
else
changeset
end
end
defp validate_max_entities_per_user(changeset) do
case Ecto.Changeset.get_field(changeset, :max_entities_per_user) do
"unlimited" ->
changeset
value when is_binary(value) ->
case Integer.parse(value) do
{num, ""} when num > 0 ->
changeset
_ ->
Ecto.Changeset.add_error(
changeset,
:max_entities_per_user,
gettext("must be 'unlimited' or a positive integer")
)
end
_ ->
Ecto.Changeset.add_error(
changeset,
:max_entities_per_user,
gettext("must be 'unlimited' or a positive integer")
)
end
end
defp validate_data_retention_days(changeset) do
case Ecto.Changeset.get_field(changeset, :data_retention_days) do
value when is_binary(value) ->
case Integer.parse(value) do
{num, ""} when num > 0 ->
changeset
_ ->
Ecto.Changeset.add_error(
changeset,
:data_retention_days,
gettext("must be a positive integer")
)
end
_ ->
Ecto.Changeset.add_error(
changeset,
:data_retention_days,
gettext("must be a positive integer")
)
end
end
defp save_settings(settings_params) do
settings_to_save = [
{"entities_auto_generate_slugs", Map.get(settings_params, "auto_generate_slugs", "true")},
{"entities_default_status", Map.get(settings_params, "default_status", "draft")},
{"entities_require_approval", Map.get(settings_params, "require_approval", "false")},
{"entities_max_per_user", Map.get(settings_params, "max_entities_per_user", "100")},
{"entities_data_retention_days", Map.get(settings_params, "data_retention_days", "365")},
{"entities_enable_revisions", Map.get(settings_params, "enable_revisions", "false")},
{"entities_enable_comments", Map.get(settings_params, "enable_comments", "false")}
]
try do
Enum.each(settings_to_save, fn {key, value} ->
Settings.update_setting(key, value)
end)
:ok
rescue
e ->
{:error, Exception.message(e)}
end
end
defp get_entities_stats do
if Entities.enabled?() do
entities_stats = Entities.get_system_stats()
data_stats = EntityData.get_data_stats()
Map.merge(entities_stats, data_stats)
else
%{
total_entities: 0,
active_entities: 0,
total_data_records: 0,
published_records: 0,
draft_records: 0,
archived_records: 0
}
end
end
# Helper functions for templates
def setting_status_class(enabled) do
if enabled, do: "badge-success", else: "badge-error"
end
def setting_status_text(enabled) do
if enabled, do: gettext("Enabled"), else: gettext("Disabled")
end
def format_retention_period(days) do
case Integer.parse(days) do
{num, ""} when num >= 365 ->
years = div(num, 365)
remainder = rem(num, 365)
if remainder == 0 do
ngettext("%{count} year", "%{count} years", years, count: years)
else
gettext("%{years} year(s), %{days} day(s)", years: years, days: remainder)
end
{num, ""} when num >= 30 ->
months = div(num, 30)
remainder = rem(num, 30)
if remainder == 0 do
ngettext("%{count} month", "%{count} months", months, count: months)
else
gettext("%{months} month(s), %{days} day(s)", months: months, days: remainder)
end
{num, ""} ->
ngettext("%{count} day", "%{count} days", num, count: num)
_ ->
days
end
end
# Build default import selections based on preview
# - NEW items default to :overwrite (will create)
# - IDENTICAL items default to :skip (nothing to do)
# - CHANGED items default to :skip (safe default)
defp build_default_selections(%{entities: entities}) do
entities
|> Enum.map(fn entity ->
def_action = default_action_for(entity.definition.action)
data_selections =
entity.data
|> Enum.map(fn record ->
{record.slug, default_action_for(record.action)}
end)
|> Map.new()
{entity.name, %{definition: def_action, data: data_selections}}
end)
|> Map.new()
end
defp default_action_for(:create), do: :overwrite
defp default_action_for(:identical), do: :skip
defp default_action_for(:conflict), do: :skip
defp default_action_for(_), do: :skip
# Helper to get current action for a record from selections
def get_record_action(selections, entity_name, slug) do
get_in(selections, [entity_name, :data, slug]) || :skip
end
def get_definition_action(selections, entity_name) do
get_in(selections, [entity_name, :definition]) || :skip
end
end