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). - **MUST:** Required fields are marked consistently (UI indicator + accessible text).
- **SHOULD:** If required-ness is configurable via settings, display it consistently in the form. - **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) ## 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. **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 ## 5. Password from File

View file

@ -115,6 +115,7 @@ defmodule MvWeb.GlobalSettingsLive do
</:subtitle> </:subtitle>
</.header> </.header>
<div class="mt-6 space-y-6 max-w-4xl px-4">
<%!-- Club Settings Section --%> <%!-- Club Settings Section --%>
<.form_section title={gettext("Club Settings")}> <.form_section title={gettext("Club Settings")}>
<.form for={@form} id="settings-form" phx-change="validate" phx-submit="save"> <.form for={@form} id="settings-form" phx-change="validate" phx-submit="save">
@ -135,7 +136,9 @@ defmodule MvWeb.GlobalSettingsLive do
<%!-- Join Form Section (Beitrittsformular) --%> <%!-- Join Form Section (Beitrittsformular) --%>
<.form_section title={gettext("Join Form")}> <.form_section title={gettext("Join Form")}>
<p class="text-sm text-base-content/70 mb-4"> <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> </p>
<%!-- Enable/disable --%> <%!-- Enable/disable --%>
@ -155,7 +158,7 @@ defmodule MvWeb.GlobalSettingsLive do
<div :if={@join_form_enabled}> <div :if={@join_form_enabled}>
<%!-- Copyable join page link (below checkbox, above field list) --%> <%!-- 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"> <p class="text-sm text-base-content/70 mb-2">
{gettext("Link to the public join page (share this with applicants):")} {gettext("Link to the public join page (share this with applicants):")}
</p> </p>
@ -253,7 +256,7 @@ defmodule MvWeb.GlobalSettingsLive do
</p> </p>
<%!-- Fields table (compact width, reorderable) --%> <%!-- 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 <.sortable_table
id="join-form-fields-table" id="join-form-fields-table"
rows={@join_form_fields} rows={@join_form_fields}
@ -263,7 +266,11 @@ defmodule MvWeb.GlobalSettingsLive do
<:col :let={field} label={gettext("Field")} class="min-w-[14rem]"> <:col :let={field} label={gettext("Field")} class="min-w-[14rem]">
{field.label} {field.label}
</:col> </: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 <input
type="checkbox" type="checkbox"
class="checkbox checkbox-sm" class="checkbox checkbox-sm"
@ -317,7 +324,8 @@ defmodule MvWeb.GlobalSettingsLive do
<% end %> <% end %>
<.form for={@form} id="smtp-form" phx-change="validate" phx-submit="save"> <.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 <.input
field={@form[:smtp_host]} field={@form[:smtp_host]}
type="text" type="text"
@ -337,6 +345,21 @@ defmodule MvWeb.GlobalSettingsLive do
disabled={@smtp_port_env_set} disabled={@smtp_port_env_set}
placeholder={if(@smtp_port_env_set, do: gettext("From SMTP_PORT"), else: "587")} 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 <.input
field={@form[:smtp_username]} field={@form[:smtp_username]}
type="text" 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 <.input
field={@form[:smtp_password]} field={@form[:smtp_password]}
type="password" type="password"
label="" label={gettext("Password")}
disabled={@smtp_password_env_set} disabled={@smtp_password_env_set}
placeholder={ placeholder={
if(@smtp_password_env_set, if(@smtp_password_env_set,
@ -375,18 +389,8 @@ defmodule MvWeb.GlobalSettingsLive do
} }
/> />
</div> </div>
<.input
field={@form[:smtp_ssl]} <div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
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)}
/>
<.input <.input
field={@form[:smtp_from_email]} field={@form[:smtp_from_email]}
type="email" type="email"
@ -409,7 +413,8 @@ defmodule MvWeb.GlobalSettingsLive do
} }
/> />
</div> </div>
<p class="mt-2 text-sm text-base-content/60"> </div>
<p class="mb-3 text-sm text-base-content/60">
{gettext( {gettext(
"The sender email must be owned by or authorized for the SMTP user on most servers." "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" class="space-y-3"
> >
<div class="flex flex-wrap items-end gap-3"> <div class="flex flex-wrap items-end gap-3">
<div class="form-control"> <fieldset class="fieldset">
<label class="label" for="smtp-test-to-email"> <label>
<span class="label-text">{gettext("Recipient")}</span> <span class="mb-1 label">{gettext("Recipient")}</span>
</label>
<input <input
id="smtp-test-to-email" id="smtp-test-to-email"
type="email" type="email"
name="to_email" name="to_email"
data-testid="smtp-test-email-input" data-testid="smtp-test-email-input"
value={@smtp_test_to_email} value={@smtp_test_to_email}
class="input input-bordered" class="w-full input input-bordered"
placeholder="test@example.com" placeholder="test@example.com"
phx-change="update_smtp_test_to_email" phx-change="update_smtp_test_to_email"
/> />
</div> </label>
</fieldset>
<.button <.button
type="submit" type="submit"
variant="outline" variant="secondary"
class="mb-1"
data-testid="smtp-send-test-email" data-testid="smtp-send-test-email"
phx-disable-with={gettext("Sending...")} phx-disable-with={gettext("Sending...")}
> >
@ -493,19 +499,27 @@ defmodule MvWeb.GlobalSettingsLive do
) )
} }
/> />
<div class="form-control"> <fieldset class="mb-2 fieldset">
<label class="label" for={@form[:vereinfacht_api_key].id}> <label>
<span class="label-text">{gettext("API Key")}</span> <span class="mb-1 label">{gettext("API Key")}</span>
<%= if @vereinfacht_api_key_set do %> <%= if @vereinfacht_api_key_set do %>
<span class="label-text-alt"> <span class="label-text-alt">
<.badge variant="neutral" size="sm">{gettext("(set)")}</.badge> <.badge variant="neutral" size="sm">{gettext("(set)")}</.badge>
</span> </span>
<% end %> <% end %>
</label> <input
<.input
field={@form[:vereinfacht_api_key]}
type="password" 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} disabled={@vereinfacht_api_key_env_set}
placeholder={ placeholder={
if(@vereinfacht_api_key_env_set, 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 <.input
field={@form[:vereinfacht_club_id]} field={@form[:vereinfacht_club_id]}
type="text" type="text"
@ -556,7 +583,7 @@ defmodule MvWeb.GlobalSettingsLive do
<.button <.button
:if={Mv.Config.vereinfacht_configured?()} :if={Mv.Config.vereinfacht_configured?()}
type="button" type="button"
variant="outline" variant="secondary"
phx-click="test_vereinfacht_connection" phx-click="test_vereinfacht_connection"
phx-disable-with={gettext("Testing...")} phx-disable-with={gettext("Testing...")}
> >
@ -565,7 +592,7 @@ defmodule MvWeb.GlobalSettingsLive do
<.button <.button
:if={Mv.Config.vereinfacht_configured?()} :if={Mv.Config.vereinfacht_configured?()}
type="button" type="button"
variant="outline" variant="secondary"
phx-click="sync_vereinfacht_contacts" phx-click="sync_vereinfacht_contacts"
phx-disable-with={gettext("Syncing...")} phx-disable-with={gettext("Syncing...")}
> >
@ -622,19 +649,27 @@ defmodule MvWeb.GlobalSettingsLive do
) )
} }
/> />
<div class="form-control"> <fieldset class="mb-2 fieldset">
<label class="label" for={@form[:oidc_client_secret].id}> <label>
<span class="label-text">{gettext("Client Secret")}</span> <span class="mb-1 label">{gettext("Client Secret")}</span>
<%= if @oidc_client_secret_set do %> <%= if @oidc_client_secret_set do %>
<span class="label-text-alt"> <span class="label-text-alt">
<.badge variant="neutral" size="sm">{gettext("(set)")}</.badge> <.badge variant="neutral" size="sm">{gettext("(set)")}</.badge>
</span> </span>
<% end %> <% end %>
</label> <input
<.input
field={@form[:oidc_client_secret]}
type="password" 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} disabled={@oidc_client_secret_env_set}
placeholder={ placeholder={
if(@oidc_client_secret_env_set, 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 <.input
field={@form[:oidc_admin_group_name]} field={@form[:oidc_admin_group_name]}
type="text" type="text"
@ -709,6 +757,7 @@ defmodule MvWeb.GlobalSettingsLive do
</.button> </.button>
</.form> </.form>
</.form_section> </.form_section>
</div>
</Layouts.app> </Layouts.app>
""" """
end 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:" msgid "To confirm deletion, please enter this text:"
msgstr "Um die Löschung zu bestätigen, gib bitte folgenden Text ein:" 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 #, elixir-autogen, elixir-format
msgid "Toggle dark mode" msgid "Toggle dark mode"
msgstr "Dunklen Modus umschalten" msgstr "Dunklen Modus umschalten"

View file

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

View file

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