feat: respect field types in join requests
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Simon 2026-05-06 11:37:40 +02:00
parent 95b666f04f
commit 6327ea00eb
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
2 changed files with 115 additions and 31 deletions

View file

@ -5,6 +5,7 @@ defmodule MvWeb.JoinLive do
""" """
use MvWeb, :live_view use MvWeb, :live_view
alias Ash.Resource.Info
alias Mv.Membership alias Mv.Membership
alias MvWeb.JoinRateLimit alias MvWeb.JoinRateLimit
alias MvWeb.Translations.MemberFields alias MvWeb.Translations.MemberFields
@ -54,10 +55,6 @@ defmodule MvWeb.JoinLive do
{gettext("Become a member")} {gettext("Become a member")}
</.header> </.header>
<p class="text-base-content/80">
{gettext("Please enter your details for the membership application here.")}
</p>
<%= if @submitted do %> <%= if @submitted do %>
<div data-testid="join-success-message" class="alert alert-success"> <div data-testid="join-success-message" class="alert alert-success">
<p class="font-medium"> <p class="font-medium">
@ -67,6 +64,9 @@ defmodule MvWeb.JoinLive do
</p> </p>
</div> </div>
<% else %> <% else %>
<p class="text-base-content/80">
{gettext("Please enter your details for the membership application here.")}
</p>
<.form <.form
for={@form} for={@form}
id="join-form" id="join-form"
@ -80,18 +80,31 @@ defmodule MvWeb.JoinLive do
<% end %> <% end %>
<%= for field <- @join_fields do %> <%= for field <- @join_fields do %>
<div> <div class={
if field.input_type == "checkbox", do: "flex items-end gap-3", else: ""
}>
<label for={"join-field-#{field.id}"} class="label"> <label for={"join-field-#{field.id}"} class="label">
<span class="label-text">{field.label}{if field.required, do: " *"}</span> <span class="label-text">{field.label}{if field.required, do: " *"}</span>
</label> </label>
<input <%= if field.input_type == "checkbox" do %>
type={input_type(field.id)} <input
name={field.id} type="checkbox"
id={"join-field-#{field.id}"} name={field.id}
value={@form.params[field.id]} id={"join-field-#{field.id}"}
required={field.required} checked={checkbox_checked?(@form.params[field.id])}
class="input input-bordered w-full" required={field.required}
/> class="checkbox checkbox-sm"
/>
<% else %>
<input
type={field.input_type}
name={field.id}
id={"join-field-#{field.id}"}
value={@form.params[field.id]}
required={field.required}
class="input input-bordered w-full"
/>
<% end %>
</div> </div>
<% end %> <% end %>
@ -216,21 +229,27 @@ defmodule MvWeb.JoinLive do
defp build_join_fields_with_labels(allowlist) do defp build_join_fields_with_labels(allowlist) do
member_field_strings = Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1) member_field_strings = Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)
custom_field_name_by_id = custom_field_name_map(allowlist, member_field_strings) custom_field_by_id = custom_field_map(allowlist, member_field_strings)
Enum.map(allowlist, fn %{id: id, required: required} -> Enum.map(allowlist, fn %{id: id, required: required} ->
label = build_join_field(id, required, member_field_strings, custom_field_by_id)
if id in member_field_strings do
MemberFields.label(String.to_existing_atom(id))
else
Map.get(custom_field_name_by_id, id, gettext("Field"))
end
%{id: id, label: label, required: required}
end) end)
end end
defp custom_field_name_map(allowlist, member_field_strings) do defp build_join_field(id, required, member_field_strings, custom_field_by_id) do
if id in member_field_strings do
label = MemberFields.label(String.to_existing_atom(id))
%{id: id, label: label, required: required, input_type: member_field_input_type(id)}
else
custom_field = Map.get(custom_field_by_id, id)
label = if custom_field, do: custom_field.name, else: gettext("Field")
input_type = custom_field_input_type(custom_field && custom_field.value_type)
%{id: id, label: label, required: required, input_type: input_type}
end
end
defp custom_field_map(allowlist, member_field_strings) do
custom_field_ids = custom_field_ids =
allowlist allowlist
|> Enum.map(& &1.id) |> Enum.map(& &1.id)
@ -242,7 +261,7 @@ defmodule MvWeb.JoinLive do
ids -> ids ->
Mv.Membership.CustomField Mv.Membership.CustomField
|> Ash.Query.select([:id, :name]) |> Ash.Query.select([:id, :name, :value_type])
|> Ash.read(domain: Mv.Membership, authorize?: false) |> Ash.read(domain: Mv.Membership, authorize?: false)
|> case do |> case do
{:ok, fields} -> {:ok, fields} ->
@ -250,7 +269,7 @@ defmodule MvWeb.JoinLive do
fields fields
|> Enum.filter(&MapSet.member?(allowed_ids, &1.id)) |> Enum.filter(&MapSet.member?(allowed_ids, &1.id))
|> Map.new(&{&1.id, &1.name}) |> Map.new(&{&1.id, &1})
{:error, _} -> {:error, _} ->
%{} %{}
@ -265,8 +284,45 @@ defmodule MvWeb.JoinLive do
|> Map.put(@honeypot_field, "") |> Map.put(@honeypot_field, "")
end end
defp input_type("email"), do: "email" defp member_field_input_type("email"), do: "email"
defp input_type(_), do: "text"
defp member_field_input_type(field_id) when is_binary(field_id) do
case member_field_atom(field_id) do
nil ->
"text"
field_atom ->
Mv.Membership.Member
|> Info.attribute(field_atom)
|> attribute_to_input_type()
end
end
defp member_field_input_type(_), do: "text"
defp member_field_atom(field_id) when is_binary(field_id) do
Mv.Constants.member_fields()
|> Enum.find(&(Atom.to_string(&1) == field_id))
end
defp custom_field_input_type(type), do: attribute_to_input_type(%{type: type})
defp attribute_to_input_type(%{type: type}) when type in [:date, Ash.Type.Date], do: "date"
defp attribute_to_input_type(%{type: type}) when type in [:integer, Ash.Type.Integer],
do: "number"
defp attribute_to_input_type(%{type: type}) when type in [:boolean, Ash.Type.Boolean],
do: "checkbox"
defp attribute_to_input_type(%{type: type}) when type in [:email, Mv.Membership.Email],
do: "email"
defp attribute_to_input_type(%{type: _}), do: "text"
defp attribute_to_input_type(nil), do: "text"
defp checkbox_checked?(value) when value in [true, "true", "on", "1"], do: true
defp checkbox_checked?(_), do: false
defp build_submit_attrs(params, join_fields) do defp build_submit_attrs(params, join_fields) do
allowlist_ids = MapSet.new(Enum.map(join_fields, & &1.id)) allowlist_ids = MapSet.new(Enum.map(join_fields, & &1.id))

