Packages

phoenix_kit

1.7.76
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_view.ex
Raw

lib/modules/entities/web/data_view.ex

defmodule PhoenixKit.Modules.Entities.Web.DataView do
@moduledoc """
LiveView for viewing entity data records.
Displays data with public form fields separated from other fields.
Uses FormBuilder with disabled fields for the form section.
"""
# Extension point: declare a route at the same path BEFORE phoenix_kit_routes()
# in your router to override this view. See lib/modules/entities/README.md.
use PhoenixKitWeb, :live_view
on_mount PhoenixKit.Modules.Entities.Web.Hooks
alias PhoenixKit.Modules.Entities
alias PhoenixKit.Modules.Entities.EntityData
alias PhoenixKit.Modules.Entities.FormBuilder
alias PhoenixKit.Modules.Entities.Multilang
alias PhoenixKit.Settings
@impl true
def mount(%{"entity_slug" => entity_slug, "id" => id} = params, _session, socket) do
locale =
params["locale"] || socket.assigns[:current_locale]
entity = Entities.get_entity_by_name(entity_slug)
data_record = EntityData.get!(id)
mount_data_view(socket, entity, data_record, locale)
end
def mount(%{"entity_id" => entity_uuid, "id" => id} = params, _session, socket) do
locale =
params["locale"] || socket.assigns[:current_locale]
entity = Entities.get_entity!(entity_uuid)
data_record = EntityData.get!(id)
mount_data_view(socket, entity, data_record, locale)
end
defp mount_data_view(socket, entity, data_record, locale) do
project_title = Settings.get_project_title()
# Get public form configuration
settings = entity.settings || %{}
public_form_enabled = Map.get(settings, "public_form_enabled", false)
public_form_fields = Map.get(settings, "public_form_fields", [])
# Check if this record was submitted via public form
is_public_submission = public_submission?(data_record.metadata)
# Get all field definitions
fields_definition = entity.fields_definition || []
# Separate fields into form fields and other fields
# Show form fields separately if:
# 1. Public form is currently enabled, OR
# 2. This record was submitted via public form (even if form is now disabled)
{form_fields, other_fields} =
if public_form_enabled || is_public_submission do
Enum.split_with(fields_definition, fn field ->
field["key"] in public_form_fields
end)
else
# If public form not enabled and not a public submission, all fields go to "other"
{[], fields_definition}
end
# Get data values
data = data_record.data || %{}
# Create changeset for FormBuilder (readonly display)
changeset = EntityData.change(data_record)
# Create a modified entity with only form fields for FormBuilder
form_entity = %{entity | fields_definition: form_fields}
# Create a modified entity with only other fields for display
other_entity = %{entity | fields_definition: other_fields}
# Multilang assigns (driven by Languages module globally)
multilang_enabled = Multilang.enabled?()
primary_language = Multilang.primary_language()
language_tabs = Multilang.build_language_tabs()
socket =
socket
|> assign(:current_locale, locale)
|> assign(:page_title, gettext("View Data"))
|> assign(:project_title, project_title)
|> assign(:entity, entity)
|> assign(:form_entity, form_entity)
|> assign(:other_entity, other_entity)
|> assign(:data_record, data_record)
|> assign(:changeset, changeset)
|> assign(:data, data)
|> assign(:public_form_enabled, public_form_enabled)
|> assign(:form_fields, form_fields)
|> assign(:other_fields, other_fields)
|> assign(:public_form_title, Map.get(settings, "public_form_title", ""))
|> assign(:public_form_description, Map.get(settings, "public_form_description", ""))
|> assign(:metadata, data_record.metadata || %{})
|> assign(:is_public_submission, public_submission?(data_record.metadata))
|> assign(:multilang_enabled, multilang_enabled)
|> assign(:primary_language, primary_language)
|> assign(:current_lang, primary_language)
|> assign(:language_tabs, language_tabs)
{:ok, socket}
end
defp public_submission?(nil), do: false
defp public_submission?(metadata), do: Map.get(metadata, "source") == "public_form"
@impl true
def handle_event("switch_language", %{"lang" => lang_code}, socket) do
enabled = Multilang.enabled_languages()
if lang_code in enabled do
{:noreply, assign(socket, :current_lang, lang_code)}
else
{:noreply, socket}
end
end
@impl true
def render(assigns) do
~H"""
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={
gettext("%{project_title} View %{entity}",
project_title: @project_title,
entity: @entity.display_name
)
}
current_path={@url_path}
project_title={@project_title}
current_locale={@current_locale}
>
<div class="container flex flex-col mx-auto px-4 py-6">
<%!-- Header Section --%>
<header class="w-full relative mb-6">
<%!-- Back Button --%>
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/entities/#{@entity.name}/data")}
class="btn btn-ghost btn-sm absolute left-0 top-0 -mb-12"
>
<.icon name="hero-arrow-left" class="w-4 h-4" />
</.link>
<%!-- Title Section --%>
<div class="text-center">
<h1 class="text-4xl font-bold text-base-content mb-3">
{@data_record.title}
</h1>
<p class="text-lg text-base-content/70">
{gettext("Viewing %{entity} record", entity: @entity.display_name)}
</p>
<%= if @data_record.slug do %>
<p class="text-sm text-base-content/50 mt-2">
<.icon name="hero-link" class="w-4 h-4 inline" />
{@data_record.slug}
</p>
<% end %>
</div>
</header>
<%!-- Record Metadata --%>
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title text-xl mb-4">
<.icon name="hero-information-circle" class="w-5 h-5" />
{gettext("Record Information")}
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<span class="text-sm text-base-content/60">{gettext("Status")}</span>
<div class="mt-1">
<span class={"badge #{status_badge_class(@data_record.status)}"}>
{@data_record.status}
</span>
</div>
</div>
<div>
<span class="text-sm text-base-content/60">{gettext("Created")}</span>
<div class="mt-1 font-medium">
{PhoenixKit.Utils.Date.format_datetime_with_user_format(@data_record.date_created)}
</div>
</div>
<div>
<span class="text-sm text-base-content/60">{gettext("Updated")}</span>
<div class="mt-1 font-medium">
{PhoenixKit.Utils.Date.format_datetime_with_user_format(@data_record.date_updated)}
</div>
</div>
</div>
</div>
</div>
<%!-- Language Selector (only when multilang enabled) --%>
<%= if @multilang_enabled && length(@language_tabs) > 1 do %>
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<.icon name="hero-language" class="w-5 h-5 text-primary" />
<h2 class="card-title text-lg m-0">{gettext("Content Language")}</h2>
</div>
<% primary_tab =
Enum.find(@language_tabs, fn t -> t.is_primary end) %>
<%= if primary_tab do %>
<div class="flex items-center gap-1.5 text-xs text-base-content/60">
<.icon name="hero-star-solid" class="w-3.5 h-3.5 text-primary" />
{gettext("Primary: %{lang}", lang: primary_tab.name)}
</div>
<% end %>
</div>
<% compact = length(@language_tabs) > 5 %>
<div
role="tablist"
class="flex flex-wrap items-center gap-1 bg-base-200 rounded-box p-1"
>
<%= if compact do %>
<%= for {tab, idx} <- Enum.with_index(@language_tabs) do %>
<%= if idx > 0 do %>
<span class="text-base-content/30">|</span>
<% end %>
<button
type="button"
role="tab"
phx-click="switch_language"
phx-value-lang={tab.code}
title={tab.name}
class={[
"tab gap-1 transition-all",
@current_lang == tab.code && "tab-active"
]}
>
<%= if tab.flag do %>
<span>{tab.flag}</span>
<% end %>
<span>{tab.short_code}</span>
<%= if tab.is_primary do %>
<.icon name="hero-star-solid" class="w-3 h-3 text-primary" />
<% end %>
</button>
<% end %>
<% else %>
<%= for tab <- @language_tabs do %>
<%= if tab.is_primary do %>
<button
type="button"
role="tab"
phx-click="switch_language"
phx-value-lang={tab.code}
class={[
"tab gap-1.5 transition-all font-medium",
@current_lang == tab.code && "tab-active"
]}
>
<%= if tab.flag do %>
<span>{tab.flag}</span>
<% end %>
<span>{tab.name}</span>
<.icon name="hero-star-solid" class="w-3 h-3 text-primary" />
</button>
<div class="divider divider-horizontal mx-0.5 h-6 self-center"></div>
<% else %>
<button
type="button"
role="tab"
phx-click="switch_language"
phx-value-lang={tab.code}
class={[
"tab gap-1.5 transition-all",
@current_lang == tab.code && "tab-active"
]}
>
<%= if tab.flag do %>
<span>{tab.flag}</span>
<% end %>
<span>{tab.name}</span>
</button>
<% end %>
<% end %>
<% end %>
</div>
<%= if @current_lang == @primary_language do %>
<p class="text-xs text-base-content/50 mt-3">
<.icon name="hero-information-circle" class="w-3.5 h-3.5 inline -mt-0.5" />
{gettext("This is the primary language.")}
</p>
<% else %>
<p class="text-xs text-base-content/50 mt-3">
<.icon name="hero-information-circle" class="w-3.5 h-3.5 inline -mt-0.5" />
{gettext("Fields without a value show the primary language value.")}
</p>
<% end %>
</div>
</div>
<% end %>
<%= if (@public_form_enabled || @is_public_submission) && length(@form_fields) > 0 do %>
<%!-- Public Form Fields Section - Using FormBuilder with disabled inputs --%>
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title text-xl mb-2">
<.icon name="hero-document-text" class="w-5 h-5" />
<%= if @public_form_title != "" do %>
{@public_form_title}
<% else %>
{gettext("Form Submission")}
<% end %>
</h2>
<%= if @public_form_description != "" do %>
<p class="text-base-content/70 mb-4">{@public_form_description}</p>
<% end %>
<%!-- Use FormBuilder with disabled fields --%>
{FormBuilder.build_fields(@form_entity, @changeset,
wrapper_class: "mb-4",
disabled: true,
lang_code: if(@multilang_enabled, do: @current_lang, else: nil)
)}
</div>
</div>
<% end %>
<%= if @is_public_submission do %>
<%!-- Security Warnings Section (if any) --%>
<%= if @metadata["security_warnings"] && length(@metadata["security_warnings"]) > 0 do %>
<div class="alert alert-warning mb-6">
<.icon name="hero-exclamation-triangle" class="w-6 h-6" />
<div>
<h3 class="font-bold">{gettext("Security Flags")}</h3>
<div class="text-sm mt-1">
<%= for warning <- @metadata["security_warnings"] do %>
<div class="flex items-center gap-2 mt-1">
<span class="badge badge-warning badge-sm">
{security_warning_label(warning["type"])}
</span>
<span class="text-xs text-base-content/70">
{security_action_label(warning["action"])}
</span>
</div>
<% end %>
</div>
</div>
</div>
<% end %>
<%!-- Submission Metadata Section --%>
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title text-xl mb-4">
<.icon name="hero-globe-alt" class="w-5 h-5" />
{gettext("Submission Details")}
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<%= if @metadata["ip_address"] do %>
<div class="flex items-start gap-3">
<div class="p-2 bg-base-200 rounded-lg">
<.icon name="hero-signal" class="w-5 h-5 text-base-content/60" />
</div>
<div>
<span class="text-sm text-base-content/60">{gettext("IP Address")}</span>
<div class="font-mono text-sm">{@metadata["ip_address"]}</div>
</div>
</div>
<% end %>
<%= if @metadata["browser"] do %>
<div class="flex items-start gap-3">
<div class="p-2 bg-base-200 rounded-lg">
<.icon name="hero-window" class="w-5 h-5 text-base-content/60" />
</div>
<div>
<span class="text-sm text-base-content/60">{gettext("Browser")}</span>
<div class="font-medium">{@metadata["browser"]}</div>
</div>
</div>
<% end %>
<%= if @metadata["os"] do %>
<div class="flex items-start gap-3">
<div class="p-2 bg-base-200 rounded-lg">
<.icon name="hero-computer-desktop" class="w-5 h-5 text-base-content/60" />
</div>
<div>
<span class="text-sm text-base-content/60">{gettext("Operating System")}</span>
<div class="font-medium">{@metadata["os"]}</div>
</div>
</div>
<% end %>
<%= if @metadata["device"] do %>
<div class="flex items-start gap-3">
<div class="p-2 bg-base-200 rounded-lg">
<.icon
name={device_icon(@metadata["device"])}
class="w-5 h-5 text-base-content/60"
/>
</div>
<div>
<span class="text-sm text-base-content/60">{gettext("Device")}</span>
<div class="font-medium capitalize">{@metadata["device"]}</div>
</div>
</div>
<% end %>
<%= if @metadata["submitted_at"] do %>
<div class="flex items-start gap-3">
<div class="p-2 bg-base-200 rounded-lg">
<.icon name="hero-clock" class="w-5 h-5 text-base-content/60" />
</div>
<div>
<span class="text-sm text-base-content/60">{gettext("Submitted At")}</span>
<div class="font-medium">{format_submitted_at(@metadata["submitted_at"])}</div>
</div>
</div>
<% end %>
<%= if @metadata["time_to_submit_seconds"] do %>
<div class="flex items-start gap-3">
<div class="p-2 bg-base-200 rounded-lg">
<.icon name="hero-stopwatch" class="w-5 h-5 text-base-content/60" />
</div>
<div>
<span class="text-sm text-base-content/60">{gettext("Time to Submit")}</span>
<div class="font-medium">
{format_duration(@metadata["time_to_submit_seconds"])}
</div>
</div>
</div>
<% end %>
<%= if @metadata["referer"] do %>
<div class="flex items-start gap-3 md:col-span-2 lg:col-span-3">
<div class="p-2 bg-base-200 rounded-lg">
<.icon
name="hero-arrow-top-right-on-square"
class="w-5 h-5 text-base-content/60"
/>
</div>
<div class="min-w-0 flex-1">
<span class="text-sm text-base-content/60">{gettext("Referrer")}</span>
<div class="font-mono text-sm truncate" title={@metadata["referer"]}>
{@metadata["referer"]}
</div>
</div>
</div>
<% end %>
</div>
<%= if @metadata["user_agent"] do %>
<div class="mt-4 pt-4 border-t border-base-200">
<details class="collapse collapse-arrow bg-base-200 rounded-lg">
<summary class="collapse-title text-sm font-medium py-2 min-h-0">
{gettext("Full User Agent")}
</summary>
<div class="collapse-content">
<code class="text-xs break-all">{@metadata["user_agent"]}</code>
</div>
</details>
</div>
<% end %>
</div>
</div>
<% end %>
<%= if length(@other_fields) > 0 do %>
<%!-- Other Fields Section - Using FormBuilder with disabled inputs --%>
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title text-xl mb-4">
<.icon name="hero-squares-2x2" class="w-5 h-5" />
<%= if (@public_form_enabled || @is_public_submission) && length(@form_fields) > 0 do %>
{gettext("Additional Data")}
<% else %>
{gettext("Data Fields")}
<% end %>
</h2>
<%!-- Use FormBuilder with disabled fields --%>
{FormBuilder.build_fields(@other_entity, @changeset,
wrapper_class: "mb-4",
disabled: true,
lang_code: if(@multilang_enabled, do: @current_lang, else: nil)
)}
</div>
</div>
<% end %>
<%= if map_size(@data) == 0 && length(@form_fields) == 0 && length(@other_fields) == 0 do %>
<%!-- Empty State --%>
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body text-center py-12">
<div class="text-4xl mb-4 opacity-50">📄</div>
<p class="text-base-content/60">{gettext("No data fields have been filled in yet.")}</p>
</div>
</div>
<% end %>
<%!-- Actions --%>
<div class="flex justify-between items-center">
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/entities/#{@entity.name}/data")}
class="btn btn-outline"
>
{gettext("Back")}
</.link>
<.link
navigate={
PhoenixKit.Utils.Routes.path(
"/admin/entities/#{@entity.name}/data/#{@data_record.uuid}/edit"
)
}
class="btn btn-primary"
>
<.icon name="hero-pencil" class="w-4 h-4 mr-2" />
{gettext("Edit %{entity}", entity: @entity.display_name)}
</.link>
</div>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>
"""
end
defp status_badge_class("published"), do: "badge-success"
defp status_badge_class("draft"), do: "badge-warning"
defp status_badge_class("archived"), do: "badge-ghost"
defp status_badge_class(_), do: "badge-ghost"
defp device_icon("mobile"), do: "hero-device-phone-mobile"
defp device_icon("tablet"), do: "hero-device-tablet"
defp device_icon(_), do: "hero-computer-desktop"
defp format_submitted_at(iso_string) when is_binary(iso_string) do
case DateTime.from_iso8601(iso_string) do
{:ok, datetime, _offset} ->
PhoenixKit.Utils.Date.format_datetime_with_user_format(datetime)
_ ->
iso_string
end
end
defp format_submitted_at(_), do: "-"
defp format_duration(seconds) when is_integer(seconds) do
cond do
seconds < 60 ->
ngettext("%{count} second", "%{count} seconds", seconds, count: seconds)
seconds < 3600 ->
minutes = div(seconds, 60)
ngettext("%{count} minute", "%{count} minutes", minutes, count: minutes)
true ->
hours = div(seconds, 3600)
minutes = div(rem(seconds, 3600), 60)
if minutes > 0 do
"#{ngettext("%{count} hour", "%{count} hours", hours, count: hours)}, #{ngettext("%{count} minute", "%{count} minutes", minutes, count: minutes)}"
else
ngettext("%{count} hour", "%{count} hours", hours, count: hours)
end
end
end
defp format_duration(_), do: "-"
defp security_warning_label("honeypot"), do: gettext("Honeypot triggered")
defp security_warning_label("too_fast"), do: gettext("Submitted too fast")
defp security_warning_label("rate_limited"), do: gettext("Rate limited")
defp security_warning_label(type), do: type
defp security_action_label("save_suspicious"), do: gettext("Marked as suspicious")
defp security_action_label("save_log"), do: gettext("Logged warning")
defp security_action_label(action), do: action
end