Improve UX of join requests and fix minor bugs #492
2 changed files with 115 additions and 31 deletions
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue