Packages

phoenix_kit

1.7.53
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
phoenix_kit lib modules entities web data_navigator.ex
Raw

lib/modules/entities/web/data_navigator.ex

defmodule PhoenixKit.Modules.Entities.Web.DataNavigator do
@moduledoc """
LiveView for browsing and managing entity data records.
Provides table view with pagination, search, filtering, and bulk operations.
"""
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.Multilang
alias PhoenixKit.Settings
alias PhoenixKit.Users.Auth.Scope
alias PhoenixKit.Utils.Routes
def mount(params, _session, socket) do
project_title = Settings.get_project_title()
entities = Entities.list_entities()
# Get entity from route params using slug (entity_slug or entity_id for backwards compat)
{entity, entity_uuid} =
case params["entity_slug"] || params["entity_id"] do
nil ->
{nil, nil}
slug when is_binary(slug) ->
# Try to get entity by name (slug)
case Entities.get_entity_by_name(slug) do
nil -> {nil, nil}
entity -> {entity, entity.uuid}
end
end
# Get stats filtered by entity if one is selected
stats = EntityData.get_data_stats(entity_uuid)
# Set page title based on entity
page_title =
if entity do
entity.display_name
else
gettext("Data Navigator")
end
socket =
socket
|> assign(:page_title, page_title)
|> assign(:project_title, project_title)
|> assign(:entities, entities)
|> assign(:total_records, stats.total_records)
|> assign(:published_records, stats.published_records)
|> assign(:draft_records, stats.draft_records)
|> assign(:archived_records, stats.archived_records)
|> assign(:selected_entity, entity)
|> assign(:selected_entity_uuid, entity_uuid)
|> assign(:selected_status, "all")
|> assign(:selected_uuids, MapSet.new())
|> assign(:search_term, "")
|> assign(:view_mode, "table")
|> apply_filters()
if connected?(socket) && entity_uuid do
Events.subscribe_to_entity_data(entity_uuid)
end
{:ok, socket}
end
def handle_params(params, _url, socket) do
# Get entity from slug in params (entity_slug or entity_id for backwards compat)
{entity, entity_uuid} = resolve_entity_from_params(params, socket)
# Recalculate stats and subscribe if entity changed
socket = maybe_update_entity_stats(socket, entity_uuid)
# Extract filter params with defaults
status = params["status"] || "all"
search_term = params["search"] || ""
view_mode = params["view"] || "table"
socket =
socket
|> assign(:selected_entity, entity)
|> assign(:selected_entity_uuid, entity_uuid)
|> assign(:selected_status, status)
|> assign(:search_term, search_term)
|> assign(:view_mode, view_mode)
|> apply_filters()
{:noreply, socket}
end
# Resolve entity and entity_uuid from URL params
defp resolve_entity_from_params(params, socket) do
case params["entity_slug"] || params["entity_id"] do
nil ->
{socket.assigns.selected_entity, socket.assigns.selected_entity_uuid}
"" ->
{nil, nil}
slug when is_binary(slug) ->
resolve_entity_by_slug(slug)
end
end
# Look up entity by slug/name
defp resolve_entity_by_slug(slug) do
case Entities.get_entity_by_name(slug) do
nil -> {nil, nil}
entity -> {entity, entity.uuid}
end
end
# Update entity stats and subscribe to events if entity changed
defp maybe_update_entity_stats(socket, new_entity_uuid) do
if new_entity_uuid != socket.assigns.selected_entity_uuid do
maybe_subscribe_to_entity(socket, new_entity_uuid)
update_entity_stats(socket, new_entity_uuid)
else
socket
end
end
# Subscribe to entity data events if connected
defp maybe_subscribe_to_entity(socket, entity_uuid) do
if connected?(socket) && entity_uuid do
Events.subscribe_to_entity_data(entity_uuid)
end
end
# Update socket with fresh entity statistics
defp update_entity_stats(socket, entity_uuid) do
stats = EntityData.get_data_stats(entity_uuid)
socket
|> assign(:total_records, stats.total_records)
|> assign(:published_records, stats.published_records)
|> assign(:draft_records, stats.draft_records)
|> assign(:archived_records, stats.archived_records)
end
def handle_event("toggle_view_mode", %{"mode" => mode}, socket) do
params =
build_url_params(
socket.assigns.selected_entity_uuid,
socket.assigns.selected_status,
socket.assigns.search_term,
mode
)
path = build_base_path(socket.assigns.selected_entity_uuid)
full_path = if params != "", do: "#{path}?#{params}", else: path
socket =
socket
|> assign(:view_mode, mode)
|> assign(:selected_uuids, MapSet.new())
|> push_patch(to: Routes.path(full_path, locale: socket.assigns.current_locale_base))
{:noreply, socket}
end
def handle_event("filter_by_entity", %{"entity_id" => ""}, socket) do
# No entity selected - redirect to entities list since global data view no longer exists
socket =
socket
|> put_flash(:info, gettext("Please select an entity to view its data"))
|> redirect(to: Routes.path("/admin/entities", locale: socket.assigns.current_locale_base))
{:noreply, socket}
end
def handle_event("filter_by_entity", %{"entity_id" => entity_uuid}, socket) do
params =
build_url_params(
entity_uuid,
socket.assigns.selected_status,
socket.assigns.search_term,
socket.assigns.view_mode
)
path = build_base_path(entity_uuid)
full_path = if params != "", do: "#{path}?#{params}", else: path
socket =
socket
|> assign(:selected_uuids, MapSet.new())
|> push_patch(to: Routes.path(full_path, locale: socket.assigns.current_locale_base))
{:noreply, socket}
end
def handle_event("filter_by_status", %{"status" => status}, socket) do
params =
build_url_params(
socket.assigns.selected_entity_uuid,
status,
socket.assigns.search_term,
socket.assigns.view_mode
)
path = build_base_path(socket.assigns.selected_entity_uuid)
full_path = if params != "", do: "#{path}?#{params}", else: path
socket =
socket
|> assign(:selected_uuids, MapSet.new())
|> push_patch(to: Routes.path(full_path, locale: socket.assigns.current_locale_base))
{:noreply, socket}
end
def handle_event("search", %{"search" => %{"term" => term}}, socket) do
params =
build_url_params(
socket.assigns.selected_entity_uuid,
socket.assigns.selected_status,
term,
socket.assigns.view_mode
)
path = build_base_path(socket.assigns.selected_entity_uuid)
full_path = if params != "", do: "#{path}?#{params}", else: path
socket =
socket
|> assign(:selected_uuids, MapSet.new())
|> push_patch(to: Routes.path(full_path, locale: socket.assigns.current_locale_base))
{:noreply, socket}
end
def handle_event("clear_filters", _params, socket) do
params =
build_url_params(
socket.assigns.selected_entity_uuid,
"all",
"",
socket.assigns.view_mode
)
path = build_base_path(socket.assigns.selected_entity_uuid)
full_path = if params != "", do: "#{path}?#{params}", else: path
socket =
socket
|> assign(:selected_uuids, MapSet.new())
|> push_patch(to: Routes.path(full_path, locale: socket.assigns.current_locale_base))
{:noreply, socket}
end
def handle_event("archive_data", %{"uuid" => uuid}, socket) do
if Scope.admin?(socket.assigns.phoenix_kit_current_scope) do
data_record = EntityData.get!(uuid)
case EntityData.update_data(data_record, %{status: "archived"}) do
{:ok, _data} ->
socket =
socket
|> apply_filters()
|> put_flash(:info, gettext("Data record archived successfully"))
{:noreply, socket}
{:error, _changeset} ->
socket = put_flash(socket, :error, gettext("Failed to archive data record"))
{:noreply, socket}
end
else
{:noreply, put_flash(socket, :error, gettext("Not authorized"))}
end
end
def handle_event("restore_data", %{"uuid" => uuid}, socket) do
if Scope.admin?(socket.assigns.phoenix_kit_current_scope) do
data_record = EntityData.get!(uuid)
case EntityData.update_data(data_record, %{status: "published"}) do
{:ok, _data} ->
socket =
socket
|> apply_filters()
|> put_flash(:info, gettext("Data record restored successfully"))
{:noreply, socket}
{:error, _changeset} ->
socket = put_flash(socket, :error, gettext("Failed to restore data record"))
{:noreply, socket}
end
else
{:noreply, put_flash(socket, :error, gettext("Not authorized"))}
end
end
def handle_event("toggle_status", %{"uuid" => uuid}, socket) do
if Scope.admin?(socket.assigns.phoenix_kit_current_scope) do
data_record = EntityData.get!(uuid)
new_status =
case data_record.status do
"draft" -> "published"
"published" -> "archived"
"archived" -> "draft"
end
case EntityData.update_data(data_record, %{status: new_status}) do
{:ok, _updated_data} ->
socket =
socket
|> refresh_data_stats()
|> apply_filters()
|> put_flash(
:info,
gettext("Status updated to %{status}", status: status_label(new_status))
)
{:noreply, socket}
{:error, _changeset} ->
socket = put_flash(socket, :error, gettext("Failed to update status"))
{:noreply, socket}
end
else
{:noreply, put_flash(socket, :error, gettext("Not authorized"))}
end
end
def handle_event("toggle_select", %{"uuid" => uuid}, socket) do
selected = socket.assigns.selected_uuids
selected =
if MapSet.member?(selected, uuid),
do: MapSet.delete(selected, uuid),
else: MapSet.put(selected, uuid)
{:noreply, assign(socket, :selected_uuids, selected)}
end
def handle_event("select_all", _params, socket) do
all_uuids = socket.assigns.entity_data_records |> Enum.map(& &1.uuid) |> MapSet.new()
{:noreply, assign(socket, :selected_uuids, all_uuids)}
end
def handle_event("deselect_all", _params, socket) do
{:noreply, assign(socket, :selected_uuids, MapSet.new())}
end
def handle_event("bulk_action", %{"action" => "archive"}, socket) do
if Scope.admin?(socket.assigns.phoenix_kit_current_scope) do
uuids = socket.assigns.selected_uuids
if MapSet.size(uuids) == 0 do
{:noreply, put_flash(socket, :error, gettext("No records selected"))}
else
{count, _} = EntityData.bulk_update_status(MapSet.to_list(uuids), "archived")
{:noreply,
socket
|> assign(:selected_uuids, MapSet.new())
|> refresh_data_stats()
|> apply_filters()
|> put_flash(:info, gettext("%{count} records archived", count: count))}
end
else
{:noreply, put_flash(socket, :error, gettext("Not authorized"))}
end
end
def handle_event("bulk_action", %{"action" => "restore"}, socket) do
if Scope.admin?(socket.assigns.phoenix_kit_current_scope) do
uuids = socket.assigns.selected_uuids
if MapSet.size(uuids) == 0 do
{:noreply, put_flash(socket, :error, gettext("No records selected"))}
else
{count, _} = EntityData.bulk_update_status(MapSet.to_list(uuids), "published")
{:noreply,
socket
|> assign(:selected_uuids, MapSet.new())
|> refresh_data_stats()
|> apply_filters()
|> put_flash(:info, gettext("%{count} records restored", count: count))}
end
else
{:noreply, put_flash(socket, :error, gettext("Not authorized"))}
end
end
def handle_event("bulk_action", %{"action" => "delete"}, socket) do
if Scope.admin?(socket.assigns.phoenix_kit_current_scope) do
uuids = socket.assigns.selected_uuids
if MapSet.size(uuids) == 0 do
{:noreply, put_flash(socket, :error, gettext("No records selected"))}
else
{count, _} = EntityData.bulk_delete(MapSet.to_list(uuids))
{:noreply,
socket
|> assign(:selected_uuids, MapSet.new())
|> refresh_data_stats()
|> apply_filters()
|> put_flash(:info, gettext("%{count} records deleted", count: count))}
end
else
{:noreply, put_flash(socket, :error, gettext("Not authorized"))}
end
end
def handle_event("bulk_action", %{"action" => "change_status", "status" => status}, socket) do
if Scope.admin?(socket.assigns.phoenix_kit_current_scope) do
uuids = socket.assigns.selected_uuids
if MapSet.size(uuids) == 0 do
{:noreply, put_flash(socket, :error, gettext("No records selected"))}
else
{count, _} = EntityData.bulk_update_status(MapSet.to_list(uuids), status)
{:noreply,
socket
|> assign(:selected_uuids, MapSet.new())
|> refresh_data_stats()
|> apply_filters()
|> put_flash(:info, gettext("%{count} records updated", count: count))}
end
else
{:noreply, put_flash(socket, :error, gettext("Not authorized"))}
end
end
## Live updates
def handle_info({:entity_created, _entity_uuid}, socket) do
{:noreply, refresh_entities_and_data(socket)}
end
def handle_info({:entity_updated, entity_uuid}, socket) do
# If the currently viewed entity was updated, check if it was archived
if socket.assigns.selected_entity_uuid && entity_uuid == socket.assigns.selected_entity_uuid do
entity = Entities.get_entity!(entity_uuid)
# If entity was archived or unpublished, redirect to entities list
if entity.status != "published" do
{:noreply,
socket
|> put_flash(
:warning,
gettext("Entity '%{name}' was %{status} in another session.",
name: entity.display_name,
status: entity.status
)
)
|> redirect(
to: Routes.path("/admin/entities", locale: socket.assigns.current_locale_base)
)}
else
# Update the selected entity and page title with fresh data
socket =
socket
|> assign(:selected_entity, entity)
|> assign(:page_title, entity.display_name)
|> refresh_entities_and_data()
{:noreply, socket}
end
else
{:noreply, refresh_entities_and_data(socket)}
end
end
def handle_info({:entity_deleted, entity_uuid}, socket) do
# If the currently viewed entity was deleted, redirect to entities list
if socket.assigns.selected_entity_uuid && entity_uuid == socket.assigns.selected_entity_uuid do
{:noreply,
socket
|> put_flash(:error, gettext("Entity was deleted in another session."))
|> redirect(to: Routes.path("/admin/entities", locale: socket.assigns.current_locale_base))}
else
{:noreply, refresh_entities_and_data(socket)}
end
end
def handle_info({event, _entity_uuid, _data_uuid}, socket)
when event in [:data_created, :data_updated, :data_deleted] do
socket =
socket
|> refresh_data_stats()
|> apply_filters()
{:noreply, socket}
end
# Helper Functions
defp build_base_path(nil), do: "/admin/entities"
defp build_base_path(entity_uuid) when is_binary(entity_uuid) do
# Get entity by UUID to get its slug
case Entities.get_entity!(entity_uuid) do
nil -> "/admin/entities"
entity -> "/admin/entities/#{entity.name}/data"
end
end
defp build_url_params(_entity_uuid, status, search_term, view_mode) do
params = []
# Don't include entity_uuid in query params since it's in the path
params =
if status && status != "all" do
[{"status", status} | params]
else
params
end
params =
if search_term && String.trim(search_term) != "" do
[{"search", search_term} | params]
else
params
end
params =
if view_mode && view_mode != "table" do
[{"view", view_mode} | params]
else
params
end
URI.encode_query(params)
end
defp apply_filters(socket) do
entity_uuid = socket.assigns[:selected_entity_uuid]
status = socket.assigns[:selected_status] || "all"
search_term = socket.assigns[:search_term] || ""
entity_data_records =
EntityData.list_all_data()
|> filter_by_entity(entity_uuid)
|> filter_by_status(status)
|> filter_by_search(search_term)
assign(socket, :entity_data_records, entity_data_records)
end
defp filter_by_entity(records, nil), do: records
defp filter_by_entity(records, entity_uuid) do
Enum.filter(records, fn record -> record.entity_uuid == entity_uuid end)
end
defp filter_by_status(records, "all"), do: records
defp filter_by_status(records, status) do
Enum.filter(records, fn record -> record.status == status end)
end
defp filter_by_search(records, ""), do: records
defp filter_by_search(records, search_term) do
search_term_lower = String.downcase(String.trim(search_term))
Enum.filter(records, fn record ->
title_match = String.contains?(String.downcase(record.title || ""), search_term_lower)
slug_match = String.contains?(String.downcase(record.slug || ""), search_term_lower)
title_match || slug_match
end)
end
defp refresh_data_stats(socket) do
stats = EntityData.get_data_stats(socket.assigns.selected_entity_uuid)
socket
|> assign(:total_records, stats.total_records)
|> assign(:published_records, stats.published_records)
|> assign(:draft_records, stats.draft_records)
|> assign(:archived_records, stats.archived_records)
end
defp refresh_entities_and_data(socket) do
socket
|> assign(:entities, Entities.list_entities())
|> refresh_data_stats()
|> apply_filters()
end
def status_badge_class(status) do
case status do
"published" -> "badge-success"
"draft" -> "badge-warning"
"archived" -> "badge-neutral"
_ -> "badge-outline"
end
end
def status_label(status) do
case status do
"published" -> gettext("Published")
"draft" -> gettext("Draft")
"archived" -> gettext("Archived")
_ -> gettext("Unknown")
end
end
def status_icon(status) do
case status do
"published" -> "hero-check-circle"
"draft" -> "hero-pencil"
"archived" -> "hero-archive-box"
_ -> "hero-question-mark-circle"
end
end
def get_entity_name(entities, entity_uuid) do
case Enum.find(entities, &(&1.uuid == entity_uuid)) do
nil -> gettext("Unknown")
entity -> entity.display_name
end
end
def get_entity_slug(entities, entity_uuid) do
case Enum.find(entities, &(&1.uuid == entity_uuid)) do
nil -> ""
entity -> entity.name
end
end
def truncate_text(text, length \\ 100)
def truncate_text(text, length) when is_binary(text) do
if String.length(text) > length do
String.slice(text, 0, length) <> "..."
else
text
end
end
def truncate_text(_, _), do: ""
def format_data_preview(data) when is_map(data) do
# For multilang data, show primary language fields
display_data =
if Multilang.multilang_data?(data) do
Multilang.flatten_to_primary(data)
else
data
end
display_data
|> Enum.take(3)
|> Enum.map_join(" • ", fn {key, value} ->
"#{key}: #{truncate_text(to_string(value), 30)}"
end)
end
def format_data_preview(_), do: ""
end