feat: adds form for member fields

This commit is contained in:
carla 2025-12-16 17:12:26 +01:00
parent 18c082a893
commit 5fa0b48acc
2 changed files with 592 additions and 105 deletions

View file

@ -6,12 +6,14 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do
- List all member fields from Mv.Constants.member_fields()
- Display show_in_overview status as badge (Yes/No)
- Display required status for required fields (first_name, last_name, email)
- Toggle show_in_overview flag for each field
- Edit member field properties (expandable form like custom fields)
- Updates Settings.member_field_visibility
"""
use MvWeb, :live_component
alias Mv.Membership
alias MvWeb.Translations.MemberFields
alias MvWeb.Translations.FieldTypes
@required_fields [:first_name, :last_name, :email]
@ -24,123 +26,123 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do
~H"""
<div id={@id}>
<.form_section title={gettext("Memberdata")}>
<p class="text-sm text-base-content/70 mb-4">
{gettext(
"These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview."
)}
</p>
<p class="text-sm text-base-content/70 mb-4">
{gettext(
"These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview."
)}
</p>
<.table id="member_fields" rows={@member_fields}>
<:col :let={{_field_name, field_data}} label={gettext("Field Name")}>
{format_field_name(field_data.field)}
</:col>
<%!-- Show form when editing --%>
<div :if={@show_form} class="mb-8">
<.live_component
module={MvWeb.MemberFieldLive.FormComponent}
id={@form_id}
member_field={@editing_member_field}
settings={@settings}
on_save={
fn member_field, action ->
send(self(), {:member_field_saved, member_field, action})
end
}
on_cancel={fn -> send_update(__MODULE__, id: @id, show_form: false) end}
/>
</div>
<:col
:let={{_field_name, field_data}}
label={gettext("Required")}
class="max-w-[9.375rem] text-center"
<%!-- Hide table when form is visible --%>
<.table
:if={!@show_form}
id="member_fields"
rows={@member_fields}
>
<:col :let={{_field_name, field_data}} label={gettext("Name")}>
{MemberFields.label(field_data.field)}
</:col>
<:col :let={{_field_name, field_data}} label={gettext("Value Type")}>
{format_value_type(field_data.field)}
</:col>
<:col :let={{_field_name, field_data}} label={gettext("Description")}>
{field_data.description || ""}
</:col>
<:col
:let={{_field_name, field_data}}
label={gettext("Required")}
class="max-w-[9.375rem] text-center"
>
<span
:if={@required?.(field_data.field)}
class="text-base-content font-semibold"
>
<span
:if={@required?.(field_data.field)}
class="text-base-content font-semibold"
>
{gettext("Required")}
</span>
<span :if={!@required?.(field_data.field)} class="text-base-content/70">
{gettext("Optional")}
</span>
</:col>
{gettext("Required")}
</span>
<span :if={!@required?.(field_data.field)} class="text-base-content/70">
{gettext("Optional")}
</span>
</:col>
<:col
:let={{_field_name, field_data}}
label={gettext("Show in overview")}
class="max-w-[9.375rem] text-center"
<:col
:let={{_field_name, field_data}}
label={gettext("Show in overview")}
class="max-w-[9.375rem] text-center"
>
<span :if={field_data.show_in_overview} class="badge badge-success">
{gettext("Yes")}
</span>
<span :if={!field_data.show_in_overview} class="badge badge-ghost">
{gettext("No")}
</span>
</:col>
<:action :let={{_field_name, field_data}}>
<.link
phx-click="edit_member_field"
phx-value-field={Atom.to_string(field_data.field)}
phx-target={@myself}
>
<span :if={field_data.show_in_overview} class="badge badge-success">
{gettext("Yes")}
</span>
<span :if={!field_data.show_in_overview} class="badge badge-ghost">
{gettext("No")}
</span>
</:col>
<:action :let={{_field_name, field_data}}>
<button
id={"member-field-#{field_data.field}-toggle"}
phx-click="toggle_field_visibility"
phx-keydown="toggle_field_visibility"
phx-key="Enter"
phx-target={@myself}
phx-value-field={field_data.field}
class="btn btn-sm btn-secondary"
aria-label={
if field_data.show_in_overview,
do:
gettext("Hide %{field} in overview", field: format_field_name(field_data.field)),
else:
gettext("Show %{field} in overview", field: format_field_name(field_data.field))
}
aria-pressed={to_string(field_data.show_in_overview)}
>
{if field_data.show_in_overview, do: gettext("Hide"), else: gettext("Show")}
</button>
</:action>
</.table>
</.form_section>
{gettext("Edit")}
</.link>
</:action>
</.table>
</div>
"""
end
@impl true
def update(assigns, socket) do
# If show_form is explicitly provided in assigns, reset editing state
socket =
if Map.has_key?(assigns, :show_form) and assigns.show_form == false do
socket
|> assign(:editing_member_field, nil)
|> assign(:form_id, "member-field-form-new")
else
socket
end
{:ok,
socket
|> assign(assigns)
|> assign_new(:settings, fn -> get_settings() end)}
|> assign_new(:settings, fn -> get_settings() end)
|> assign_new(:show_form, fn -> false end)
|> assign_new(:form_id, fn -> "member-field-form-new" end)
|> assign_new(:editing_member_field, fn -> nil end)}
end
@impl true
def handle_event("toggle_field_visibility", %{"field" => field_string}, socket) do
def handle_event("edit_member_field", %{"field" => field_string}, socket) do
# Validate that the field is a valid member field before converting to atom
valid_fields = Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)
if field_string in valid_fields do
{:ok, settings} = Membership.get_settings()
field_atom = String.to_existing_atom(field_string)
# Get current visibility config
current_visibility = settings.member_field_visibility || %{}
# Normalize keys to strings
normalized_visibility =
Enum.reduce(current_visibility, %{}, fn
{key, value}, acc when is_atom(key) ->
Map.put(acc, Atom.to_string(key), value)
{key, value}, acc when is_binary(key) ->
Map.put(acc, key, value)
end)
# Toggle the field visibility
current_value = Map.get(normalized_visibility, field_string, true)
new_value = !current_value
updated_visibility = Map.put(normalized_visibility, field_string, new_value)
# Update settings
case Membership.update_member_field_visibility(settings, updated_visibility) do
{:ok, updated_settings} ->
# Send message to parent LiveView
send(self(), {:member_field_visibility_updated})
{:noreply,
socket
|> assign(:settings, updated_settings)}
{:error, error} ->
# Send error message to parent LiveView for user feedback
send(self(), {:member_field_visibility_error, error})
{:noreply, socket}
end
{:noreply,
socket
|> assign(:show_form, true)
|> assign(:editing_member_field, field_atom)
|> assign(:form_id, "member-field-form-#{field_string}")}
else
{:noreply, socket}
end
@ -169,9 +171,57 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do
Enum.map(member_fields, fn field ->
show_in_overview = Map.get(normalized_config, field, true)
attribute = Ash.Resource.Info.attribute(Mv.Membership.Member, field)
{Atom.to_string(field), %{field: field, show_in_overview: show_in_overview}}
%{
field: field,
show_in_overview: show_in_overview,
value_type: (attribute && attribute.type) || :string,
description: nil
}
end)
|> Enum.map(fn field_data ->
{Atom.to_string(field_data.field), field_data}
end)
end
defp format_value_type(field) when is_atom(field) do
case Ash.Resource.Info.attribute(Mv.Membership.Member, field) do
nil -> FieldTypes.label(:string)
attribute -> format_value_type(attribute.type)
end
end
defp format_value_type(type) when is_atom(type) do
type_string = to_string(type)
# Check if it's an Ash type module (e.g., Ash.Type.String or Elixir.Ash.Type.String)
if String.contains?(type_string, "Ash.Type.") do
# Extract the base type name from Ash type modules
# e.g., "Elixir.Ash.Type.String" -> "String" -> :string
type_name =
type_string
|> String.split(".")
|> List.last()
|> String.downcase()
try do
type_atom = String.to_existing_atom(type_name)
FieldTypes.label(type_atom)
rescue
ArgumentError ->
# Fallback if atom doesn't exist
FieldTypes.label(:string)
end
else
# It's already an atom like :string, :boolean, :date
FieldTypes.label(type)
end
end
defp format_value_type(type) do
# Fallback for unknown types
to_string(type)
end
defp normalize_visibility_config(config) when is_map(config) do
@ -197,12 +247,4 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do
defp required?(field) when field in @required_fields, do: true
defp required?(_), do: false
defp format_field_name(field) when is_atom(field) do
field
|> Atom.to_string()
|> String.replace("_", " ")
|> String.split()
|> Enum.map_join(" ", &String.capitalize/1)
end
end