2026-02-21 19:29:34 +00:00
|
|
|
defmodule BerrypodWeb.Admin.EmailSettings do
|
|
|
|
|
use BerrypodWeb, :live_view
|
|
|
|
|
|
2026-03-04 12:17:56 +00:00
|
|
|
alias Berrypod.KeyValidation
|
2026-02-21 19:29:34 +00:00
|
|
|
alias Berrypod.Mailer
|
|
|
|
|
alias Berrypod.Mailer.Adapters
|
|
|
|
|
alias Berrypod.Settings
|
|
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
def mount(_params, _session, socket) do
|
|
|
|
|
env_locked = Mailer.env_var_configured?()
|
|
|
|
|
{current_adapter, current_values} = Mailer.current_config()
|
|
|
|
|
saved_adapter = Settings.get_setting("email_adapter")
|
|
|
|
|
|
|
|
|
|
adapter_key = current_adapter || saved_adapter
|
|
|
|
|
|
|
|
|
|
{:ok,
|
|
|
|
|
socket
|
|
|
|
|
|> assign(:page_title, "Email settings")
|
|
|
|
|
|> assign(:env_locked, env_locked)
|
|
|
|
|
|> assign(:adapter_key, adapter_key)
|
|
|
|
|
|> assign(:current_values, current_values)
|
|
|
|
|
|> assign(:all_adapters, Adapters.all())
|
|
|
|
|
|> assign(:provider_options, provider_options())
|
|
|
|
|
|> assign(:email_configured, Mailer.email_configured?())
|
|
|
|
|
|> assign(
|
|
|
|
|
:from_address,
|
|
|
|
|
Settings.get_setting("email_from_address") || socket.assigns.current_scope.user.email
|
|
|
|
|
)
|
|
|
|
|
|> assign(:sending_test, false)
|
2026-03-03 17:41:08 +00:00
|
|
|
|> assign(:from_checklist, false)
|
2026-03-04 12:17:56 +00:00
|
|
|
|> assign(:field_errors, %{})
|
2026-02-21 19:29:34 +00:00
|
|
|
|> assign(:form, to_form(%{}, as: :email))}
|
|
|
|
|
end
|
|
|
|
|
|
2026-03-03 17:41:08 +00:00
|
|
|
@impl true
|
|
|
|
|
def handle_params(params, _uri, socket) do
|
|
|
|
|
{:noreply, assign(socket, :from_checklist, params["from"] == "checklist")}
|
|
|
|
|
end
|
|
|
|
|
|
2026-02-21 19:57:23 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
value =
|
|
|
|
|
case field.type do
|
|
|
|
|
:secret -> Settings.secret_hint(settings_key)
|
|
|
|
|
_ -> Settings.get_setting(settings_key)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
{field.key, value}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2026-02-21 19:29:34 +00:00
|
|
|
defp provider_options do
|
|
|
|
|
Enum.map(Adapters.all(), fn adapter ->
|
|
|
|
|
%{
|
|
|
|
|
value: adapter.key,
|
|
|
|
|
name: adapter.name,
|
|
|
|
|
description: adapter.description,
|
|
|
|
|
tags: adapter.tags,
|
|
|
|
|
url: adapter.url
|
|
|
|
|
}
|
|
|
|
|
end)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
def handle_event("change_adapter", %{"email" => %{"adapter" => key}}, socket) do
|
2026-02-21 19:57:23 +00:00
|
|
|
values = load_adapter_values(key)
|
2026-03-04 12:17:56 +00:00
|
|
|
|
|
|
|
|
{:noreply,
|
|
|
|
|
socket
|
|
|
|
|
|> assign(:adapter_key, key)
|
|
|
|
|
|> assign(:current_values, values)
|
|
|
|
|
|> assign(:field_errors, %{})}
|
2026-02-21 19:29:34 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def handle_event("save", %{"email" => params}, socket) do
|
|
|
|
|
if socket.assigns.env_locked do
|
|
|
|
|
{:noreply, put_flash(socket, :error, "Email config is controlled by environment variables")}
|
|
|
|
|
else
|
|
|
|
|
adapter_key = params["adapter"]
|
|
|
|
|
adapter_info = Adapters.get(adapter_key)
|
|
|
|
|
|
|
|
|
|
if adapter_info do
|
|
|
|
|
save_adapter_config(socket, adapter_info, params)
|
|
|
|
|
else
|
|
|
|
|
{:noreply, put_flash(socket, :error, "Please select an email provider")}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def handle_event("disconnect", _params, socket) do
|
|
|
|
|
if socket.assigns.env_locked do
|
|
|
|
|
{:noreply, put_flash(socket, :error, "Email config is controlled by environment variables")}
|
|
|
|
|
else
|
|
|
|
|
# Clear all email settings
|
|
|
|
|
Settings.delete_setting("email_adapter")
|
|
|
|
|
|
|
|
|
|
for key <- Adapters.all_field_keys() do
|
|
|
|
|
Settings.delete_setting(key)
|
|
|
|
|
end
|
|
|
|
|
|
2026-02-21 22:25:27 +00:00
|
|
|
Mailer.clear_email_verified()
|
|
|
|
|
|
2026-02-21 19:29:34 +00:00
|
|
|
# Reset to Local adapter
|
|
|
|
|
Application.put_env(:berrypod, Mailer, adapter: Swoosh.Adapters.Local)
|
|
|
|
|
|
|
|
|
|
{:noreply,
|
|
|
|
|
socket
|
|
|
|
|
|> assign(:adapter_key, nil)
|
|
|
|
|
|> assign(:current_values, %{})
|
|
|
|
|
|> assign(:email_configured, false)
|
|
|
|
|
|> put_flash(:info, "Email provider disconnected")}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def handle_event("send_test", _params, socket) do
|
|
|
|
|
user = socket.assigns.current_scope.user
|
|
|
|
|
|
|
|
|
|
socket = assign(socket, :sending_test, true)
|
|
|
|
|
|
|
|
|
|
case Mailer.send_test_email(user.email, socket.assigns.from_address) do
|
|
|
|
|
{:ok, _} ->
|
2026-02-21 22:25:27 +00:00
|
|
|
Mailer.mark_email_verified()
|
|
|
|
|
|
2026-02-21 19:29:34 +00:00
|
|
|
{:noreply,
|
|
|
|
|
socket
|
|
|
|
|
|> assign(:sending_test, false)
|
|
|
|
|
|> put_flash(:info, "Test email sent to #{user.email}")}
|
|
|
|
|
|
|
|
|
|
{:error, reason} ->
|
|
|
|
|
{:noreply,
|
|
|
|
|
socket
|
|
|
|
|
|> assign(:sending_test, false)
|
|
|
|
|
|> put_flash(:error, "Failed to send test email: #{inspect(reason)}")}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp save_adapter_config(socket, adapter_info, params) do
|
2026-03-04 12:17:56 +00:00
|
|
|
# Trim all values
|
|
|
|
|
params = Map.new(params, fn {k, v} -> {k, if(is_binary(v), do: String.trim(v), else: v)} end)
|
|
|
|
|
|
2026-02-21 19:29:34 +00:00
|
|
|
# Validate required fields
|
|
|
|
|
missing =
|
|
|
|
|
adapter_info.fields
|
|
|
|
|
|> Enum.filter(& &1.required)
|
|
|
|
|
|> Enum.filter(fn field ->
|
|
|
|
|
val = params[field.key]
|
|
|
|
|
empty = is_nil(val) or val == ""
|
2026-02-21 19:57:23 +00:00
|
|
|
settings_key = Adapters.settings_key(adapter_info.key, field.key)
|
2026-02-21 19:29:34 +00:00
|
|
|
# Secret fields can be left blank to keep existing value
|
2026-02-21 19:57:23 +00:00
|
|
|
empty and not (field.type == :secret and Settings.get_secret(settings_key) != nil)
|
2026-02-21 19:29:34 +00:00
|
|
|
end)
|
|
|
|
|
|
2026-03-04 12:17:56 +00:00
|
|
|
# Build per-field errors for missing required fields
|
|
|
|
|
missing_errors =
|
|
|
|
|
for field <- missing, into: %{} do
|
|
|
|
|
{field.key, "#{field.label} is required"}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Validate secret field formats for known providers
|
|
|
|
|
format_errors =
|
|
|
|
|
for field <- adapter_info.fields,
|
|
|
|
|
field.type == :secret,
|
|
|
|
|
value = params[field.key],
|
|
|
|
|
value != nil and value != "",
|
|
|
|
|
{:error, message} <- [
|
|
|
|
|
KeyValidation.validate_email_key(value, adapter_info.key, field.key)
|
|
|
|
|
],
|
|
|
|
|
into: %{} do
|
|
|
|
|
{field.key, message}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
field_errors = Map.merge(missing_errors, format_errors)
|
|
|
|
|
|
|
|
|
|
if field_errors != %{} do
|
|
|
|
|
{:noreply, assign(socket, :field_errors, field_errors)}
|
2026-02-21 19:29:34 +00:00
|
|
|
else
|
|
|
|
|
# Save adapter type
|
|
|
|
|
Settings.put_setting("email_adapter", adapter_info.key)
|
|
|
|
|
|
|
|
|
|
# Save current adapter fields (blank secrets keep existing value)
|
|
|
|
|
for field <- adapter_info.fields do
|
|
|
|
|
value = params[field.key]
|
2026-02-21 19:57:23 +00:00
|
|
|
settings_key = Adapters.settings_key(adapter_info.key, field.key)
|
2026-02-21 19:29:34 +00:00
|
|
|
|
|
|
|
|
cond do
|
|
|
|
|
value && value != "" ->
|
|
|
|
|
case field.type do
|
|
|
|
|
:secret -> Settings.put_secret(settings_key, value)
|
|
|
|
|
:integer -> Settings.put_setting(settings_key, String.to_integer(value), "integer")
|
|
|
|
|
_ -> Settings.put_setting(settings_key, value)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
field.type == :secret ->
|
|
|
|
|
:keep
|
|
|
|
|
|
|
|
|
|
true ->
|
|
|
|
|
Settings.delete_setting(settings_key)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Save from address
|
|
|
|
|
from_address = params["from_address"] || ""
|
|
|
|
|
|
|
|
|
|
if from_address != "" do
|
|
|
|
|
Settings.put_setting("email_from_address", from_address)
|
|
|
|
|
end
|
|
|
|
|
|
2026-02-21 22:25:27 +00:00
|
|
|
# Config changed — require re-verification
|
|
|
|
|
Mailer.clear_email_verified()
|
|
|
|
|
|
2026-02-21 19:29:34 +00:00
|
|
|
# Apply config immediately
|
|
|
|
|
Mailer.load_config()
|
|
|
|
|
|
|
|
|
|
# Re-read current state
|
|
|
|
|
{current_adapter, current_values} = Mailer.current_config()
|
|
|
|
|
|
|
|
|
|
{:noreply,
|
|
|
|
|
socket
|
|
|
|
|
|> assign(:adapter_key, current_adapter)
|
|
|
|
|
|> assign(:current_values, current_values)
|
|
|
|
|
|> assign(:from_address, from_address)
|
|
|
|
|
|> assign(:email_configured, Mailer.email_configured?())
|
2026-03-04 12:17:56 +00:00
|
|
|
|> assign(:field_errors, %{})
|
2026-02-21 19:29:34 +00:00
|
|
|
|> put_flash(:info, "Email settings saved")}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
def render(assigns) do
|
|
|
|
|
~H"""
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<div class="admin-content-medium">
|
2026-03-03 17:41:08 +00:00
|
|
|
<div :if={@from_checklist} class="admin-checklist-banner">
|
|
|
|
|
<.icon name="hero-clipboard-document-check" class="size-5 admin-checklist-banner-icon" />
|
|
|
|
|
<span class="admin-checklist-banner-text">
|
|
|
|
|
You're setting up email for your shop.
|
|
|
|
|
</span>
|
|
|
|
|
<.link navigate={~p"/admin"} class="admin-link admin-checklist-banner-link">
|
|
|
|
|
← Back to checklist
|
|
|
|
|
</.link>
|
|
|
|
|
</div>
|
2026-02-21 19:29:34 +00:00
|
|
|
<.header>
|
|
|
|
|
Email settings
|
|
|
|
|
<:subtitle>
|
|
|
|
|
Configure how your shop sends email. <strong>Transactional</strong>
|
|
|
|
|
providers only handle order confirmations and password resets. <strong>All email</strong>
|
|
|
|
|
providers also support newsletters and marketing campaigns.
|
|
|
|
|
</:subtitle>
|
|
|
|
|
</.header>
|
|
|
|
|
|
|
|
|
|
<%= if @env_locked do %>
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<div class="admin-callout-warning">
|
|
|
|
|
<div class="admin-callout-warning-body">
|
|
|
|
|
<span class="admin-callout-warning-icon">
|
|
|
|
|
<.icon name="hero-lock-closed" class="size-5" />
|
|
|
|
|
</span>
|
2026-02-21 19:29:34 +00:00
|
|
|
<div>
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<p class="admin-callout-warning-title">
|
2026-02-21 19:29:34 +00:00
|
|
|
Controlled by environment variables
|
|
|
|
|
</p>
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<p class="admin-callout-warning-desc">
|
2026-02-21 19:29:34 +00:00
|
|
|
Email is configured via <code>SMTP_HOST</code> and related env vars.
|
|
|
|
|
Remove them to configure email from this page instead.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<% end %>
|
|
|
|
|
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<section class="admin-section">
|
2026-02-21 19:29:34 +00:00
|
|
|
<.form for={@form} phx-change="change_adapter" phx-submit="save">
|
|
|
|
|
<div id="email-provider-cards" phx-hook="CardRadioScroll">
|
|
|
|
|
<.card_radio_group
|
|
|
|
|
name="email[adapter]"
|
|
|
|
|
value={@adapter_key}
|
|
|
|
|
legend="Email provider"
|
|
|
|
|
options={@provider_options}
|
|
|
|
|
disabled={@env_locked}
|
|
|
|
|
display={:tags}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<%= for adapter <- @all_adapters do %>
|
|
|
|
|
<% selected = @adapter_key == adapter.key %>
|
|
|
|
|
<div
|
|
|
|
|
id={"adapter-config-#{adapter.key}"}
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
class="admin-adapter-config"
|
2026-02-21 19:29:34 +00:00
|
|
|
hidden={!selected}
|
|
|
|
|
data-adapter={adapter.key}
|
|
|
|
|
>
|
|
|
|
|
<div>
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<h3 class="admin-section-subheading">
|
2026-02-21 19:29:34 +00:00
|
|
|
{adapter.name}
|
2026-03-04 07:12:25 +00:00
|
|
|
<.external_link
|
2026-02-21 19:29:34 +00:00
|
|
|
:if={adapter.url}
|
|
|
|
|
href={adapter.url}
|
2026-03-04 07:12:25 +00:00
|
|
|
icon={false}
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
class="admin-link-subtle admin-adapter-link"
|
2026-03-04 07:12:25 +00:00
|
|
|
aria-label={adapter.name <> " website"}
|
2026-02-21 19:29:34 +00:00
|
|
|
>
|
|
|
|
|
↗
|
2026-03-04 07:12:25 +00:00
|
|
|
</.external_link>
|
2026-02-21 19:29:34 +00:00
|
|
|
</h3>
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<p class="admin-section-desc">{adapter.description}</p>
|
2026-02-21 19:29:34 +00:00
|
|
|
</div>
|
|
|
|
|
<.input
|
|
|
|
|
name="email[from_address]"
|
|
|
|
|
value={@from_address}
|
|
|
|
|
type="email"
|
|
|
|
|
label="From address"
|
|
|
|
|
placeholder="noreply@yourshop.com"
|
|
|
|
|
disabled={!selected || @env_locked}
|
|
|
|
|
/>
|
|
|
|
|
<%= for field <- adapter.fields do %>
|
|
|
|
|
<.adapter_field_static
|
|
|
|
|
field_def={field}
|
|
|
|
|
value={if selected, do: @current_values[field.key]}
|
|
|
|
|
disabled={!selected || @env_locked}
|
2026-03-04 12:17:56 +00:00
|
|
|
error={if selected, do: @field_errors[field.key]}
|
2026-02-21 19:29:34 +00:00
|
|
|
/>
|
|
|
|
|
<% end %>
|
|
|
|
|
<%= unless @env_locked do %>
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<div class="admin-row admin-row-lg">
|
2026-02-21 19:29:34 +00:00
|
|
|
<.button phx-disable-with="Saving..." disabled={!selected}>
|
|
|
|
|
Save settings
|
|
|
|
|
</.button>
|
|
|
|
|
<%= if selected && @email_configured do %>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
phx-click="disconnect"
|
|
|
|
|
data-confirm="Remove email configuration? Transactional emails will stop being sent."
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
class="admin-link-danger"
|
2026-02-21 19:29:34 +00:00
|
|
|
>
|
|
|
|
|
Disconnect
|
|
|
|
|
</button>
|
|
|
|
|
<% end %>
|
|
|
|
|
</div>
|
|
|
|
|
<% end %>
|
|
|
|
|
</div>
|
|
|
|
|
<% end %>
|
|
|
|
|
</.form>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<%= if @email_configured do %>
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<section class="admin-section-bordered">
|
|
|
|
|
<h2 class="admin-section-heading">Test email</h2>
|
|
|
|
|
<p class="admin-help-text">
|
2026-02-21 19:29:34 +00:00
|
|
|
Send a test email to <strong>{@current_scope.user.email}</strong>
|
|
|
|
|
to verify delivery works.
|
|
|
|
|
</p>
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<div class="admin-section-body">
|
2026-02-21 19:29:34 +00:00
|
|
|
<button
|
|
|
|
|
phx-click="send_test"
|
|
|
|
|
disabled={@sending_test}
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
class="admin-btn admin-btn-outline"
|
2026-02-21 19:29:34 +00:00
|
|
|
>
|
|
|
|
|
<.icon name="hero-paper-airplane" class="size-4" />
|
|
|
|
|
{if @sending_test, do: "Sending...", else: "Send test email"}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
<% end %>
|
|
|
|
|
</div>
|
|
|
|
|
"""
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
attr :field_def, :map, required: true
|
|
|
|
|
attr :value, :any, default: nil
|
|
|
|
|
attr :disabled, :boolean, default: false
|
2026-03-04 12:17:56 +00:00
|
|
|
attr :error, :string, default: nil
|
2026-02-21 19:29:34 +00:00
|
|
|
|
|
|
|
|
defp adapter_field_static(%{field_def: %{type: :secret}} = assigns) do
|
2026-03-04 12:17:56 +00:00
|
|
|
assigns = assign(assigns, :errors, if(assigns.error, do: [assigns.error], else: []))
|
|
|
|
|
|
2026-02-21 19:29:34 +00:00
|
|
|
~H"""
|
|
|
|
|
<div>
|
|
|
|
|
<.input
|
|
|
|
|
name={"email[#{@field_def.key}]"}
|
|
|
|
|
value=""
|
2026-03-04 12:17:56 +00:00
|
|
|
type="text"
|
2026-02-21 19:29:34 +00:00
|
|
|
label={@field_def.label}
|
|
|
|
|
autocomplete="off"
|
|
|
|
|
placeholder={if @value, do: @value, else: ""}
|
|
|
|
|
required={@field_def.required && !@value}
|
|
|
|
|
disabled={@disabled}
|
2026-03-04 12:17:56 +00:00
|
|
|
errors={@errors}
|
2026-02-21 19:29:34 +00:00
|
|
|
/>
|
|
|
|
|
<%= if @value && !@disabled do %>
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<p class="admin-help-text">
|
2026-02-21 19:29:34 +00:00
|
|
|
Current: <code>{@value}</code> — leave blank to keep existing value
|
|
|
|
|
</p>
|
|
|
|
|
<% end %>
|
|
|
|
|
</div>
|
|
|
|
|
"""
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp adapter_field_static(%{field_def: %{type: :integer}} = assigns) do
|
2026-03-04 12:17:56 +00:00
|
|
|
assigns = assign(assigns, :errors, if(assigns.error, do: [assigns.error], else: []))
|
|
|
|
|
|
2026-02-21 19:29:34 +00:00
|
|
|
~H"""
|
|
|
|
|
<.input
|
|
|
|
|
name={"email[#{@field_def.key}]"}
|
|
|
|
|
value={@value || @field_def.default || ""}
|
|
|
|
|
type="number"
|
|
|
|
|
label={@field_def.label}
|
|
|
|
|
required={@field_def.required}
|
|
|
|
|
disabled={@disabled}
|
2026-03-04 12:17:56 +00:00
|
|
|
errors={@errors}
|
2026-02-21 19:29:34 +00:00
|
|
|
/>
|
|
|
|
|
"""
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp adapter_field_static(assigns) do
|
2026-03-04 12:17:56 +00:00
|
|
|
assigns = assign(assigns, :errors, if(assigns.error, do: [assigns.error], else: []))
|
|
|
|
|
|
2026-02-21 19:29:34 +00:00
|
|
|
~H"""
|
|
|
|
|
<.input
|
|
|
|
|
name={"email[#{@field_def.key}]"}
|
|
|
|
|
value={@value || @field_def.default || ""}
|
|
|
|
|
type="text"
|
|
|
|
|
label={@field_def.label}
|
|
|
|
|
required={@field_def.required}
|
|
|
|
|
disabled={@disabled}
|
2026-03-04 12:17:56 +00:00
|
|
|
errors={@errors}
|
2026-02-21 19:29:34 +00:00
|
|
|
/>
|
|
|
|
|
"""
|
|
|
|
|
end
|
|
|
|
|
end
|