Packages

phoenix_kit

1.7.77
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 entity_form.html.heex
Raw

lib/modules/entities/web/entity_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(@entity.uuid, do: gettext("Edit"), else: gettext("New")),
entity: gettext("Entity")
)
}
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")}>
<h1 class="text-xl sm:text-2xl lg:text-3xl font-bold text-base-content">
{if @entity.uuid, do: gettext("Edit Entity"), else: gettext("Create New Entity")}
</h1>
<p class="text-sm text-base-content/60 mt-0.5">
{gettext("Define your custom content type with dynamic fields")}
</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 entity is currently being edited by another user. You are in view-only mode."
)}
</span>
</div>
<% end %>
<.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={!@changeset.valid? or @readonly?}
>
{if @entity.uuid, do: gettext("Update Entity"), else: gettext("Create Entity")}
</button>
</div>
<%!-- Entity Metadata Section --%>
<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(
"Entity Information"
)}
</h2>
<% translations = (@entity.settings || %{})["translations"] || %{} %>
<% is_secondary = @multilang_enabled && @current_lang != @primary_language %>
<% lang_translations =
if is_secondary, do: translations[@current_lang] || %{}, else: %{} %>
<%!-- Language tabs --%>
<%= if @multilang_enabled && length(@language_tabs) > 1 do %>
<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 entity's name, plural name, slug, and description. 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>
<% end %>
<%!-- 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 space-y-6"
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-36"></div>
<div class="skeleton h-12 w-full"></div>
<div class="skeleton h-3 w-48"></div>
</div>
<div class="space-y-2">
<div class="skeleton h-4 w-36"></div>
<div class="skeleton h-12 w-full"></div>
<div class="skeleton h-3 w-48"></div>
</div>
</div>
<div class="space-y-2">
<div class="skeleton h-4 w-24"></div>
<div class="skeleton h-12 w-full"></div>
<div class="skeleton h-3 w-56"></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>
<%!-- Translatable fields --%>
<div
id={"translatable-fields-#{@current_lang}"}
data-translatable="fields"
class="space-y-6"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<%!-- Entity Name (Singular) --%>
<div>
<.label for="entity_display_name">
{gettext("Entity Name (Singular)")}{unless is_secondary, do: " *"}
</.label>
<%= if is_secondary do %>
<input
id={"entity_display_name_#{@current_lang}"}
type="text"
name={"entities[translations][#{@current_lang}][display_name]"}
value={lang_translations["display_name"]}
placeholder={f[:display_name].value || gettext("Brand")}
class="input input-bordered w-full"
phx-debounce="300"
disabled={@readonly?}
/>
<% else %>
<.input
field={f[:display_name]}
type="text"
placeholder={gettext("Brand")}
phx-debounce="300"
disabled={@readonly?}
/>
<% end %>
<.label class="label">
<span class="label-text-alt">
{gettext("Singular form (e.g., \"Brand\")")}
</span>
</.label>
</div>
<%!-- Entity Name (Plural) --%>
<div>
<.label for="entity_display_name_plural">
{gettext("Entity Name (Plural)")}{unless is_secondary, do: " *"}
</.label>
<%= if is_secondary do %>
<input
id={"entity_display_name_plural_#{@current_lang}"}
type="text"
name={"entities[translations][#{@current_lang}][display_name_plural]"}
value={lang_translations["display_name_plural"]}
placeholder={f[:display_name_plural].value || gettext("Brands")}
class="input input-bordered w-full"
phx-debounce="300"
disabled={@readonly?}
/>
<% else %>
<.input
field={f[:display_name_plural]}
type="text"
placeholder={gettext("Brands")}
phx-debounce="300"
disabled={@readonly?}
/>
<% end %>
<.label class="label">
<span class="label-text-alt">
{gettext("Plural form (e.g., \"Brands\")")}
</span>
</.label>
</div>
</div>
<%!-- Slug (translatable) --%>
<div>
<.label for="entity_name">
{gettext("Slug")}{unless is_secondary, do: " *"}
<%= unless is_secondary do %>
<button
type="button"
class="btn btn-ghost btn-xs ml-2"
phx-click="generate_entity_slug"
title={gettext("Generate from Entity Name")}
disabled={@readonly?}
>
<.icon name="hero-arrow-path" class="w-3 h-3" /> {gettext("Generate")}
</button>
<% end %>
</.label>
<%= if is_secondary do %>
<input
id={"entity_name_#{@current_lang}"}
type="text"
name={"entities[translations][#{@current_lang}][name]"}
value={lang_translations["name"]}
placeholder={f[:name].value || gettext("brand")}
class="input input-bordered w-full"
phx-debounce="300"
disabled={@readonly?}
/>
<% else %>
<.input
field={f[:name]}
type="text"
placeholder={gettext("brand")}
phx-debounce="300"
disabled={@readonly?}
/>
<% end %>
<.label class="label">
<span class="label-text-alt">
{gettext("snake_case identifier used in the system")}
</span>
</.label>
</div>
<%!-- Description (translatable) --%>
<div>
<.label for="entity_description">{gettext("Description (Optional)")}</.label>
<%= if is_secondary do %>
<textarea
id={"entity_description_#{@current_lang}"}
name={"entities[translations][#{@current_lang}][description]"}
placeholder={
f[:description].value || gettext("Describe what this entity represents...")
}
class="textarea textarea-bordered w-full"
rows="3"
phx-debounce="300"
disabled={@readonly?}
>{lang_translations["description"]}</textarea>
<% else %>
<textarea
id="entity_description"
name={f[:description].name}
placeholder={gettext("Describe what this entity represents...")}
class="textarea textarea-bordered w-full"
rows="3"
phx-debounce="300"
disabled={@readonly?}
>{f[:description].value}</textarea>
<% end %>
</div>
</div>
</div>
</div>
<%!-- Entity System Settings (non-translatable) --%>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">
<.icon name="hero-cog-6-tooth" class="w-6 h-6" /> {gettext("System Settings")}
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<%!-- Icon --%>
<div>
<.label for="entity_icon">{gettext("Icon (Optional)")}</.label>
<div class="flex gap-2">
<div class="flex-1">
<.input
field={f[:icon]}
type="text"
placeholder={gettext("hero-document-text")}
phx-debounce="300"
disabled={@readonly?}
/>
</div>
<button
type="button"
phx-click="open_icon_picker"
class="btn btn-outline btn-square"
title={gettext("Browse icons")}
disabled={@readonly?}
>
<.icon name="hero-squares-2x2" class="w-5 h-5" />
</button>
<%= if f[:icon].value && f[:icon].value != "" do %>
<button
type="button"
phx-click="clear_icon"
class="btn btn-outline btn-square btn-error"
title={gettext("Clear icon")}
disabled={@readonly?}
>
<.icon name="hero-x-mark" class="w-5 h-5" />
</button>
<%= if String.starts_with?(f[:icon].value, "hero-") do %>
<div class="btn btn-square btn-ghost pointer-events-none">
<.icon name={f[:icon].value} class="w-6 h-6" />
</div>
<% end %>
<% end %>
</div>
<.label class="label">
<span class="label-text-alt">
{gettext("Heroicon name or click Browse")}
</span>
</.label>
</div>
<%!-- Status --%>
<div>
<.label for="entity_status">{gettext("Status")} *</.label>
<select
id="entity_status"
name={f[:status].name}
class="select select-bordered w-full"
required
disabled={@readonly?}
>
<option value="published" selected={f[:status].value == "published"}>
{gettext("Published (active)")}
</option>
<option value="draft" selected={f[:status].value == "draft"}>
{gettext("Draft (not visible)")}
</option>
<option value="archived" selected={f[:status].value == "archived"}>
{gettext("Archived (hidden)")}
</option>
</select>
<.label class="label">
<span class="label-text-alt">
{gettext("Only published can be used")}
</span>
</.label>
</div>
<%!-- Sort Mode --%>
<div>
<.label for="entity_sort_mode">{gettext("Record Ordering")}</.label>
<select
id="entity_sort_mode"
name="entities[sort_mode]"
class="select select-bordered w-full"
disabled={@readonly?}
>
<option
value="auto"
selected={@sort_mode == "auto"}
>
{gettext("Automatic (by creation date)")}
</option>
<option
value="manual"
selected={@sort_mode == "manual"}
>
{gettext("Manual (custom order)")}
</option>
</select>
<%!-- TODO: uncomment when table drag-and-drop is ready --%>
<%!-- <.label class="label">
<span class="label-text-alt">
{gettext("Manual mode enables drag-and-drop reordering of records")}
</span>
</.label> --%>
</div>
</div>
</div>
</div>
<%!-- Fields Section --%>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<h2 class="card-title text-2xl">
<.icon name="hero-list-bullet" class="w-6 h-6" />
{ngettext(
"%{count} Field Definition",
"%{count} Field Definitions",
length(@fields),
count: length(@fields)
)}
</h2>
<button
type="button"
class="btn btn-primary"
phx-click="add_field"
disabled={@readonly?}
>
<.icon name="hero-plus" class="w-4 h-4 mr-2" /> {gettext("Add Field")}
</button>
</div>
<%= if Enum.empty?(@fields) do %>
<%!-- Empty Fields State --%>
<div class="text-center py-8 border-2 border-dashed border-base-300 rounded-lg">
<div class="text-4xl mb-4 opacity-50">📝</div>
<h3 class="text-lg font-semibold text-base-content/60 mb-2">
{gettext("No Fields Yet")}
</h3>
<p class="text-base-content/50 mb-4">
{gettext("Add fields to define what data this entity can store")}
</p>
<button
type="button"
class="btn btn-primary"
phx-click="add_field"
disabled={@readonly?}
>
<.icon name="hero-plus" class="w-4 h-4 mr-2" /> {gettext("Add Your First Field")}
</button>
</div>
<% else %>
<%!-- Fields List --%>
<div class="space-y-4">
<%= for {field, index} <- Enum.with_index(@fields) do %>
<div class="card bg-base-200 border border-base-300">
<div class="card-body p-4">
<div class="flex items-center justify-between">
<%!-- Move buttons (stacked vertically) at the beginning --%>
<div class="flex flex-col -space-y-1 mr-3">
<button
type="button"
class={"btn btn-ghost btn-xs px-1.5 #{if index == 0, do: "invisible", else: ""}"}
phx-click="move_field_up"
phx-value-index={index}
title={gettext("Move up")}
disabled={index == 0 or @readonly?}
>
<.icon name="hero-chevron-up" class="w-3 h-3" />
</button>
<button
type="button"
class={"btn btn-ghost btn-xs px-1.5 #{if index >= length(@fields) - 1, do: "invisible", else: ""}"}
phx-click="move_field_down"
phx-value-index={index}
title={gettext("Move down")}
disabled={index >= length(@fields) - 1 or @readonly?}
>
<.icon name="hero-chevron-down" class="w-3 h-3" />
</button>
</div>
<div class="flex items-center justify-between flex-1">
<%!-- Field Icon & Info --%>
<div class="flex items-center space-x-2">
<.icon
name={field_type_icon(field["type"])}
class="w-5 h-5 text-primary"
/>
<div>
<div class="font-medium">{field["label"]}</div>
<div class="text-sm text-base-content/60">
{field["key"]} · {field_type_label(field["type"])}
{if field["required"], do: " · #{gettext("Required")}"}
</div>
</div>
</div>
<%!-- Field Actions --%>
<div class="flex items-center space-x-2">
<%= if @delete_confirm_index == index do %>
<%!-- Delete confirmation buttons --%>
<button
type="button"
class="btn btn-error btn-sm"
phx-click="delete_field"
phx-value-index={index}
disabled={@readonly?}
>
{gettext("Confirm?")}
</button>
<button
type="button"
class="btn btn-outline btn-sm"
phx-click="cancel_delete_field"
disabled={@readonly?}
>
{gettext("Cancel")}
</button>
<% else %>
<%!-- Edit button --%>
<button
type="button"
class="btn btn-primary btn-sm"
phx-click="edit_field"
phx-value-index={index}
disabled={@readonly?}
>
<.icon name="hero-pencil" class="w-4 h-4" />
</button>
<%!-- Delete button --%>
<button
type="button"
class="btn btn-error btn-sm"
phx-click="confirm_delete_field"
phx-value-index={index}
title={gettext("Delete field")}
disabled={@readonly?}
>
<.icon name="hero-trash" class="w-4 h-4" />
</button>
<% end %>
</div>
</div>
</div>
<%!-- Field Options Preview --%>
<%= if requires_options?(field["type"]) && field["options"] do %>
<div class="mt-2 text-sm text-base-content/60">
<span class="font-medium">{gettext("Options")}:</span>
{Enum.join(field["options"], ", ")}
</div>
<% end %>
</div>
</div>
<% end %>
</div>
<% end %>
</div>
</div>
<%!-- Public Form Configuration Section --%>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<div>
<h2 class="card-title text-2xl">
<.icon name="hero-globe-alt" class="w-6 h-6" />
{gettext("Public Form Configuration")}
</h2>
<p class="text-sm text-base-content/70 mt-1">
{gettext("Enable this entity to be used as an embeddable form on public pages")}
</p>
</div>
</div>
<%!-- Enable Public Form Toggle --%>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
name="public_form_enabled"
class="toggle toggle-primary"
checked={get_in(@entity.settings, ["public_form_enabled"]) || false}
phx-click="toggle_public_form"
disabled={@readonly?}
/>
<div>
<span class="label-text font-semibold">
{gettext("Enable Public Form")}
</span>
<p class="text-xs text-base-content/60 mt-1">
{gettext("Allow this entity to be embedded as a form on public pages")}
</p>
</div>
</label>
</div>
<%= if get_in(@entity.settings, ["public_form_enabled"]) do %>
<div class="divider"></div>
<%!-- Form Configuration --%>
<div class="space-y-4">
<%!-- Form Title --%>
<div>
<.label>{gettext("Form Title")}</.label>
<input
type="text"
name="public_form_title"
value={get_in(@entity.settings, ["public_form_title"]) || @entity.display_name}
placeholder={@entity.display_name}
class="input input-bordered w-full"
phx-blur="update_public_form_setting"
phx-debounce="500"
disabled={@readonly?}
/>
</div>
<%!-- Form Description --%>
<div>
<.label>{gettext("Form Description (Optional)")}</.label>
<textarea
name="public_form_description"
placeholder={gettext("Describe what this form is for...")}
class="textarea textarea-bordered w-full"
rows="2"
phx-blur="update_public_form_setting"
phx-debounce="500"
disabled={@readonly?}
>{get_in(@entity.settings, ["public_form_description"]) || ""}</textarea>
</div>
<%!-- Submit Button Text --%>
<div>
<.label>{gettext("Submit Button Text")}</.label>
<input
type="text"
name="public_form_submit_text"
value={
get_in(@entity.settings, ["public_form_submit_text"]) || gettext("Submit")
}
placeholder={gettext("Submit")}
class="input input-bordered w-full"
phx-blur="update_public_form_setting"
phx-debounce="500"
disabled={@readonly?}
/>
</div>
<%!-- Success Message --%>
<div>
<.label>{gettext("Success Message")}</.label>
<textarea
name="public_form_success_message"
placeholder={gettext("Thank you for your submission!")}
class="textarea textarea-bordered w-full"
rows="2"
phx-blur="update_public_form_setting"
phx-debounce="500"
disabled={@readonly?}
>{get_in(@entity.settings, ["public_form_success_message"]) || gettext("Thank you for your submission!")}</textarea>
</div>
<%!-- Field Selection --%>
<%= if not Enum.empty?(@fields) do %>
<div>
<.label>{gettext("Form Fields")}</.label>
<p class="text-sm text-base-content/60 mb-3">
{gettext(
"Select which fields to include in the public form. Fields not selected will only be visible in the admin data viewer."
)}
</p>
<div class="space-y-2 max-h-64 overflow-y-auto border border-base-300 rounded-lg p-3">
<%= for field <- @fields do %>
<label class="flex items-center gap-3 p-2 hover:bg-base-200 rounded cursor-pointer">
<input
type="checkbox"
name="public_form_field"
value={field["key"]}
checked={
field["key"] in (get_in(@entity.settings, ["public_form_fields"]) ||
[])
}
phx-click="toggle_public_form_field"
phx-value-field={field["key"]}
class="checkbox checkbox-primary"
disabled={@readonly?}
/>
<div class="flex items-center gap-2 flex-1">
<.icon
name={field_type_icon(field["type"])}
class="w-4 h-4 text-primary"
/>
<div>
<span class="font-medium">{field["label"]}</span>
<span class="text-xs text-base-content/50 ml-2">
({field["key"]})
</span>
</div>
</div>
<span class="badge badge-sm badge-outline">
{field_type_label(field["type"])}
</span>
</label>
<% end %>
</div>
</div>
<% else %>
<div class="alert alert-warning">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<span>
{gettext(
"Add fields to this entity first before configuring the public form."
)}
</span>
</div>
<% end %>
<%!-- Usage Example --%>
<%= if not Enum.empty?(get_in(@entity.settings, ["public_form_fields"]) || []) do %>
<div class="alert alert-info">
<.icon name="hero-information-circle" class="w-5 h-5" />
<div class="flex-1">
<p class="font-semibold mb-1">{gettext("Embed this form:")}</p>
<code class="text-xs bg-base-300 px-2 py-1 rounded">
&lt;EntityForm entity_slug="{@entity.name}" /&gt;
</code>
</div>
</div>
<% end %>
<div class="divider"></div>
<%!-- Security Section --%>
<div>
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
<.icon name="hero-shield-check" class="w-5 h-5" />
{gettext("Security")}
</h3>
<div class="space-y-4">
<%!-- Collect Metadata Toggle --%>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
name="public_form_collect_metadata"
class="toggle toggle-primary"
checked={
get_in(@entity.settings, ["public_form_collect_metadata"]) != false
}
phx-click="toggle_security_setting"
phx-value-setting="public_form_collect_metadata"
disabled={@readonly?}
/>
<div>
<span class="label-text font-medium">
{gettext("Collect Submission Metadata")}
</span>
<p class="text-xs text-base-content/60 mt-1">
{gettext(
"Record IP address, browser, device info, and referrer for each submission"
)}
</p>
</div>
</label>
</div>
<%!-- Debug Mode Toggle --%>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
name="public_form_debug_mode"
class="toggle toggle-warning"
checked={get_in(@entity.settings, ["public_form_debug_mode"]) || false}
phx-click="toggle_security_setting"
phx-value-setting="public_form_debug_mode"
disabled={@readonly?}
/>
<div>
<span class="label-text font-medium">
{gettext("Debug Mode")}
</span>
<p class="text-xs text-base-content/60 mt-1">
{gettext(
"Show detailed error messages when security checks fail (for troubleshooting only)"
)}
</p>
</div>
</label>
</div>
<%= if get_in(@entity.settings, ["public_form_debug_mode"]) do %>
<div class="alert alert-warning">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<span class="text-sm">
{gettext(
"Debug mode is enabled. Detailed security errors will be shown to users. Disable this in production."
)}
</span>
</div>
<% end %>
<%!-- Honeypot Protection --%>
<div class="border border-base-300 rounded-lg p-4">
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
name="public_form_honeypot"
class="toggle toggle-primary"
checked={get_in(@entity.settings, ["public_form_honeypot"]) || false}
phx-click="toggle_security_setting"
phx-value-setting="public_form_honeypot"
disabled={@readonly?}
/>
<div>
<span class="label-text font-medium">
{gettext("Honeypot Protection")}
</span>
<p class="text-xs text-base-content/60 mt-1">
{gettext("Add a hidden field that bots typically fill out")}
</p>
</div>
</label>
</div>
<%= if get_in(@entity.settings, ["public_form_honeypot"]) do %>
<div class="mt-3 pl-14">
<.label class="text-sm">{gettext("When triggered:")}</.label>
<select
name="public_form_honeypot_action"
class="select select-bordered select-sm w-full max-w-xs mt-1"
phx-change="update_security_action"
phx-value-setting="public_form_honeypot_action"
disabled={@readonly?}
>
<option
value="reject_silent"
selected={
get_in(@entity.settings, ["public_form_honeypot_action"]) ==
"reject_silent" ||
is_nil(get_in(@entity.settings, ["public_form_honeypot_action"]))
}
>
{gettext("Reject silently (fake success)")}
</option>
<option
value="reject_error"
selected={
get_in(@entity.settings, ["public_form_honeypot_action"]) ==
"reject_error"
}
>
{gettext("Reject with error message")}
</option>
<option
value="save_suspicious"
selected={
get_in(@entity.settings, ["public_form_honeypot_action"]) ==
"save_suspicious"
}
>
{gettext("Save but mark as suspicious")}
</option>
<option
value="save_log"
selected={
get_in(@entity.settings, ["public_form_honeypot_action"]) ==
"save_log"
}
>
{gettext("Save and log warning")}
</option>
</select>
</div>
<% end %>
</div>
<%!-- Time-based Validation --%>
<div class="border border-base-300 rounded-lg p-4">
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
name="public_form_time_check"
class="toggle toggle-primary"
checked={get_in(@entity.settings, ["public_form_time_check"]) || false}
phx-click="toggle_security_setting"
phx-value-setting="public_form_time_check"
disabled={@readonly?}
/>
<div>
<span class="label-text font-medium">
{gettext("Time-based Validation")}
</span>
<p class="text-xs text-base-content/60 mt-1">
{gettext(
"Check if submission happens too quickly (less than 3 seconds)"
)}
</p>
</div>
</label>
</div>
<%= if get_in(@entity.settings, ["public_form_time_check"]) do %>
<div class="mt-3 pl-14">
<.label class="text-sm">{gettext("When triggered:")}</.label>
<select
name="public_form_time_check_action"
class="select select-bordered select-sm w-full max-w-xs mt-1"
phx-change="update_security_action"
phx-value-setting="public_form_time_check_action"
disabled={@readonly?}
>
<option
value="reject_error"
selected={
get_in(@entity.settings, ["public_form_time_check_action"]) ==
"reject_error" ||
is_nil(
get_in(@entity.settings, ["public_form_time_check_action"])
)
}
>
{gettext("Reject with error message")}
</option>
<option
value="reject_silent"
selected={
get_in(@entity.settings, ["public_form_time_check_action"]) ==
"reject_silent"
}
>
{gettext("Reject silently (fake success)")}
</option>
<option
value="save_suspicious"
selected={
get_in(@entity.settings, ["public_form_time_check_action"]) ==
"save_suspicious"
}
>
{gettext("Save but mark as suspicious")}
</option>
<option
value="save_log"
selected={
get_in(@entity.settings, ["public_form_time_check_action"]) ==
"save_log"
}
>
{gettext("Save and log warning")}
</option>
</select>
</div>
<% end %>
</div>
<%!-- Rate Limiting --%>
<div class="border border-base-300 rounded-lg p-4">
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
name="public_form_rate_limit"
class="toggle toggle-primary"
checked={get_in(@entity.settings, ["public_form_rate_limit"]) || false}
phx-click="toggle_security_setting"
phx-value-setting="public_form_rate_limit"
disabled={@readonly?}
/>
<div>
<span class="label-text font-medium">
{gettext("Rate Limiting")}
</span>
<p class="text-xs text-base-content/60 mt-1">
{gettext("Limit submissions to 5 per minute per IP address")}
</p>
</div>
</label>
</div>
<%= if get_in(@entity.settings, ["public_form_rate_limit"]) do %>
<div class="mt-3 pl-14">
<.label class="text-sm">{gettext("When triggered:")}</.label>
<select
name="public_form_rate_limit_action"
class="select select-bordered select-sm w-full max-w-xs mt-1"
phx-change="update_security_action"
phx-value-setting="public_form_rate_limit_action"
disabled={@readonly?}
>
<option
value="reject_error"
selected={
get_in(@entity.settings, ["public_form_rate_limit_action"]) ==
"reject_error" ||
is_nil(
get_in(@entity.settings, ["public_form_rate_limit_action"])
)
}
>
{gettext("Reject with error message")}
</option>
<option
value="reject_silent"
selected={
get_in(@entity.settings, ["public_form_rate_limit_action"]) ==
"reject_silent"
}
>
{gettext("Reject silently (fake success)")}
</option>
<option
value="save_suspicious"
selected={
get_in(@entity.settings, ["public_form_rate_limit_action"]) ==
"save_suspicious"
}
>
{gettext("Save but mark as suspicious")}
</option>
<option
value="save_log"
selected={
get_in(@entity.settings, ["public_form_rate_limit_action"]) ==
"save_log"
}
>
{gettext("Save and log warning")}
</option>
</select>
</div>
<% end %>
</div>
</div>
</div>
<%!-- Form Statistics --%>
<% stats = get_in(@entity.settings, ["public_form_stats"]) || %{} %>
<div class="divider"></div>
<div>
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
<.icon name="hero-chart-bar" class="w-5 h-5" />
{gettext("Form Statistics")}
</h3>
<div class="stats stats-vertical lg:stats-horizontal shadow w-full bg-base-200">
<div class="stat">
<div class="stat-figure text-primary">
<.icon name="hero-document-text" class="w-8 h-8" />
</div>
<div class="stat-title">{gettext("Total Submissions")}</div>
<div class="stat-value text-primary">
{stats["total_submissions"] || 0}
</div>
</div>
<div class="stat">
<div class="stat-figure text-success">
<.icon name="hero-check-circle" class="w-8 h-8" />
</div>
<div class="stat-title">{gettext("Successful")}</div>
<div class="stat-value text-success">
{stats["successful_submissions"] || 0}
</div>
</div>
<div class="stat">
<div class="stat-figure text-error">
<.icon name="hero-x-circle" class="w-8 h-8" />
</div>
<div class="stat-title">{gettext("Rejected")}</div>
<div class="stat-value text-error">
{stats["rejected_submissions"] || 0}
</div>
</div>
</div>
<%!-- Security trigger breakdown --%>
<%= if stats["honeypot_triggers"] || stats["too_fast_triggers"] || stats["rate_limited_triggers"] do %>
<div class="mt-4">
<h4 class="text-sm font-medium text-base-content/70 mb-2">
{gettext("Security Triggers")}
</h4>
<div class="flex flex-wrap gap-2">
<%= if stats["honeypot_triggers"] do %>
<div class="badge badge-warning gap-1">
<.icon name="hero-bug-ant" class="w-3 h-3" />
{gettext("Honeypot")}: {stats["honeypot_triggers"]}
</div>
<% end %>
<%= if stats["too_fast_triggers"] do %>
<div class="badge badge-warning gap-1">
<.icon name="hero-bolt" class="w-3 h-3" />
{gettext("Too Fast")}: {stats["too_fast_triggers"]}
</div>
<% end %>
<%= if stats["rate_limited_triggers"] do %>
<div class="badge badge-warning gap-1">
<.icon name="hero-clock" class="w-3 h-3" />
{gettext("Rate Limited")}: {stats["rate_limited_triggers"]}
</div>
<% end %>
</div>
</div>
<% end %>
<%!-- Last submission time --%>
<%= if stats["last_submission_at"] do %>
<div class="mt-4 text-sm text-base-content/60">
{gettext("Last submission")}: {format_stats_datetime(
stats["last_submission_at"]
)}
</div>
<% end %>
<%!-- Reset Stats Button (only show if there are stats) --%>
<%= if stats["total_submissions"] do %>
<div class="mt-4">
<button
type="button"
class="btn btn-ghost btn-sm"
phx-click="reset_form_stats"
disabled={@readonly?}
>
<.icon name="hero-arrow-path" class="w-4 h-4" />
{gettext("Reset Statistics")}
</button>
</div>
<% end %>
</div>
</div>
<% end %>
</div>
</div>
<%!-- Backup Settings Section --%>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<div>
<h2 class="card-title text-2xl">
<.icon name="hero-arrow-down-tray" class="w-6 h-6" />
{gettext("Backup Settings")}
</h2>
<p class="text-sm text-base-content/70 mt-1">
{gettext("Configure automatic backup to file for this entity")}
</p>
</div>
<%= if @entity.uuid do %>
<button
type="button"
class="btn btn-primary btn-sm"
phx-click="export_entity_now"
disabled={@readonly?}
>
<.icon name="hero-arrow-down-tray" class="w-4 h-4" />
{gettext("Export Now")}
</button>
<% end %>
</div>
<%= if @entity.uuid do %>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<%!-- Definition Sync Toggle --%>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
class="toggle toggle-primary"
checked={PhoenixKit.Modules.Entities.mirror_definitions_enabled?(@entity)}
phx-click="toggle_backup_definitions"
disabled={@readonly?}
/>
<div>
<span class="label-text font-semibold">
{gettext("Sync Definition to File")}
</span>
<p class="text-xs text-base-content/60 mt-1">
{gettext("Automatically export entity schema when changes are saved")}
</p>
</div>
</label>
</div>
<%!-- Records Sync Toggle --%>
<div class={"form-control #{unless PhoenixKit.Modules.Entities.mirror_definitions_enabled?(@entity), do: "opacity-50"}"}>
<label class="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
class="toggle toggle-primary"
checked={PhoenixKit.Modules.Entities.mirror_data_enabled?(@entity)}
phx-click="toggle_backup_data"
disabled={
@readonly? or
not PhoenixKit.Modules.Entities.mirror_definitions_enabled?(@entity)
}
/>
<div>
<span class="label-text font-semibold">
{gettext("Sync Records to File")}
</span>
<p class="text-xs text-base-content/60 mt-1">
{gettext(
"Automatically export data records when they are created or updated"
)}
</p>
</div>
</label>
</div>
</div>
<%!-- File Info --%>
<div class="mt-4 p-4 bg-base-200 rounded-lg">
<div class="flex items-center gap-2 text-sm">
<.icon name="hero-folder" class="w-4 h-4 text-base-content/70" />
<span class="text-base-content/70">{gettext("Export path")}:</span>
<code class="text-xs bg-base-300 px-2 py-1 rounded break-all">
{@mirror_path}/{@entity.name}.json
</code>
</div>
</div>
<% else %>
<%!-- New entity - show message --%>
<div class="alert alert-info">
<.icon name="hero-information-circle" class="w-5 h-5" />
<span>
{gettext("Save the entity first to configure backup settings.")}
</span>
</div>
<% end %>
</div>
</div>
<%!-- Form Actions --%>
<div class="flex justify-between items-center">
<div class="flex gap-2">
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/entities")}
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={!@changeset.valid? or @readonly?}
>
{if @entity.uuid, do: gettext("Update Entity"), else: gettext("Create Entity")}
</button>
</div>
</.form>
<%!-- Field Form Modal --%>
<%= if @show_field_form do %>
<div class="modal modal-open" phx-click="cancel_field">
<div class="modal-box w-11/12 max-w-2xl" phx-click="stop_propagation">
<h3 class="font-bold text-lg mb-4">
{if @editing_field_index, do: gettext("Edit Field"), else: gettext("Add New Field")}
</h3>
<%!-- Field Error Alert --%>
<%= if @field_error do %>
<div class="alert alert-error mb-4">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<span>{@field_error}</span>
</div>
<% end %>
<.form
for={%{}}
phx-change="update_field_form"
phx-submit="save_field"
class="space-y-4"
>
<%!-- Field Type --%>
<div>
<.label>{gettext("Field Type")} *</.label>
<select
name="field[type]"
class="select select-bordered w-full"
phx-debounce="300"
value={@field_form["type"]}
>
<%= for {category_key, _label} <- PhoenixKit.Modules.Entities.FieldTypes.category_list() do %>
<optgroup label={field_category_label(category_key)}>
<%= for type <- PhoenixKit.Modules.Entities.FieldTypes.by_category(category_key) do %>
<option value={type.name} selected={@field_form["type"] == type.name}>
{field_type_label(type.name)}
</option>
<% end %>
</optgroup>
<% end %>
</select>
</div>
<%!-- Field Label and Key --%>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<.label>{gettext("Field Label")} *</.label>
<input
type="text"
name="field[label]"
class="input input-bordered w-full"
placeholder={gettext("Field Name")}
value={@field_form["label"]}
phx-debounce="300"
required
/>
<.label class="label">
<span class="label-text-alt">{gettext("Display name for users")}</span>
</.label>
</div>
<div>
<.label>
{gettext("Slug")} *
<button
type="button"
class="btn btn-ghost btn-xs ml-2"
phx-click="generate_field_key"
title={gettext("Generate from Field Label")}
>
<.icon name="hero-arrow-path" class="w-3 h-3" /> {gettext("Generate")}
</button>
</.label>
<input
type="text"
name="field[key]"
class="input input-bordered w-full"
placeholder={gettext("field_name")}
value={@field_form["key"]}
phx-debounce="300"
required
/>
<.label class="label">
<span class="label-text-alt">{gettext("snake_case identifier")}</span>
</.label>
</div>
</div>
<%!-- Required and Default Value --%>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<.label class="label cursor-pointer">
<span class="label-text">{gettext("Required Field")}</span>
<input
type="checkbox"
name="field[required]"
class="toggle toggle-primary"
value="true"
checked={@field_form["required"]}
/>
</.label>
</div>
<div>
<.label>{gettext("Default Value (Optional)")}</.label>
<input
type="text"
name="field[default]"
class="input input-bordered w-full"
phx-debounce="300"
value={@field_form["default"]}
/>
</div>
</div>
<%!-- File Upload Configuration (only for file type) --%>
<%= if @field_form["type"] == "file" do %>
<div class="space-y-4 p-4 bg-base-200 rounded-lg">
<div class="text-sm font-semibold text-base-content/70">
{gettext("File Upload Settings")}
</div>
<%!-- Max Files --%>
<div class="form-control">
<.label>{gettext("Maximum Files")}</.label>
<input
type="number"
name="field[max_entries]"
value={@field_form["max_entries"] || 5}
min="1"
max="20"
class="input input-bordered"
placeholder="5"
phx-debounce="300"
/>
<.label class="label">
<span class="label-text-alt">
{gettext("Max number of files users can upload (1-20)")}
</span>
</.label>
</div>
<%!-- Max File Size --%>
<div class="form-control">
<.label>{gettext("Max File Size (MB)")}</.label>
<input
type="number"
name="field[max_file_size_mb]"
value={bytes_to_mb(@field_form["max_file_size"] || 15_728_640)}
min="1"
max="100"
step="0.1"
class="input input-bordered"
placeholder="15"
phx-debounce="300"
/>
<.label class="label">
<span class="label-text-alt">
{gettext("Maximum size per file in megabytes (1-100 MB)")}
</span>
</.label>
</div>
<%!-- Accepted File Types --%>
<div class="form-control">
<.label>{gettext("Accepted File Types")}</.label>
<textarea
name="field[accept]"
rows="3"
class="textarea textarea-bordered"
placeholder=".pdf, .jpg, .png"
phx-debounce="300"
><%= format_accept_list(@field_form["accept"]) %></textarea>
<.label class="label">
<span class="label-text-alt">
{gettext(
"Comma-separated list of extensions (e.g., .pdf, .jpg, .png, .step, .dwg)"
)}
</span>
</.label>
</div>
</div>
<% end %>
<%!-- Options (for choice fields) --%>
<%= if requires_options?(@field_form["type"]) do %>
<div>
<div class="flex items-center justify-between mb-2">
<.label>{gettext("Options")} *</.label>
<button
type="button"
class="btn btn-sm btn-outline"
phx-click="add_option"
>
<.icon name="hero-plus" class="w-4 h-4 mr-1" /> {gettext("Add Option")}
</button>
</div>
<div class="space-y-2">
<%= for {option, option_index} <- Enum.with_index(@field_form["options"] || []) do %>
<div class="flex gap-2">
<input
type="text"
name={"option[#{option_index}]"}
value={option}
class="input input-bordered flex-1"
phx-change="update_option"
phx-value-index={option_index}
phx-debounce="300"
placeholder={gettext("Option value")}
/>
<button
type="button"
class="btn btn-error btn-sm"
phx-click="remove_option"
phx-value-index={option_index}
>
<.icon name="hero-trash" class="w-4 h-4" />
</button>
</div>
<% end %>
</div>
<%= if Enum.empty?(@field_form["options"] || []) do %>
<div class="text-center text-base-content/60 py-4">
{gettext("No options added yet. Click \"Add Option\" to get started.")}
</div>
<% end %>
</div>
<% end %>
<%!-- Modal Actions --%>
<div class="modal-action">
<button
type="button"
class="btn btn-outline"
phx-click="cancel_field"
>
{gettext("Cancel")}
</button>
<button
type="submit"
class="btn btn-primary"
>
{if @editing_field_index, do: gettext("Update Field"), else: gettext("Add Field")}
</button>
</div>
</.form>
</div>
</div>
<% end %>
<%!-- Icon Picker Modal --%>
<%= if @show_icon_picker do %>
<div
class="fixed inset-0 bg-base-content/50 z-50 flex items-center justify-center p-4"
phx-click="close_icon_picker"
>
<div
class="bg-base-100 rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] flex flex-col"
phx-click="stop_propagation"
>
<%!-- Modal Header --%>
<div class="flex items-center justify-between p-6 border-b border-base-300">
<h2 class="text-2xl font-bold flex items-center gap-2">
<.icon name="hero-squares-2x2" class="w-6 h-6" /> {gettext("Select an Icon")}
</h2>
<button
type="button"
phx-click="close_icon_picker"
class="btn btn-ghost btn-sm btn-circle"
>
<.icon name="hero-x-mark" class="w-5 h-5" />
</button>
</div>
<%!-- Search Bar --%>
<div class="p-4 border-b border-base-300">
<.form for={%{}} phx-change="search_icons" phx-submit="search_icons">
<div class="join w-full">
<input
type="text"
name="search"
value={@icon_search}
placeholder={gettext("Search icons...")}
class="input input-bordered join-item flex-1"
phx-debounce="300"
/>
<button type="submit" class="btn btn-primary join-item">
<.icon name="hero-magnifying-glass" class="w-4 h-4" />
</button>
</div>
</.form>
</div>
<%!-- Category Tabs --%>
<div class="px-4 py-2 border-b border-base-300 overflow-x-auto">
<div class="tabs tabs-boxed inline-flex">
<%= for category <- @icon_categories do %>
<button
type="button"
phx-click="filter_by_category"
phx-value-category={category}
class={[
"tab",
@selected_category == category && "tab-active"
]}
>
{icon_category_label(category)}
</button>
<% end %>
</div>
</div>
<%!-- Icon Grid --%>
<div class="flex-1 overflow-y-auto p-6">
<%= if Enum.empty?(@available_icons) do %>
<div class="text-center py-12">
<div class="text-4xl mb-4 opacity-50">🔍</div>
<p class="text-base-content/70">
{gettext("No icons found matching your search")}
</p>
</div>
<% else %>
<div class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-3">
<%= for {icon_name, display_name} <- @available_icons do %>
<button
type="button"
phx-click="select_icon"
phx-value-icon={icon_name}
class="btn btn-outline flex flex-col h-auto py-3 gap-1 hover:btn-primary min-h-0"
title={display_name}
>
<.icon name={icon_name} class="w-6 h-6 flex-shrink-0" />
<span class="text-xs leading-tight text-center break-words w-full">
{display_name}
</span>
</button>
<% end %>
</div>
<% end %>
</div>
<%!-- Modal Footer --%>
<div class="p-4 border-t border-base-300 bg-base-200">
<div class="flex justify-between items-center text-sm text-base-content/70">
<span>
{ngettext(
"%{count} icon available",
"%{count} icons available",
length(@available_icons),
count: length(@available_icons)
)}
</span>
<button type="button" phx-click="close_icon_picker" class="btn btn-ghost btn-sm">
{gettext("Cancel")}
</button>
</div>
</div>
</div>
</div>
<% end %>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>