View file

@ -192,7 +192,12 @@ defmodule MvWeb.JoinLiveTest do
{:ok, view, _html} = live(conn, "/join") {:ok, view, _html} = live(conn, "/join")
assert has_element?(view, "#join-form") assert has_element?(view, "#join-form")
assert has_element?(view, "input#join-field-#{boolean_field.id}[name='#{boolean_field.id}']")
assert has_element?(
view,
"input#join-field-#{boolean_field.id}[name='#{boolean_field.id}']"
)
assert has_element?(view, "input#join-field-#{boolean_field.id}[type='checkbox']") assert has_element?(view, "input#join-field-#{boolean_field.id}[type='checkbox']")
refute has_element?(view, "input#join-field-#{boolean_field.id}[type='text']") refute has_element?(view, "input#join-field-#{boolean_field.id}[type='text']")
end end
@ -203,13 +208,19 @@ defmodule MvWeb.JoinLiveTest do
{:ok, settings} = Membership.get_settings() {:ok, settings} = Membership.get_settings()
{:ok, integer_field} = {:ok, integer_field} =
Membership.create_custom_field(%{name: "Lucky number", value_type: :integer}, actor: system_actor) Membership.create_custom_field(%{name: "Lucky number", value_type: :integer},
actor: system_actor
)
{:ok, date_field} = {:ok, date_field} =
Membership.create_custom_field(%{name: "Birth date", value_type: :date}, actor: system_actor) Membership.create_custom_field(%{name: "Birth date", value_type: :date},
actor: system_actor
)
{:ok, email_field} = {:ok, email_field} =
Membership.create_custom_field(%{name: "Secondary email", value_type: :email}, actor: system_actor) Membership.create_custom_field(%{name: "Secondary email", value_type: :email},
actor: system_actor
)
{:ok, _} = {:ok, _} =
Membership.update_settings(settings, %{ Membership.update_settings(settings, %{
@ -229,6 +240,23 @@ defmodule MvWeb.JoinLiveTest do
assert has_element?(view, "input#join-field-#{date_field.id}[type='date']") assert has_element?(view, "input#join-field-#{date_field.id}[type='date']")
assert has_element?(view, "input#join-field-#{email_field.id}[type='email']") assert has_element?(view, "input#join-field-#{email_field.id}[type='email']")
end end
@tag role: :unauthenticated
test "renders standard date member fields with date input type", %{conn: conn} do
{:ok, settings} = Membership.get_settings()
{:ok, _} =
Membership.update_settings(settings, %{
join_form_enabled: true,
join_form_field_ids: ["email", "join_date"],
join_form_field_required: %{"email" => true, "join_date" => false}
})
{:ok, view, _html} = live(conn, "/join")
assert has_element?(view, "input#join-field-join_date[type='date']")
refute has_element?(view, "input#join-field-join_date[type='text']")
end
end end
describe "submit join form with typed custom fields" do describe "submit join form with typed custom fields" do