feat: adds form for member fields
This commit is contained in:
parent
18c082a893
commit
5fa0b48acc
2 changed files with 592 additions and 105 deletions
445
lib/mv_web/live/member_field_live/form_component.ex
Normal file
445
lib/mv_web/live/member_field_live/form_component.ex
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
defmodule MvWeb.MemberFieldLive.FormComponent do
|
||||
@moduledoc """
|
||||
LiveComponent form for editing member field properties (embedded in settings).
|
||||
|
||||
## Features
|
||||
- Edit member field properties (name, value type, description, immutable, required, show in overview)
|
||||
- Display member field information from Member Resource
|
||||
- Restrict editing for email field (only show_in_overview can be changed)
|
||||
- Real-time validation
|
||||
- Updates Settings.member_field_visibility
|
||||
|
||||
## Props
|
||||
- `member_field` - The member field atom to edit (e.g., :first_name, :email)
|
||||
- `settings` - The current Settings resource
|
||||
- `on_save` - Callback function to call when form is saved
|
||||
- `on_cancel` - Callback function to call when form is cancelled
|
||||
"""
|
||||
use MvWeb, :live_component
|
||||
|
||||
alias Mv.Membership
|
||||
alias MvWeb.Translations.MemberFields
|
||||
alias MvWeb.Translations.FieldTypes
|
||||
|
||||
@required_fields [:first_name, :last_name, :email]
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:field_attributes, get_field_attributes(assigns.member_field))
|
||||
|> assign(:is_email_field?, assigns.member_field == :email)
|
||||
|> assign(:field_label, MemberFields.label(assigns.member_field))
|
||||
|
||||
~H"""
|
||||
<div id={@id} class="mb-8 border shadow-xl card border-base-300">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<.button
|
||||
type="button"
|
||||
phx-click="cancel"
|
||||
phx-target={@myself}
|
||||
aria-label={gettext("Back to member field overview")}
|
||||
>
|
||||
<.icon name="hero-arrow-left" class="w-4 h-4" />
|
||||
</.button>
|
||||
<h3 class="card-title">
|
||||
{gettext("Edit Field: %{field}", field: @field_label)}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<.form
|
||||
for={@form}
|
||||
id={@id <> "-form"}
|
||||
phx-change="validate"
|
||||
phx-submit="save"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<div
|
||||
class="tooltip tooltip-right"
|
||||
data-tip={gettext("This is a technical field and cannot be changed")}
|
||||
aria-label={gettext("This is a technical field and cannot be changed")}
|
||||
>
|
||||
<fieldset class="mb-2 fieldset">
|
||||
<label>
|
||||
<span class="mb-1 label flex items-center gap-2">
|
||||
{gettext("Name")}
|
||||
<.icon
|
||||
name="hero-information-circle"
|
||||
class="w-4 h-4 text-base-content/60 cursor-help"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
name={@form[:name].name}
|
||||
id={@form[:name].id}
|
||||
value={@field_label}
|
||||
disabled
|
||||
readonly
|
||||
class="w-full input"
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="tooltip tooltip-right"
|
||||
data-tip={gettext("This is a technical field and cannot be changed")}
|
||||
aria-label={gettext("This is a technical field and cannot be changed")}
|
||||
>
|
||||
<fieldset class="mb-2 fieldset">
|
||||
<label>
|
||||
<span class="mb-1 label flex items-center gap-2">
|
||||
{gettext("Value type")}
|
||||
<.icon
|
||||
name="hero-information-circle"
|
||||
class="w-4 h-4 text-base-content/60 cursor-help"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
name={@form[:value_type].name}
|
||||
id={@form[:value_type].id}
|
||||
value={format_value_type(@field_attributes.value_type)}
|
||||
disabled
|
||||
readonly
|
||||
class="w-full input"
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:if={@is_email_field?}
|
||||
class="tooltip tooltip-right"
|
||||
data-tip={gettext("This is a technical field and cannot be changed")}
|
||||
aria-label={gettext("This is a technical field and cannot be changed")}
|
||||
>
|
||||
<fieldset class="mb-2 fieldset">
|
||||
<label>
|
||||
<span class="mb-1 label flex items-center gap-2">
|
||||
{gettext("Description")}
|
||||
<.icon
|
||||
name="hero-information-circle"
|
||||
class="w-4 h-4 text-base-content/60 cursor-help"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
name={@form[:description].name}
|
||||
id={@form[:description].id}
|
||||
value={@form[:description].value}
|
||||
disabled
|
||||
readonly
|
||||
class="w-full input"
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
<.input
|
||||
:if={not @is_email_field?}
|
||||
field={@form[:description]}
|
||||
type="text"
|
||||
label={gettext("Description")}
|
||||
disabled={@is_email_field?}
|
||||
readonly={@is_email_field?}
|
||||
/>
|
||||
|
||||
<div
|
||||
:if={@is_email_field?}
|
||||
class="tooltip tooltip-right"
|
||||
data-tip={gettext("This is a technical field and cannot be changed")}
|
||||
aria-label={gettext("This is a technical field and cannot be changed")}
|
||||
>
|
||||
<fieldset class="mb-2 fieldset">
|
||||
<label>
|
||||
<input type="hidden" name={@form[:immutable].name} value="false" disabled />
|
||||
<span class="label flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name={@form[:immutable].name}
|
||||
id={@form[:immutable].id}
|
||||
value="true"
|
||||
checked={@form[:immutable].value}
|
||||
disabled
|
||||
readonly
|
||||
class="checkbox checkbox-sm"
|
||||
/>
|
||||
<span class="flex items-center gap-2">
|
||||
{gettext("Immutable")}
|
||||
<.icon
|
||||
name="hero-information-circle"
|
||||
class="w-4 h-4 text-base-content/60 cursor-help"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
<.input
|
||||
:if={not @is_email_field?}
|
||||
field={@form[:immutable]}
|
||||
type="checkbox"
|
||||
label={gettext("Immutable")}
|
||||
disabled={@is_email_field?}
|
||||
readonly={@is_email_field?}
|
||||
/>
|
||||
|
||||
<div
|
||||
:if={@is_email_field?}
|
||||
class="tooltip tooltip-right"
|
||||
data-tip={gettext("This is a technical field and cannot be changed")}
|
||||
aria-label={gettext("This is a technical field and cannot be changed")}
|
||||
>
|
||||
<fieldset class="mb-2 fieldset">
|
||||
<label>
|
||||
<input type="hidden" name={@form[:required].name} value="false" disabled />
|
||||
<span class="label flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name={@form[:required].name}
|
||||
id={@form[:required].id}
|
||||
value="true"
|
||||
checked={@form[:required].value}
|
||||
disabled
|
||||
readonly
|
||||
class="checkbox checkbox-sm"
|
||||
/>
|
||||
<span class="flex items-center gap-2">
|
||||
{gettext("Required")}
|
||||
<.icon
|
||||
name="hero-information-circle"
|
||||
class="w-4 h-4 text-base-content/60 cursor-help"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
<.input
|
||||
:if={not @is_email_field?}
|
||||
field={@form[:required]}
|
||||
type="checkbox"
|
||||
label={gettext("Required")}
|
||||
disabled={@is_email_field?}
|
||||
readonly={@is_email_field?}
|
||||
/>
|
||||
|
||||
<.input
|
||||
field={@form[:show_in_overview]}
|
||||
type="checkbox"
|
||||
label={gettext("Show in overview")}
|
||||
/>
|
||||
|
||||
<div class="justify-end mt-4 card-actions">
|
||||
<.button type="button" phx-click="cancel" phx-target={@myself}>
|
||||
{gettext("Cancel")}
|
||||
</.button>
|
||||
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
||||
{gettext("Save Field")}
|
||||
</.button>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign_form()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"member_field" => member_field_params}, socket) do
|
||||
# For member fields, we only validate show_in_overview
|
||||
# Other fields are read-only or derived from the Member Resource
|
||||
form = socket.assigns.form
|
||||
|
||||
updated_params =
|
||||
member_field_params
|
||||
|> Map.put("show_in_overview", parse_boolean(member_field_params["show_in_overview"]))
|
||||
|> Map.put("name", form.source["name"])
|
||||
|> Map.put("value_type", form.source["value_type"])
|
||||
|> Map.put("description", form.source["description"])
|
||||
|> Map.put("immutable", form.source["immutable"])
|
||||
|> Map.put("required", form.source["required"])
|
||||
|
||||
updated_form =
|
||||
form
|
||||
|> Map.put(:value, updated_params)
|
||||
|> Map.put(:errors, [])
|
||||
|
||||
{:noreply, assign(socket, form: updated_form)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("save", %{"member_field" => member_field_params}, socket) do
|
||||
# Only show_in_overview can be changed for member fields
|
||||
show_in_overview = parse_boolean(member_field_params["show_in_overview"])
|
||||
|
||||
# Get current visibility config and update only the current field
|
||||
current_visibility = socket.assigns.settings.member_field_visibility || %{}
|
||||
field_string = Atom.to_string(socket.assigns.member_field)
|
||||
|
||||
# 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)
|
||||
|
||||
# Update the specific field
|
||||
updated_visibility = Map.put(normalized_visibility, field_string, show_in_overview)
|
||||
|
||||
# Update settings with new visibility
|
||||
case Membership.update_member_field_visibility(
|
||||
socket.assigns.settings,
|
||||
updated_visibility
|
||||
) do
|
||||
{:ok, _updated_settings} ->
|
||||
socket.assigns.on_save.(socket.assigns.member_field, "update")
|
||||
{:noreply, socket}
|
||||
|
||||
{:error, error} ->
|
||||
# Add error to form
|
||||
form =
|
||||
socket.assigns.form
|
||||
|> Map.put(:errors, [
|
||||
%{field: :show_in_overview, message: format_error(error)}
|
||||
])
|
||||
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("cancel", _params, socket) do
|
||||
socket.assigns.on_cancel.()
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
# Helper functions
|
||||
|
||||
defp assign_form(%{assigns: %{member_field: member_field, settings: settings}} = socket) do
|
||||
field_attributes = get_field_attributes(member_field)
|
||||
visibility_config = settings.member_field_visibility || %{}
|
||||
normalized_config = normalize_visibility_config(visibility_config)
|
||||
show_in_overview = Map.get(normalized_config, member_field, true)
|
||||
|
||||
# Create a manual form structure with string keys
|
||||
form_data = %{
|
||||
"name" => MemberFields.label(member_field),
|
||||
"value_type" => format_value_type(field_attributes.value_type),
|
||||
"description" => field_attributes.description || "",
|
||||
"immutable" => field_attributes.immutable,
|
||||
"required" => field_attributes.required,
|
||||
"show_in_overview" => show_in_overview
|
||||
}
|
||||
|
||||
form = to_form(form_data, as: "member_field")
|
||||
|
||||
assign(socket, form: form)
|
||||
end
|
||||
|
||||
defp get_field_attributes(field) when is_atom(field) do
|
||||
# Get attribute info from Member Resource
|
||||
case Ash.Resource.Info.attribute(Mv.Membership.Member, field) do
|
||||
nil ->
|
||||
# Fallback for fields not in resource (shouldn't happen with Constants)
|
||||
%{
|
||||
value_type: :string,
|
||||
description: nil,
|
||||
immutable: field == :email,
|
||||
required: field in @required_fields
|
||||
}
|
||||
|
||||
attribute ->
|
||||
%{
|
||||
value_type: attribute.type,
|
||||
description: nil,
|
||||
immutable: field == :email,
|
||||
required: not attribute.allow_nil?
|
||||
}
|
||||
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
|
||||
Enum.reduce(config, %{}, fn
|
||||
{key, value}, acc when is_atom(key) ->
|
||||
Map.put(acc, key, value)
|
||||
|
||||
{key, value}, acc when is_binary(key) ->
|
||||
try do
|
||||
atom_key = String.to_existing_atom(key)
|
||||
Map.put(acc, atom_key, value)
|
||||
rescue
|
||||
ArgumentError ->
|
||||
acc
|
||||
end
|
||||
|
||||
_, acc ->
|
||||
acc
|
||||
end)
|
||||
end
|
||||
|
||||
defp normalize_visibility_config(_), do: %{}
|
||||
|
||||
defp parse_boolean(value) when is_boolean(value), do: value
|
||||
defp parse_boolean("true"), do: true
|
||||
defp parse_boolean("false"), do: false
|
||||
defp parse_boolean(1), do: true
|
||||
defp parse_boolean(0), do: false
|
||||
defp parse_boolean(_), do: false
|
||||
|
||||
defp format_error(%Ash.Error.Invalid{} = error) do
|
||||
Ash.ErrorKind.message(error)
|
||||
end
|
||||
|
||||
defp format_error(error) do
|
||||
inspect(error)
|
||||
end
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue