From 8933ad9d14854db6c05f3513a0b01c68fcaedd4e Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 23 Feb 2026 22:11:02 +0100 Subject: [PATCH] Member field settings: required checkbox, line break, toggle fix Index/Form use member_field_required; Required disabled for email and Vereinfacht-required fields with tooltip. Rebuild form with to_form on validate to fix checkbox toggle. Add mt-4 block before Required. --- .../live/member_field_live/form_component.ex | 245 +++++++++--------- .../live/member_field_live/index_component.ex | 43 ++- priv/gettext/de/LC_MESSAGES/default.po | 7 +- priv/gettext/default.pot | 7 +- priv/gettext/en/LC_MESSAGES/default.po | 7 +- 5 files changed, 154 insertions(+), 155 deletions(-) diff --git a/lib/mv_web/live/member_field_live/form_component.ex b/lib/mv_web/live/member_field_live/form_component.ex index 1bba048..eba86d1 100644 --- a/lib/mv_web/live/member_field_live/form_component.ex +++ b/lib/mv_web/live/member_field_live/form_component.ex @@ -16,8 +16,8 @@ defmodule MvWeb.MemberFieldLive.FormComponent do - `on_cancel` - Callback function to call when form is cancelled ## Note - Member fields are technical fields that cannot be changed (name, value_type, description, required). - Only the visibility (show_in_overview) can be modified. + Member fields are technical fields that cannot be changed (name, value_type). + Visibility (show_in_overview) and required flag are stored in Settings and can be modified. """ use MvWeb, :live_component @@ -27,14 +27,13 @@ defmodule MvWeb.MemberFieldLive.FormComponent do alias MvWeb.Helpers.FieldTypeFormatter alias MvWeb.Translations.MemberFields - @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(:vereinfacht_required_field?, vereinfacht_required_field?(assigns)) |> assign(:field_label, MemberFields.label(assigns.member_field)) ~H""" @@ -117,89 +116,64 @@ defmodule MvWeb.MemberFieldLive.FormComponent do -
-
- -
-
- <.input - :if={not @is_email_field?} - field={@form[:description]} - type="text" - label={gettext("Description")} - disabled={@is_email_field?} - readonly={@is_email_field?} - /> - -
-
-
+
+ <.input + :if={not @is_email_field? and not @vereinfacht_required_field?} + field={@form[:required]} + type="checkbox" + label={gettext("Required")} + /> - <.input - field={@form[:show_in_overview]} - type="checkbox" - label={gettext("Show in overview")} - /> + <.input + field={@form[:show_in_overview]} + type="checkbox" + label={gettext("Show in overview")} + /> +
<.button type="button" phx-click="cancel" phx-target={@myself}> @@ -225,24 +199,35 @@ defmodule MvWeb.MemberFieldLive.FormComponent do @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", + # Unchecked checkboxes are not in params; preserve current form value when key is missing + show_in_overview = + if Map.has_key?(member_field_params, "show_in_overview") do TypeParsers.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("required", form.source["required"]) + else + form.source["show_in_overview"] + end + + required = + socket.assigns[:vereinfacht_required_field?] || + if Map.has_key?(member_field_params, "required") do + TypeParsers.parse_boolean(member_field_params["required"]) + else + form.source["required"] + end + + # Merge so we keep name/value_type and have current checkbox state; use as new form source + merged_source = + form.source + |> Map.merge(%{ + "show_in_overview" => show_in_overview, + "required" => required, + "name" => form.source["name"], + "value_type" => form.source["value_type"] + }) updated_form = - form - |> Map.put(:value, updated_params) + to_form(merged_source, as: "member_field") |> Map.put(:errors, []) {:noreply, assign(socket, form: updated_form)} @@ -250,23 +235,36 @@ defmodule MvWeb.MemberFieldLive.FormComponent do @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 = TypeParsers.parse_boolean(member_field_params["show_in_overview"]) + form = socket.assigns.form + # Unchecked checkboxes are not in submit params; use form source when key missing + show_in_overview = + if Map.has_key?(member_field_params, "show_in_overview") do + TypeParsers.parse_boolean(member_field_params["show_in_overview"]) + else + form.source["show_in_overview"] + end + + required = + socket.assigns[:vereinfacht_required_field?] || + if Map.has_key?(member_field_params, "required") do + TypeParsers.parse_boolean(member_field_params["required"]) + else + form.source["required"] + end + field_string = Atom.to_string(socket.assigns.member_field) - # Use atomic action to update only this single field - # This prevents lost updates in concurrent scenarios - case Membership.update_single_member_field_visibility( + case Membership.update_single_member_field( socket.assigns.settings, field: field_string, - show_in_overview: show_in_overview + show_in_overview: show_in_overview, + required: required ) 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, [ @@ -288,16 +286,22 @@ defmodule MvWeb.MemberFieldLive.FormComponent do 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 = VisibilityConfig.normalize(visibility_config) - show_in_overview = Map.get(normalized_config, member_field, true) + required_config = settings.member_field_required || %{} + normalized_visibility = VisibilityConfig.normalize(visibility_config) + normalized_required = VisibilityConfig.normalize(required_config) + show_in_overview = Map.get(normalized_visibility, member_field, true) + vereinfacht_required? = Mv.Config.vereinfacht_configured?() + + # Email always required; Vereinfacht-required fields when integration active; else from settings + required = + member_field == :email || + (vereinfacht_required? && Mv.Constants.vereinfacht_required_field?(member_field)) || + Map.get(normalized_required, member_field, false) - # Create a manual form structure with string keys - # Note: immutable is not included as it's not editable for member fields form_data = %{ "name" => MemberFields.label(member_field), "value_type" => FieldTypeFormatter.format(field_attributes.value_type), - "description" => field_attributes.description || "", - "required" => field_attributes.required, + "required" => required, "show_in_overview" => show_in_overview } @@ -307,24 +311,14 @@ defmodule MvWeb.MemberFieldLive.FormComponent do end defp get_field_attributes(field) when is_atom(field) do - # Get attribute info from Member Resource alias Ash.Resource.Info case Info.attribute(Mv.Membership.Member, field) do nil -> - # Fallback for fields not in resource (shouldn't happen with Constants) - %{ - value_type: :string, - description: nil, - required: field in @required_fields - } + %{value_type: :string} attribute -> - %{ - value_type: attribute.type, - description: nil, - required: not attribute.allow_nil? - } + %{value_type: attribute.type} end end @@ -335,4 +329,9 @@ defmodule MvWeb.MemberFieldLive.FormComponent do defp format_error(error) do inspect(error) end + + defp vereinfacht_required_field?(assigns) do + Mv.Config.vereinfacht_configured?() && + Mv.Constants.vereinfacht_required_field?(assigns.member_field) + end end diff --git a/lib/mv_web/live/member_field_live/index_component.ex b/lib/mv_web/live/member_field_live/index_component.ex index 5204030..db62778 100644 --- a/lib/mv_web/live/member_field_live/index_component.ex +++ b/lib/mv_web/live/member_field_live/index_component.ex @@ -22,7 +22,6 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do assigns = assigns |> assign(:member_fields, get_member_fields_with_visibility(assigns.settings)) - |> assign(:required?, &required?/1) ~H"""
@@ -62,22 +61,15 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do {format_value_type(field_data.field)} - <:col :let={{_field_name, field_data}} label={gettext("Description")}> - {field_data.description || ""} - - <:col :let={{_field_name, field_data}} label={gettext("Required")} class="max-w-[9.375rem] text-center" > - + {gettext("Required")} - + {gettext("Optional")} @@ -173,26 +165,35 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do {:error, _} -> # Return a minimal struct-like map for fallback # This is only used for initial rendering, actual settings will be loaded properly - %{member_field_visibility: %{}} + %{member_field_visibility: %{}, member_field_required: %{}} end end defp get_member_fields_with_visibility(settings) do member_fields = Mv.Constants.member_fields() visibility_config = settings.member_field_visibility || %{} + required_config = settings.member_field_required || %{} + vereinfacht_required? = Mv.Config.vereinfacht_configured?() - # Normalize visibility config keys to atoms - normalized_config = VisibilityConfig.normalize(visibility_config) + normalized_visibility = VisibilityConfig.normalize(visibility_config) + normalized_required = VisibilityConfig.normalize(required_config) Enum.map(member_fields, fn field -> - show_in_overview = Map.get(normalized_config, field, true) + show_in_overview = Map.get(normalized_visibility, field, true) + + # Email always required; Vereinfacht-required fields when integration active; else from settings + required = + field == :email || + (vereinfacht_required? && Mv.Constants.vereinfacht_required_field?(field)) || + Map.get(normalized_required, field, false) + attribute = Info.attribute(Mv.Membership.Member, field) %{ field: field, show_in_overview: show_in_overview, - value_type: (attribute && attribute.type) || :string, - description: nil + required: required, + value_type: (attribute && attribute.type) || :string } end) |> Enum.map(fn field_data -> @@ -206,14 +207,4 @@ defmodule MvWeb.MemberFieldLive.IndexComponent do attribute -> FieldTypeFormatter.format(attribute.type) end end - - # Check if a field is required by checking the actual attribute definition - defp required?(field) when is_atom(field) do - case Info.attribute(Mv.Membership.Member, field) do - nil -> false - attribute -> not attribute.allow_nil? - end - end - - defp required?(_), do: false end diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index d39d86b..c418dca 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -287,8 +287,6 @@ msgstr "Abbrechen" #: lib/mv_web/live/group_live/form.ex #: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/show.ex -#: lib/mv_web/live/member_field_live/form_component.ex -#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/index.html.heex @@ -2911,3 +2909,8 @@ msgstr "Okt." #, elixir-autogen, elixir-format msgid "Sep." msgstr "Sep." + +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format +msgid "Required for Vereinfacht integration and cannot be disabled." +msgstr "Für die Vereinfacht-Integration erforderlich und kann nicht deaktiviert werden." diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index ff466ab..2e7e480 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -288,8 +288,6 @@ msgstr "" #: lib/mv_web/live/group_live/form.ex #: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/show.ex -#: lib/mv_web/live/member_field_live/form_component.ex -#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/index.html.heex @@ -2911,3 +2909,8 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Sep." msgstr "" + +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format +msgid "Required for Vereinfacht integration and cannot be disabled." +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index e5e0181..3c53a7e 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -288,8 +288,6 @@ msgstr "" #: lib/mv_web/live/group_live/form.ex #: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/group_live/show.ex -#: lib/mv_web/live/member_field_live/form_component.ex -#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/membership_fee_type_live/form.ex #: lib/mv_web/live/role_live/form.ex #: lib/mv_web/live/role_live/index.html.heex @@ -2911,3 +2909,8 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Sep." msgstr "" + +#: lib/mv_web/live/member_field_live/form_component.ex +#, elixir-autogen, elixir-format +msgid "Required for Vereinfacht integration and cannot be disabled." +msgstr "Required for Vereinfacht integration and cannot be disabled."