rework email settings for true progressive enhancement
All checks were successful
deploy / deploy (push) Successful in 1m19s
All checks were successful
deploy / deploy (push) Successful in 1m19s
Render all adapter field sections in the form with CSS :has(:checked) controlling visibility. Selecting a provider instantly shows its config fields — no JS, no page reload, no server round-trip needed. - Render all 6 adapter configs with data-adapter attribute - CSS :has(:checked) show/hide rules per adapter in admin stylesheet - Namespace field names per adapter (email[brevo][api_key] etc) - Drop 4 transactional-only providers (Resend, Postmark, Mailgun, MailPace) - Remove noscript "Switch provider" button and controller redirect workaround - Remove configured_adapter hidden input tracking - Hide JS-only test email button for no-JS users via noscript style - LiveView progressively enhances with async save and test email Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -84,8 +84,6 @@ defmodule Berrypod.KeyValidation do
|
||||
@known_prefixes [
|
||||
{"SG.", "SendGrid"},
|
||||
{"xkeysib-", "Brevo"},
|
||||
{"re_", "Resend"},
|
||||
{"key-", "Mailgun"},
|
||||
{"mlsn.", "MailerSend"}
|
||||
]
|
||||
|
||||
@@ -98,56 +96,6 @@ defmodule Berrypod.KeyValidation do
|
||||
end
|
||||
end
|
||||
|
||||
# Postmark: UUID format
|
||||
defp validate_email_format(key, "postmark", "api_key") do
|
||||
uuid_pattern = ~r/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
|
||||
cond do
|
||||
Regex.match?(uuid_pattern, key) ->
|
||||
{:ok, key}
|
||||
|
||||
wrong = wrong_provider_hint(key, "postmark") ->
|
||||
{:error, wrong}
|
||||
|
||||
true ->
|
||||
{:error,
|
||||
"Postmark server tokens are UUIDs (like abc12345-abcd-1234-abcd-123456789abc). Make sure you copy the server token, not the account ID"}
|
||||
end
|
||||
end
|
||||
|
||||
# Resend: re_ prefix
|
||||
defp validate_email_format(key, "resend", "api_key") do
|
||||
cond do
|
||||
String.starts_with?(key, "re_") ->
|
||||
{:ok, key}
|
||||
|
||||
wrong = wrong_provider_hint(key, "resend") ->
|
||||
{:error, wrong}
|
||||
|
||||
true ->
|
||||
{:error,
|
||||
"Resend API keys start with re_ — find yours in your Resend dashboard under API Keys"}
|
||||
end
|
||||
end
|
||||
|
||||
# Mailgun: key- prefix (classic) or long RBAC keys
|
||||
defp validate_email_format(key, "mailgun", "api_key") do
|
||||
cond do
|
||||
String.starts_with?(key, "key-") ->
|
||||
{:ok, key}
|
||||
|
||||
String.length(key) >= 20 ->
|
||||
{:ok, key}
|
||||
|
||||
wrong = wrong_provider_hint(key, "mailgun") ->
|
||||
{:error, wrong}
|
||||
|
||||
true ->
|
||||
{:error,
|
||||
"Mailgun API keys usually start with key- — find yours in Settings → API Security"}
|
||||
end
|
||||
end
|
||||
|
||||
# Brevo: xkeysib- prefix
|
||||
defp validate_email_format(key, "brevo", "api_key") do
|
||||
cond do
|
||||
@@ -210,21 +158,6 @@ defmodule Berrypod.KeyValidation do
|
||||
end
|
||||
end
|
||||
|
||||
# MailPace: server token, no known prefix
|
||||
defp validate_email_format(key, "mailpace", "api_key") do
|
||||
cond do
|
||||
String.length(key) >= 10 ->
|
||||
{:ok, key}
|
||||
|
||||
wrong = wrong_provider_hint(key, "mailpace") ->
|
||||
{:error, wrong}
|
||||
|
||||
true ->
|
||||
{:error,
|
||||
"This looks too short — find your server API token under your domain settings in MailPace"}
|
||||
end
|
||||
end
|
||||
|
||||
# Non-api_key fields (domain, relay, base_url, etc.), basic checks
|
||||
defp validate_email_format(key, _adapter_key, _field_key) do
|
||||
if String.length(key) < 3 do
|
||||
|
||||
@@ -69,64 +69,6 @@ defmodule Berrypod.Mailer.Adapters do
|
||||
%Field{key: "api_key", label: "API key", type: :secret, required: true}
|
||||
]
|
||||
},
|
||||
# ── Transactional only ──
|
||||
%Adapter{
|
||||
key: "resend",
|
||||
name: "Resend",
|
||||
module: Swoosh.Adapters.Resend,
|
||||
description: "Developer-friendly API, simple setup.",
|
||||
tags: ["Transactional", "US"],
|
||||
category: :transactional,
|
||||
free_tier: "3,000 emails/month free",
|
||||
setup_hint: "Paste one API key",
|
||||
url: "https://resend.com",
|
||||
fields: [
|
||||
%Field{key: "api_key", label: "API key", type: :secret, required: true}
|
||||
]
|
||||
},
|
||||
%Adapter{
|
||||
key: "postmark",
|
||||
name: "Postmark",
|
||||
module: Swoosh.Adapters.Postmark,
|
||||
description: "Excellent deliverability tracking.",
|
||||
tags: ["Transactional", "US"],
|
||||
category: :transactional,
|
||||
free_tier: "100 emails/month free",
|
||||
setup_hint: "Paste one API key",
|
||||
url: "https://postmarkapp.com",
|
||||
fields: [
|
||||
%Field{key: "api_key", label: "API key", type: :secret, required: true}
|
||||
]
|
||||
},
|
||||
%Adapter{
|
||||
key: "mailgun",
|
||||
name: "Mailgun",
|
||||
module: Swoosh.Adapters.Mailgun,
|
||||
description: "EU region option available.",
|
||||
tags: ["Transactional", "EU option", "Sweden"],
|
||||
category: :transactional,
|
||||
free_tier: "100 emails/day trial",
|
||||
setup_hint: "API key + domain name",
|
||||
url: "https://www.mailgun.com",
|
||||
fields: [
|
||||
%Field{key: "api_key", label: "API key", type: :secret, required: true},
|
||||
%Field{key: "domain", label: "Domain", type: :string, required: true}
|
||||
]
|
||||
},
|
||||
%Adapter{
|
||||
key: "mailpace",
|
||||
name: "MailPace",
|
||||
module: Swoosh.Adapters.MailPace,
|
||||
description: "Privacy-focused, simple API.",
|
||||
tags: ["Transactional", "UK"],
|
||||
category: :transactional,
|
||||
free_tier: "3,000 emails/month free",
|
||||
setup_hint: "Paste one API key",
|
||||
url: "https://mailpace.com",
|
||||
fields: [
|
||||
%Field{key: "api_key", label: "API key", type: :secret, required: true}
|
||||
]
|
||||
},
|
||||
# ── Advanced ──
|
||||
%Adapter{
|
||||
key: "smtp",
|
||||
|
||||
@@ -10,27 +10,22 @@ defmodule BerrypodWeb.EmailSettingsController do
|
||||
alias Berrypod.Mailer
|
||||
|
||||
def update(conn, %{"email" => params}) do
|
||||
selected = params["adapter"]
|
||||
configured = params["configured_adapter"]
|
||||
adapter_key = params["adapter"]
|
||||
# Fields are namespaced: email[brevo][api_key] → params["brevo"]["api_key"]
|
||||
adapter_params = params[adapter_key] || %{}
|
||||
|
||||
if selected != configured do
|
||||
# User changed adapter radio but config fields are for the old adapter.
|
||||
# Redirect to show the new adapter's config fields.
|
||||
redirect(conn, to: ~p"/admin/settings/email?adapter=#{selected}")
|
||||
else
|
||||
case Mailer.save_config(selected, params, conn.assigns.current_scope.user.email) do
|
||||
{:ok, _adapter_info} ->
|
||||
conn
|
||||
|> put_flash(:info, "Settings saved — send a test email to check it works")
|
||||
|> redirect(to: ~p"/admin/settings/email")
|
||||
case Mailer.save_config(adapter_key, adapter_params, conn.assigns.current_scope.user.email) do
|
||||
{:ok, _adapter_info} ->
|
||||
conn
|
||||
|> put_flash(:info, "Settings saved — send a test email to check it works")
|
||||
|> redirect(to: ~p"/admin/settings/email")
|
||||
|
||||
{:error, field_errors} when is_map(field_errors) ->
|
||||
message = field_errors |> Map.values() |> Enum.join(". ")
|
||||
{:error, field_errors} when is_map(field_errors) ->
|
||||
message = field_errors |> Map.values() |> Enum.join(". ")
|
||||
|
||||
conn
|
||||
|> put_flash(:error, message)
|
||||
|> redirect(to: ~p"/admin/settings/email?adapter=#{selected}")
|
||||
end
|
||||
conn
|
||||
|> put_flash(:error, message)
|
||||
|> redirect(to: ~p"/admin/settings/email")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -8,21 +8,23 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
env_locked = Mailer.env_var_configured?()
|
||||
{current_adapter, current_values} = Mailer.current_config()
|
||||
{current_adapter, _current_values} = Mailer.current_config()
|
||||
saved_adapter = Settings.get_setting("email_adapter")
|
||||
|
||||
adapter_key = current_adapter || saved_adapter
|
||||
grouped = Adapters.grouped()
|
||||
all_adapters = Adapters.all()
|
||||
all_values = load_all_adapter_values()
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Email settings")
|
||||
|> assign(:env_locked, env_locked)
|
||||
|> assign(:adapter_key, adapter_key)
|
||||
|> assign(:current_values, current_values)
|
||||
|> assign(:all_values, all_values)
|
||||
|> assign(:all_email_adapters, grouped[:all_email] || [])
|
||||
|> assign(:transactional_adapters, grouped[:transactional] || [])
|
||||
|> assign(:advanced_adapters, grouped[:advanced] || [])
|
||||
|> assign(:all_adapters, all_adapters)
|
||||
|> assign(:email_configured, Mailer.email_configured?())
|
||||
|> assign(:selected_adapter, adapter_key && Adapters.get(adapter_key))
|
||||
|> assign(:sending_test, false)
|
||||
@@ -36,37 +38,14 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _uri, socket) do
|
||||
# Support ?adapter=X for no-JS adapter switching
|
||||
adapter_key = params["adapter"] || socket.assigns.adapter_key
|
||||
|
||||
socket =
|
||||
if adapter_key && adapter_key != socket.assigns.adapter_key do
|
||||
values = load_adapter_values(adapter_key)
|
||||
|
||||
socket
|
||||
|> assign(:adapter_key, adapter_key)
|
||||
|> assign(:selected_adapter, Adapters.get(adapter_key))
|
||||
|> assign(:current_values, values)
|
||||
|> assign(:field_errors, %{})
|
||||
|> assign(:test_result, nil)
|
||||
|> assign(:test_error, nil)
|
||||
else
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, :from_checklist, params["from"] == "checklist")}
|
||||
end
|
||||
|
||||
defp load_adapter_values(nil), do: %{}
|
||||
|
||||
defp load_adapter_values(adapter_key) do
|
||||
case Adapters.get(adapter_key) do
|
||||
nil ->
|
||||
%{}
|
||||
|
||||
adapter_info ->
|
||||
for field <- adapter_info.fields, into: %{} do
|
||||
settings_key = Adapters.settings_key(adapter_key, field.key)
|
||||
defp load_all_adapter_values do
|
||||
for adapter <- Adapters.all(), into: %{} do
|
||||
values =
|
||||
for field <- adapter.fields, into: %{} do
|
||||
settings_key = Adapters.settings_key(adapter.key, field.key)
|
||||
|
||||
value =
|
||||
case field.type do
|
||||
@@ -76,6 +55,8 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
|
||||
{field.key, value}
|
||||
end
|
||||
|
||||
{adapter.key, values}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -84,13 +65,10 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
if key == socket.assigns.adapter_key do
|
||||
{:noreply, socket}
|
||||
else
|
||||
values = load_adapter_values(key)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:adapter_key, key)
|
||||
|> assign(:selected_adapter, Adapters.get(key))
|
||||
|> assign(:current_values, values)
|
||||
|> assign(:field_errors, %{})
|
||||
|> assign(:test_result, nil)
|
||||
|> assign(:test_error, nil)}
|
||||
@@ -102,16 +80,20 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
{:noreply, put_flash(socket, :error, "Email config is controlled by environment variables")}
|
||||
else
|
||||
adapter_key = params["adapter"]
|
||||
# Fields are namespaced: email[brevo][api_key] → params["brevo"]["api_key"]
|
||||
adapter_params = params[adapter_key] || %{}
|
||||
|
||||
case Mailer.save_config(adapter_key, params, socket.assigns.current_scope.user.email) do
|
||||
case Mailer.save_config(
|
||||
adapter_key,
|
||||
adapter_params,
|
||||
socket.assigns.current_scope.user.email
|
||||
) do
|
||||
{:ok, _adapter_info} ->
|
||||
{current_adapter, current_values} = Mailer.current_config()
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:adapter_key, current_adapter)
|
||||
|> assign(:selected_adapter, Adapters.get(current_adapter))
|
||||
|> assign(:current_values, current_values)
|
||||
|> assign(:adapter_key, adapter_key)
|
||||
|> assign(:selected_adapter, Adapters.get(adapter_key))
|
||||
|> assign(:all_values, load_all_adapter_values())
|
||||
|> assign(:email_configured, Mailer.email_configured?())
|
||||
|> assign(:field_errors, %{})
|
||||
|> assign(:test_result, nil)
|
||||
@@ -204,13 +186,6 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
phx-change="form_change"
|
||||
phx-submit="save"
|
||||
>
|
||||
<%!-- Hidden field tracks which adapter's config fields are rendered --%>
|
||||
<input
|
||||
type="hidden"
|
||||
name="email[configured_adapter]"
|
||||
value={@adapter_key || ""}
|
||||
/>
|
||||
|
||||
<%!-- Step 1: Choose a provider --%>
|
||||
<div class="admin-setup-step">
|
||||
<div class="admin-setup-step-header">
|
||||
@@ -221,16 +196,6 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
<fieldset class="card-radio-fieldset" disabled={@env_locked}>
|
||||
<legend class="sr-only">Email provider</legend>
|
||||
|
||||
<h3 class="card-radio-group-heading">Popular providers</h3>
|
||||
<p class="card-radio-group-desc">
|
||||
Newsletters and transactional emails. All have free tiers.
|
||||
<span
|
||||
:if={Enum.any?(@all_email_adapters, & &1.recommended)}
|
||||
class="card-radio-group-hint"
|
||||
>
|
||||
Not sure which? Pick the recommended one.
|
||||
</span>
|
||||
</p>
|
||||
<div class="card-radio-grid">
|
||||
<.provider_card
|
||||
:for={adapter <- @all_email_adapters}
|
||||
@@ -240,20 +205,6 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 class="card-radio-group-heading">Transactional only</h3>
|
||||
<p class="card-radio-group-desc">
|
||||
Order confirmations and shipping updates.
|
||||
You'll need a separate service for newsletters later.
|
||||
</p>
|
||||
<div class="card-radio-grid">
|
||||
<.provider_card
|
||||
:for={adapter <- @transactional_adapters}
|
||||
adapter={adapter}
|
||||
selected={@adapter_key}
|
||||
disabled={@env_locked}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<details class="admin-provider-other">
|
||||
<summary class="admin-provider-other-toggle">
|
||||
Already have your own email server?
|
||||
@@ -270,60 +221,24 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<%!-- Steps 2 & 3 appear for the selected adapter only --%>
|
||||
<div :if={@selected_adapter} class="admin-adapter-config">
|
||||
<%!-- Step 2: Create an account (providers with sign-up URLs) --%>
|
||||
<div :if={@selected_adapter.url} class="admin-setup-step">
|
||||
<div class="admin-setup-step-header">
|
||||
<span class="admin-setup-step-number">2</span>
|
||||
<h2 class="admin-setup-step-title">Create a free account</h2>
|
||||
</div>
|
||||
<p class="admin-setup-step-desc">
|
||||
<.external_link href={@selected_adapter.url} class="admin-link">
|
||||
Sign up at {@selected_adapter.name} ↗
|
||||
</.external_link>
|
||||
if you don't already have an account. It's free.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%!-- Step 3 (or 2 for advanced): Paste your key --%>
|
||||
<div class="admin-setup-step">
|
||||
<div class="admin-setup-step-header">
|
||||
<span class="admin-setup-step-number">
|
||||
{if @selected_adapter.url, do: "3", else: "2"}
|
||||
</span>
|
||||
<h2 class="admin-setup-step-title">{adapter_fields_title(@selected_adapter)}</h2>
|
||||
</div>
|
||||
<p class="admin-setup-step-desc">{adapter_fields_instruction(@selected_adapter)}</p>
|
||||
<%= for field <- @selected_adapter.fields do %>
|
||||
<.adapter_field_static
|
||||
field_def={field}
|
||||
value={@current_values[field.key]}
|
||||
disabled={@env_locked}
|
||||
error={@field_errors[field.key]}
|
||||
/>
|
||||
<% end %>
|
||||
<%= unless @env_locked do %>
|
||||
<div class="admin-row admin-row-lg">
|
||||
<.button phx-disable-with="Saving...">
|
||||
Save settings
|
||||
</.button>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- No-JS: if adapter changed but config not yet shown, submit to reload --%>
|
||||
<noscript>
|
||||
<div :if={!@selected_adapter} class="admin-row admin-row-lg" style="margin-top: 1rem;">
|
||||
<.button>Continue</.button>
|
||||
</div>
|
||||
</noscript>
|
||||
<%!-- Steps 2 & 3 for each adapter (CSS :has(:checked) shows the active one) --%>
|
||||
<.adapter_config
|
||||
:for={adapter <- @all_adapters}
|
||||
adapter={adapter}
|
||||
values={@all_values[adapter.key] || %{}}
|
||||
field_errors={if(@adapter_key == adapter.key, do: @field_errors, else: %{})}
|
||||
env_locked={@env_locked}
|
||||
/>
|
||||
</.form>
|
||||
</section>
|
||||
|
||||
<%!-- Step 4: Send a test email (only after config saved) --%>
|
||||
<div :if={@email_configured} class="admin-setup-step" style="margin-top: 1.5rem;">
|
||||
<div
|
||||
:if={@email_configured}
|
||||
id="test-email-step"
|
||||
class="admin-setup-step"
|
||||
style="margin-top: 1.5rem;"
|
||||
>
|
||||
<div class="admin-setup-step-header">
|
||||
<span class={[
|
||||
"admin-setup-step-number",
|
||||
@@ -393,23 +308,31 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
Send a test to <strong>{@current_scope.user.email}</strong> to check everything works.
|
||||
</p>
|
||||
<div class="admin-row admin-row-sm">
|
||||
<.button
|
||||
type="button"
|
||||
phx-click="send_test"
|
||||
disabled={@sending_test}
|
||||
phx-disable-with="Sending..."
|
||||
>
|
||||
<.icon name="hero-paper-airplane" class="size-4" /> Send test email
|
||||
</.button>
|
||||
<%!-- No-JS fallback for test email --%>
|
||||
<%!-- JS: async send via LiveView --%>
|
||||
<span id="test-email-js">
|
||||
<.button
|
||||
type="button"
|
||||
phx-click="send_test"
|
||||
disabled={@sending_test}
|
||||
phx-disable-with="Sending..."
|
||||
>
|
||||
<.icon name="hero-paper-airplane" class="size-4" /> Send test email
|
||||
</.button>
|
||||
</span>
|
||||
<%!-- No-JS: form POST fallback (hides the JS button above) --%>
|
||||
<noscript>
|
||||
<style>
|
||||
#test-email-js { display: none; }
|
||||
</style>
|
||||
<.form
|
||||
for={%{}}
|
||||
action={~p"/admin/settings/email/test"}
|
||||
method="post"
|
||||
style="display:inline"
|
||||
>
|
||||
<.button>Send test email</.button>
|
||||
<.button>
|
||||
<.icon name="hero-paper-airplane" class="size-4" /> Send test email
|
||||
</.button>
|
||||
</.form>
|
||||
</noscript>
|
||||
</div>
|
||||
@@ -422,6 +345,58 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
|
||||
# ── Local components ──
|
||||
|
||||
attr :adapter, :map, required: true
|
||||
attr :values, :map, required: true
|
||||
attr :field_errors, :map, required: true
|
||||
attr :env_locked, :boolean, required: true
|
||||
|
||||
defp adapter_config(assigns) do
|
||||
~H"""
|
||||
<div class="admin-adapter-config" data-adapter={@adapter.key}>
|
||||
<%!-- Create an account (providers with sign-up URLs) --%>
|
||||
<div :if={@adapter.url} class="admin-setup-step">
|
||||
<div class="admin-setup-step-header">
|
||||
<span class="admin-setup-step-number">2</span>
|
||||
<h2 class="admin-setup-step-title">Create a free account</h2>
|
||||
</div>
|
||||
<p class="admin-setup-step-desc">
|
||||
<.external_link href={@adapter.url} class="admin-link">
|
||||
Sign up at {@adapter.name} ↗
|
||||
</.external_link>
|
||||
if you don't already have an account. It's free.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%!-- Paste your key / server details --%>
|
||||
<div class="admin-setup-step">
|
||||
<div class="admin-setup-step-header">
|
||||
<span class="admin-setup-step-number">
|
||||
{if @adapter.url, do: "3", else: "2"}
|
||||
</span>
|
||||
<h2 class="admin-setup-step-title">{adapter_fields_title(@adapter)}</h2>
|
||||
</div>
|
||||
<p class="admin-setup-step-desc">{adapter_fields_instruction(@adapter)}</p>
|
||||
<%= for field <- @adapter.fields do %>
|
||||
<.adapter_field_input
|
||||
adapter_key={@adapter.key}
|
||||
field_def={field}
|
||||
value={@values[field.key]}
|
||||
disabled={@env_locked}
|
||||
error={@field_errors[field.key]}
|
||||
/>
|
||||
<% end %>
|
||||
<%= unless @env_locked do %>
|
||||
<div class="admin-row admin-row-lg">
|
||||
<.button phx-disable-with="Saving...">
|
||||
Save settings
|
||||
</.button>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp adapter_fields_title(%{key: "smtp"}), do: "Enter your server details"
|
||||
defp adapter_fields_title(%{key: "postal"}), do: "Enter your server details"
|
||||
defp adapter_fields_title(%{key: "mailjet"}), do: "Paste your API keys"
|
||||
@@ -448,10 +423,7 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
|
||||
defp provider_card(assigns) do
|
||||
~H"""
|
||||
<label class={[
|
||||
"card-radio-card",
|
||||
@selected == @adapter.key && "card-radio-card-selected"
|
||||
]}>
|
||||
<label class="card-radio-card">
|
||||
<input
|
||||
type="radio"
|
||||
id={"email-adapter-#{@adapter.key}"}
|
||||
@@ -474,18 +446,20 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
end
|
||||
|
||||
# ── Field renderers ──
|
||||
# Fields are namespaced per adapter: email[brevo][api_key], email[smtp][relay], etc.
|
||||
|
||||
attr :adapter_key, :string, required: true
|
||||
attr :field_def, :map, required: true
|
||||
attr :value, :any, default: nil
|
||||
attr :disabled, :boolean, default: false
|
||||
attr :error, :string, default: nil
|
||||
|
||||
defp adapter_field_static(%{field_def: %{type: :secret}} = assigns) do
|
||||
defp adapter_field_input(%{field_def: %{type: :secret}} = assigns) do
|
||||
assigns = assign(assigns, :errors, if(assigns.error, do: [assigns.error], else: []))
|
||||
|
||||
~H"""
|
||||
<.input
|
||||
name={"email[#{@field_def.key}]"}
|
||||
name={"email[#{@adapter_key}][#{@field_def.key}]"}
|
||||
value=""
|
||||
type="text"
|
||||
label={@field_def.label}
|
||||
@@ -497,12 +471,12 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
"""
|
||||
end
|
||||
|
||||
defp adapter_field_static(%{field_def: %{type: :integer}} = assigns) do
|
||||
defp adapter_field_input(%{field_def: %{type: :integer}} = assigns) do
|
||||
assigns = assign(assigns, :errors, if(assigns.error, do: [assigns.error], else: []))
|
||||
|
||||
~H"""
|
||||
<.input
|
||||
name={"email[#{@field_def.key}]"}
|
||||
name={"email[#{@adapter_key}][#{@field_def.key}]"}
|
||||
value={@value || @field_def.default || ""}
|
||||
type="number"
|
||||
label={@field_def.label}
|
||||
@@ -512,12 +486,12 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
"""
|
||||
end
|
||||
|
||||
defp adapter_field_static(assigns) do
|
||||
defp adapter_field_input(assigns) do
|
||||
assigns = assign(assigns, :errors, if(assigns.error, do: [assigns.error], else: []))
|
||||
|
||||
~H"""
|
||||
<.input
|
||||
name={"email[#{@field_def.key}]"}
|
||||
name={"email[#{@adapter_key}][#{@field_def.key}]"}
|
||||
value={@value || @field_def.default || ""}
|
||||
type="text"
|
||||
label={@field_def.label}
|
||||
|
||||
Reference in New Issue
Block a user