feat: rearrange smtp settings
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Simon 2026-03-13 15:56:02 +01:00
parent 104faf7006
commit eb18209669
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
6 changed files with 636 additions and 580 deletions

View file

@ -221,6 +221,11 @@ If these cannot be met, use `secondary`/`outline` instead of `ghost`.
- **MUST:** Required fields are marked consistently (UI indicator + accessible text).
- **SHOULD:** If required-ness is configurable via settings, display it consistently in the form.
### 6.4 Form layout (settings / long forms)
- **SHOULD:** On wide viewports, use a responsive grid so related fields share a row and reduce scrolling (e.g. `grid grid-cols-1 lg:grid-cols-2` or `lg:grid-cols-[2fr_5rem_1fr]` for mixed widths).
- **SHOULD:** Limit the main content width for readability (e.g. Settings page uses `max-w-4xl mx-auto px-4` around the content area below the header).
- **Example:** SMTP settings use three rows on large screens (Host, Port, TLS/SSL | Username, Password | Sender email, Sender name) without subsection labels.
---
## 7) Lists, Search & Filters (mandatory UX consistency)

View file

@ -44,6 +44,8 @@ When an ENV variable is set, the corresponding Settings field is read-only in th
**Important:** On most SMTP servers (e.g. Postfix with strict relay policies) the sender email (`smtp_from_email`) must be the same address as `smtp_username` or an alias that is owned by that account.
**Settings UI:** The form uses three rows on wide viewports: host, port, TLS/SSL | username, password | sender email, sender name. Content width is limited by the global settings wrapper (see `DESIGN_GUIDELINES.md` §6.4).
---
## 5. Password from File

View file

