Packages

phoenix_kit

1.7.62
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_form.html.heex
Raw

lib/modules/entities/web/data_form.html.heex

<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={
gettext("%{project_title} %{action} %{entity}",
project_title: @project_title,
action: if(@data_record.uuid, do: gettext("Edit"), else: gettext("New")),
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 --%>
<.admin_page_header back={
PhoenixKit.Utils.Routes.path("/admin/entities/#{@entity.name}/data")
}>
<h1 class="text-xl sm:text-2xl lg:text-3xl font-bold text-base-content">
<%= if @data_record.uuid do %>
{gettext("Edit %{entity}", entity: @entity.display_name)}
<% else %>
{gettext("Create New %{entity}", entity: @entity.display_name)}
<% end %>
</h1>
<p class="text-sm text-base-content/60 mt-0.5">
<%= if @data_record.uuid do %>
{gettext("Update data for the %{entity} entity", entity: @entity.display_name)}
<% else %>
{gettext("Add data for the %{entity} entity", entity: @entity.display_name)}
<% end %>
</p>
</.admin_page_header>
<%!-- Readonly Banner --%>
<%= if @readonly? do %>
<div class="alert alert-info mb-6">
<.icon name="hero-eye" class="w-5 h-5" />
<span>
{gettext(
"This record is currently being edited by another user. You are in view-only mode."
)}
</span>
</div>
<% end %>
<%!-- Edit Mode Form --%>
<.form
:let={f}
for={@changeset}
phx-change="validate"
phx-debounce="500"
phx-submit="save"
class="space-y-8"
>
<div class="flex justify-end">
<button
type="submit"
class="btn btn-primary"
disabled={@readonly?}
>
<%= if @data_record.uuid do %>
{gettext("Update %{entity}", entity: @entity.display_name)}
<% else %>
{gettext("Create %{entity}", entity: @entity.display_name)}
<% end %>
</button>
</div>
<%= if @multilang_enabled && length(@language_tabs) > 1 do %>
<%!-- Multilang: unified card with language tabs wrapping all content --%>
<div class="card bg-base-100 shadow-xl">
<%!-- Language tab bar --%>
<div class="card-body pb-0">
<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>
<div class="alert alert-info py-2 text-xs mb-4">
<.icon name="hero-information-circle" class="w-4 h-4" />
<span>
{gettext(
"Use the language tabs below to translate this record's content. The primary language (marked with a star) is required. Other languages are optional — any empty fields will fall back to the primary language value."
)}
</span>
</div>
<% compact = length(@language_tabs) > 5 %>
<div
role="tablist"
class="inline-flex flex-wrap items-center gap-1 p-1 bg-base-200 rounded-box mb-4"
>
<%= if compact do %>
<%= for {tab, idx} <- Enum.with_index(@language_tabs) do %>
<%= if idx > 0 do %>
<span class="text-base-content/30 text-xs">|</span>
<% end %>
<button
type="button"
role="tab"
phx-click={switch_lang_js(tab.code, @current_lang)}
title={tab.name}
class={[
"inline-flex items-center gap-1 px-2 py-1 rounded-lg text-sm transition-all cursor-pointer",
@current_lang == tab.code && "bg-base-100 font-semibold shadow-sm"
]}
>
<%= 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_lang_js(tab.code, @current_lang)}
class={[
"inline-flex items-center gap-1 px-2 py-1 rounded-lg text-sm transition-all font-medium cursor-pointer",
@current_lang == tab.code && "bg-base-100 font-semibold shadow-sm"
]}
>
<%= 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>
<span class="w-px h-4 bg-base-content/20 self-center"></span>
<% else %>
<button
type="button"
role="tab"
phx-click={switch_lang_js(tab.code, @current_lang)}
class={[
"inline-flex items-center gap-1 px-2 py-1 rounded-lg text-sm transition-all cursor-pointer",
@current_lang == tab.code && "bg-base-100 font-semibold shadow-sm"
]}
>
<%= if tab.flag do %>
<span>{tab.flag}</span>
<% end %>
<span>{tab.name}</span>
</button>
<% end %>
<% end %>
<% end %>
</div>
</div>
<%!-- Skeleton placeholders (shown instantly on tab click).
IMPORTANT: IDs MUST include @current_lang so morphdom treats them as new
elements on language switch. Static IDs would preserve JS-modified classes
across LiveView patches, causing skeletons to stick. --%>
<div
id={"translatable-skeletons-#{@current_lang}"}
data-translatable="skeletons"
class="hidden card-body pt-4"
aria-busy="true"
aria-label={gettext("Loading language content")}
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<div class="skeleton h-4 w-24"></div>
<div class="skeleton h-12 w-full"></div>
</div>
<div class="space-y-2">
<div class="skeleton h-4 w-32"></div>
<div class="skeleton h-12 w-full"></div>
<div class="skeleton h-3 w-48"></div>
</div>
</div>
<div class="divider my-2"></div>
<div class="space-y-4">
<div class="space-y-2">
<div class="skeleton h-4 w-24"></div>
<div class="skeleton h-12 w-full"></div>
</div>
<div class="space-y-2">
<div class="skeleton h-4 w-40"></div>
<div class="skeleton h-24 w-full"></div>
</div>
</div>
</div>
<div
id={"translatable-fields-#{@current_lang}"}
data-translatable="fields"
>
<div class="divider mx-6 my-0"></div>
<%!-- Tab content: Title & Slug (translatable) --%>
<% lang_data =
Multilang.get_raw_language_data(
Ecto.Changeset.get_field(@changeset, :data),
@current_lang
) %>
<div class="card-body pt-4 pb-4">
<h3 class="text-sm font-semibold text-base-content/70 uppercase tracking-wide mb-3">
<.icon name="hero-information-circle" class="w-4 h-4 inline -mt-0.5" />
{gettext("Basic Information")}
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<%!-- Title --%>
<div>
<%= if @current_lang == @primary_language do %>
<.label for="phoenix_kit_entity_data_title">
{gettext("Title")} *
</.label>
<input
type="text"
name="phoenix_kit_entity_data[title]"
id="phoenix_kit_entity_data_title"
value={Ecto.Changeset.get_field(@changeset, :title) || ""}
placeholder={gettext("Enter a title for this record")}
class="input input-bordered w-full"
phx-debounce="300"
required
disabled={@readonly?}
/>
<% else %>
<.label for={"phoenix_kit_entity_data_title_#{@current_lang}"}>
{gettext("Title")}
</.label>
<input
type="text"
name="phoenix_kit_entity_data[lang_title]"
id={"phoenix_kit_entity_data_title_#{@current_lang}"}
value={lang_data["_title"]}
placeholder={
Ecto.Changeset.get_field(@changeset, :title) ||
gettext("Enter a title for this record")
}
class="input input-bordered w-full"
phx-debounce="300"
disabled={@readonly?}
/>
<% end %>
</div>
<%!-- Slug (translatable) --%>
<div>
<%= if @current_lang == @primary_language do %>
<.label for="phoenix_kit_entity_data_slug">
{gettext("Slug (URL-friendly identifier)")}
<button
type="button"
class="btn btn-ghost btn-xs ml-2"
phx-click="generate_slug"
title={gettext("Generate from title")}
disabled={@readonly?}
>
<.icon name="hero-arrow-path" class="w-3 h-3" /> {gettext("Generate")}
</button>
</.label>
<input
type="text"
name="phoenix_kit_entity_data[slug]"
id="phoenix_kit_entity_data_slug"
value={Ecto.Changeset.get_field(@changeset, :slug) || ""}
placeholder={gettext("auto-generated-slug")}
class="input input-bordered w-full"
pattern="[a-z0-9]+(?:-[a-z0-9]+)*"
title={gettext("Use lowercase letters, numbers, and hyphens only.")}
phx-debounce="300"
disabled={@readonly?}
/>
<.label class="label">
<span class="label-text-alt">
{gettext("Leave empty to auto-generate from title")}
</span>
</.label>
<% else %>
<.label for={"phoenix_kit_entity_data_slug_#{@current_lang}"}>
{gettext("Slug (URL-friendly identifier)")}
<button
type="button"
class="btn btn-ghost btn-xs ml-2"
phx-click="generate_slug"
title={gettext("Generate from title")}
disabled={@readonly?}
>
<.icon name="hero-arrow-path" class="w-3 h-3" /> {gettext("Generate")}
</button>
</.label>
<input
type="text"
name="phoenix_kit_entity_data[lang_slug]"
id={"phoenix_kit_entity_data_slug_#{@current_lang}"}
value={lang_data["_slug"]}
placeholder={
Ecto.Changeset.get_field(@changeset, :slug) ||
gettext("auto-generated-slug")
}
class="input input-bordered w-full"
pattern="[a-z0-9]+(?:-[a-z0-9]+)*"
title={gettext("Use lowercase letters, numbers, and hyphens only.")}
phx-debounce="300"
disabled={@readonly?}
/>
<.label class="label">
<span class="label-text-alt">
{gettext("Leave empty to use the primary language slug")}
</span>
</.label>
<% end %>
</div>
</div>
</div>
<%= if @entity.fields_definition != nil and @entity.fields_definition != [] do %>
<div class="divider mx-6 my-0"></div>
<%!-- Tab content: Custom Fields (translatable) --%>
<div class="card-body pt-4">
<h3 class="text-sm font-semibold text-base-content/70 uppercase tracking-wide mb-3">
<.icon name="hero-list-bullet" class="w-4 h-4 inline -mt-0.5" />
{gettext("Custom Fields")}
</h3>
<%!-- Dynamic form fields generated by FormBuilder --%>
{PhoenixKit.Modules.Entities.FormBuilder.build_fields(@entity, f,
wrapper_class: "mb-6",
disabled: @readonly?,
lang_code: if(@multilang_enabled, do: @current_lang, else: nil)
)}
</div>
<% end %>
</div>
</div>
<%!-- Record Settings (non-translatable, separate card) --%>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-lg mb-4">
<.icon name="hero-cog-6-tooth" class="w-5 h-5" />
{gettext("Record Settings")}
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<%!-- Status --%>
<div>
<.label for="phoenix_kit_entity_data_status">{gettext("Status")}</.label>
<select
name="phoenix_kit_entity_data[status]"
class="select select-bordered w-full"
disabled={@readonly?}
>
<option
value="draft"
selected={Ecto.Changeset.get_field(@changeset, :status) == "draft"}
>
{gettext("Draft")}
</option>
<option
value="published"
selected={Ecto.Changeset.get_field(@changeset, :status) == "published"}
>
{gettext("Published")}
</option>
<option
value="archived"
selected={Ecto.Changeset.get_field(@changeset, :status) == "archived"}
>
{gettext("Archived")}
</option>
</select>
</div>
<%!-- Entity Type (Read-only) --%>
<div>
<.label>{gettext("Entity Type")}</.label>
<div class="input input-bordered w-full bg-base-200 flex items-center">
<%= if @entity.icon do %>
<.icon name={@entity.icon} class="w-4 h-4 mr-2" />
<% end %>
{@entity.display_name}
</div>
<input
type="hidden"
name="phoenix_kit_entity_data[entity_uuid]"
value={@entity.uuid}
/>
<input
type="hidden"
name="phoenix_kit_entity_data[entity_uuid]"
value={@entity.uuid}
/>
</div>
</div>
</div>
</div>
<% else %>
<%!-- Non-multilang: separate cards (original layout) --%>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">
<.icon name="hero-information-circle" class="w-6 h-6" />
{gettext("Basic Information")}
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<%!-- Title --%>
<div>
<.label for="phoenix_kit_entity_data_title">{gettext("Title")} *</.label>
<input
type="text"
name="phoenix_kit_entity_data[title]"
id="phoenix_kit_entity_data_title"
value={Ecto.Changeset.get_field(@changeset, :title) || ""}
placeholder={gettext("Enter a title for this record")}
class="input input-bordered w-full"
phx-debounce="300"
required
disabled={@readonly?}
/>
</div>
<%!-- Slug with Generator --%>
<div>
<.label for="phoenix_kit_entity_data_slug">
{gettext("Slug (URL-friendly identifier)")}
<button
type="button"
class="btn btn-ghost btn-xs ml-2"
phx-click="generate_slug"
title={gettext("Generate from title")}
disabled={@readonly?}
>
<.icon name="hero-arrow-path" class="w-3 h-3" /> {gettext("Generate")}
</button>
</.label>
<input
type="text"
name="phoenix_kit_entity_data[slug]"
id="phoenix_kit_entity_data_slug"
value={Ecto.Changeset.get_field(@changeset, :slug) || ""}
placeholder={gettext("auto-generated-slug")}
class="input input-bordered w-full"
pattern="[a-z0-9]+(?:-[a-z0-9]+)*"
title={gettext("Use lowercase letters, numbers, and hyphens only.")}
phx-debounce="300"
disabled={@readonly?}
/>
<.label class="label">
<span class="label-text-alt">
{gettext("Leave empty to auto-generate from title")}
</span>
</.label>
</div>
<%!-- Status --%>
<div>
<.label for="phoenix_kit_entity_data_status">{gettext("Status")}</.label>
<select
name="phoenix_kit_entity_data[status]"
class="select select-bordered w-full"
disabled={@readonly?}
>
<option
value="draft"
selected={Ecto.Changeset.get_field(@changeset, :status) == "draft"}
>
{gettext("Draft")}
</option>
<option
value="published"
selected={Ecto.Changeset.get_field(@changeset, :status) == "published"}
>
{gettext("Published")}
</option>
<option
value="archived"
selected={Ecto.Changeset.get_field(@changeset, :status) == "archived"}
>
{gettext("Archived")}
</option>
</select>
</div>
<%!-- Entity Type (Read-only) --%>
<div>
<.label>{gettext("Entity Type")}</.label>
<div class="input input-bordered w-full bg-base-200 flex items-center">
<%= if @entity.icon do %>
<.icon name={@entity.icon} class="w-4 h-4 mr-2" />
<% end %>
{@entity.display_name}
</div>
<input
type="hidden"
name="phoenix_kit_entity_data[entity_uuid]"
value={@entity.uuid}
/>
<input
type="hidden"
name="phoenix_kit_entity_data[entity_uuid]"
value={@entity.uuid}
/>
</div>
</div>
</div>
</div>
<%= if @entity.fields_definition != nil and @entity.fields_definition != [] do %>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">
<.icon name="hero-list-bullet" class="w-6 h-6" /> {gettext("Custom Fields")}
</h2>
<%!-- Dynamic form fields generated by FormBuilder --%>
{PhoenixKit.Modules.Entities.FormBuilder.build_fields(@entity, f,
wrapper_class: "mb-6",
disabled: @readonly?,
lang_code: nil
)}
</div>
</div>
<% end %>
<% end %>
<%!-- Form Actions --%>
<div class="flex justify-between items-center">
<div class="flex gap-2">
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/entities/#{@entity.name}/data")}
class="btn btn-outline"
>
{gettext("Cancel")}
</.link>
<button type="button" phx-click="reset" class="btn btn-warning" disabled={@readonly?}>
<.icon name="hero-arrow-path" class="w-4 h-4" />
{gettext("Reset Changes")}
</button>
</div>
<button
type="submit"
class="btn btn-primary"
disabled={@readonly?}
>
<.icon name="hero-check" class="w-4 h-4 mr-2" />
<%= if @data_record.uuid do %>
{gettext("Update %{entity}", entity: @entity.display_name)}
<% else %>
{gettext("Create %{entity}", entity: @entity.display_name)}
<% end %>
</button>
</div>
</.form>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>