@ -115,6 +115,7 @@ defmodule MvWeb.GlobalSettingsLive do
</:subtitle>
</.header>
<div class="mt-6 space-y-6 max-w-4xl px-4">
<%!-- Club Settings Section --%>
<.form_section title={gettext("Club Settings")}>
<.form for={@form} id="settings-form" phx-change="validate" phx-submit="save">
@ -135,7 +136,9 @@ defmodule MvWeb.GlobalSettingsLive do
<%!-- Join Form Section (Beitrittsformular) --%>
<.form_section title={gettext("Join Form")}>
<p class="text-sm text-base-content/70 mb-4">
{gettext("Configure the public join form that allows new members to submit a join request.")}
{gettext(
"Configure the public join form that allows new members to submit a join request."
)}
</p>
<%!-- Enable/disable --%>
@ -155,7 +158,7 @@ defmodule MvWeb.GlobalSettingsLive do
<div :if={@join_form_enabled}>
<%!-- Copyable join page link (below checkbox, above field list) --%>
<div class="mb-4 p-3 max-w-2xl rounded-lg border border-base-300 bg-base-200/50">
<div class="mb-4 p-3 rounded-lg border border-base-300 bg-base-200/50">
<p class="text-sm text-base-content/70 mb-2">
{gettext("Link to the public join page (share this with applicants):")}
</p>
@ -253,7 +256,7 @@ defmodule MvWeb.GlobalSettingsLive do
</p>
<%!-- Fields table (compact width, reorderable) --%>
<div :if={not Enum.empty?(@join_form_fields)} class="mb-4 max-w-2xl">
<div :if={not Enum.empty?(@join_form_fields)} class="mb-4">
<.sortable_table
id="join-form-fields-table"
rows={@join_form_fields}
@ -263,7 +266,11 @@ defmodule MvWeb.GlobalSettingsLive do
<:col :let={field} label={gettext("Field")} class="min-w-[14rem]">
{field.label}
</:col>
<:col :let={field} label={gettext("Required")} class="w-24 max-w-[9.375rem] text-center">
<:col
:let={field}
label={gettext("Required")}
class="w-24 max-w-[9.375rem] text-center"
>
<input
type="checkbox"
class="checkbox checkbox-sm"
@ -317,7 +324,8 @@ defmodule MvWeb.GlobalSettingsLive do
<% end %>
<.form for={@form} id="smtp-form" phx-change="validate" phx-submit="save">
<div class="grid gap-4">
<div class="">
<div class="grid grid-cols-1 gap-4 lg:grid-cols-[2fr_5rem_1fr]">
<.input
field={@form[:smtp_host]}
type="text"
@ -337,6 +345,21 @@ defmodule MvWeb.GlobalSettingsLive do
disabled={@smtp_port_env_set}
placeholder={if(@smtp_port_env_set, do: gettext("From SMTP_PORT"), else: "587")}
/>
<.input
field={@form[:smtp_ssl]}
type="select"
label={gettext("TLS/SSL")}
disabled={@smtp_ssl_env_set}
options={[
{gettext("TLS (port 587, recommended)"), "tls"},
{gettext("SSL (port 465)"), "ssl"},
{gettext("None (port 25, insecure)"), "none"}
]}
placeholder={if(@smtp_ssl_env_set, do: gettext("From SMTP_SSL"), else: nil)}
/>
</div>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<.input
field={@form[:smtp_username]}
type="text"
@ -349,19 +372,10 @@ defmodule MvWeb.GlobalSettingsLive do
)
}
/>
<div class="form-control">
<label class="label" for={@form[:smtp_password].id}>
<span class="label-text">{gettext("Password")}</span>
<%= if @smtp_password_set do %>
<span class="label-text-alt">
<.badge variant="neutral" size="sm">{gettext("(set)")}</.badge>
</span>
<% end %>
</label>
<.input
field={@form[:smtp_password]}
type="password"
label=""
label={gettext("Password")}
disabled={@smtp_password_env_set}
placeholder={
if(@smtp_password_env_set,
@ -375,18 +389,8 @@ defmodule MvWeb.GlobalSettingsLive do
}
/>
</div>
<.input
field={@form[:smtp_ssl]}
type="select"
label={gettext("TLS/SSL")}
disabled={@smtp_ssl_env_set}
options={[
{gettext("TLS (port 587, recommended)"), "tls"},
{gettext("SSL (port 465)"), "ssl"},
{gettext("None (port 25, insecure)"), "none"}
]}
placeholder={if(@smtp_ssl_env_set, do: gettext("From SMTP_SSL"), else: nil)}
/>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<.input
field={@form[:smtp_from_email]}
type="email"
@ -409,7 +413,8 @@ defmodule MvWeb.GlobalSettingsLive do
}
/>
</div>
<p class="mt-2 text-sm text-base-content/60">
</div>
<p class="mb-3 text-sm text-base-content/60">
{gettext(
"The sender email must be owned by or authorized for the SMTP user on most servers."
)}
@ -439,24 +444,25 @@ defmodule MvWeb.GlobalSettingsLive do
class="space-y-3"
>
<div class="flex flex-wrap items-end gap-3">
<div class="form-control">
<label class="label" for="smtp-test-to-email">
<span class="label-text">{gettext("Recipient")}</span>
</label>
<fieldset class="fieldset">
<label>
<span class="mb-1 label">{gettext("Recipient")}</span>
<input
id="smtp-test-to-email"
type="email"
name="to_email"
data-testid="smtp-test-email-input"
value={@smtp_test_to_email}
class="input input-bordered"
class="w-full input input-bordered"
placeholder="test@example.com"
phx-change="update_smtp_test_to_email"
/>
</div>
</label>
</fieldset>
<.button
type="submit"
variant="outline"
variant="secondary"
class="mb-1"
data-testid="smtp-send-test-email"
phx-disable-with={gettext("Sending...")}
>
@ -493,19 +499,27 @@ defmodule MvWeb.GlobalSettingsLive do
)
}
/>
<div class="form-control">
<label class="label" for={@form[:vereinfacht_api_key].id}>
<span class="label-text">{gettext("API Key")}</span>
<fieldset class="mb-2 fieldset">
<label>
<span class="mb-1 label">{gettext("API Key")}</span>
<%= if @vereinfacht_api_key_set do %>
<span class="label-text-alt">
<.badge variant="neutral" size="sm">{gettext("(set)")}</.badge>
</span>
<% end %>
</label>
<.input
field={@form[:vereinfacht_api_key]}
<input
type="password"
label=""
name={@form[:vereinfacht_api_key].name}
id={@form[:vereinfacht_api_key].id}
value={
Phoenix.HTML.Form.normalize_value("password", @form[:vereinfacht_api_key].value)
}
class={
if Phoenix.Component.used_input?(@form[:vereinfacht_api_key]) &&
@form[:vereinfacht_api_key].errors != [],
do: "w-full input input-error",
else: "w-full input"
}
disabled={@vereinfacht_api_key_env_set}
placeholder={
if(@vereinfacht_api_key_env_set,
@ -518,7 +532,20 @@ defmodule MvWeb.GlobalSettingsLive do
)
}
/>
</div>
</label>
<%= for msg <- (
if Phoenix.Component.used_input?(@form[:vereinfacht_api_key]) do
Enum.map(@form[:vereinfacht_api_key].errors, &MvWeb.CoreComponents.translate_error/1)
else
[]
end
) do %>
<p class="mt-1.5 flex gap-2 items-center text-sm text-error">
<.icon name="hero-exclamation-circle" class="size-5" />
{msg}
</p>
<% end %>
</fieldset>
<.input
field={@form[:vereinfacht_club_id]}
type="text"
@ -556,7 +583,7 @@ defmodule MvWeb.GlobalSettingsLive do
<.button
:if={Mv.Config.vereinfacht_configured?()}
type="button"
variant="outline"
variant="secondary"
phx-click="test_vereinfacht_connection"
phx-disable-with={gettext("Testing...")}
>
@ -565,7 +592,7 @@ defmodule MvWeb.GlobalSettingsLive do
<.button
:if={Mv.Config.vereinfacht_configured?()}
type="button"
variant="outline"
variant="secondary"
phx-click="sync_vereinfacht_contacts"
phx-disable-with={gettext("Syncing...")}
>
@ -622,19 +649,27 @@ defmodule MvWeb.GlobalSettingsLive do
)
}
/>
<div class="form-control">
<label class="label" for={@form[:oidc_client_secret].id}>
<span class="label-text">{gettext("Client Secret")}</span>
<fieldset class="mb-2 fieldset">
<label>
<span class="mb-1 label">{gettext("Client Secret")}</span>
<%= if @oidc_client_secret_set do %>
<span class="label-text-alt">
<.badge variant="neutral" size="sm">{gettext("(set)")}</.badge>
</span>
<% end %>
</label>
<.input
field={@form[:oidc_client_secret]}
<input
type="password"
label=""
name={@form[:oidc_client_secret].name}
id={@form[:oidc_client_secret].id}
value={
Phoenix.HTML.Form.normalize_value("password", @form[:oidc_client_secret].value)
}
class={
if Phoenix.Component.used_input?(@form[:oidc_client_secret]) &&
@form[:oidc_client_secret].errors != [],
do: "w-full input input-error",
else: "w-full input"
}
disabled={@oidc_client_secret_env_set}
placeholder={
if(@oidc_client_secret_env_set,
@ -647,7 +682,20 @@ defmodule MvWeb.GlobalSettingsLive do
)
}
/>
</div>
</label>
<%= for msg <- (
if Phoenix.Component.used_input?(@form[:oidc_client_secret]) do
Enum.map(@form[:oidc_client_secret].errors, &MvWeb.CoreComponents.translate_error/1)
else
[]
end
) do %>
<p class="mt-1.5 flex gap-2 items-center text-sm text-error">
<.icon name="hero-exclamation-circle" class="size-5" />
{msg}
</p>
<% end %>
</fieldset>
<.input
field={@form[:oidc_admin_group_name]}
type="text"
@ -709,6 +757,7 @@ defmodule MvWeb.GlobalSettingsLive do
</.button>
</.form>
</.form_section>
</div>
</Layouts.app>
"""
end

View file

@ -3306,7 +3306,7 @@ msgstr "Um die Löschung zu bestätigen, gib bitte den Gruppennamen ein:"
msgid "To confirm deletion, please enter this text:"
msgstr "Um die Löschung zu bestätigen, gib bitte folgenden Text ein:"
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/components/core_components.ex
#, elixir-autogen, elixir-format
msgid "Toggle dark mode"
msgstr "Dunklen Modus umschalten"

View file

@ -3307,7 +3307,7 @@ msgstr ""
msgid "To confirm deletion, please enter this text:"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/components/core_components.ex
#, elixir-autogen, elixir-format
msgid "Toggle dark mode"
msgstr ""

View file

@ -3307,7 +3307,7 @@ msgstr ""
msgid "To confirm deletion, please enter this text:"
msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/components/core_components.ex
#, elixir-autogen, elixir-format
msgid "Toggle dark mode"
msgstr ""