From 6c38d7455f3d936e97efbdddea8745e0aaa66031 Mon Sep 17 00:00:00 2001 From: carla Date: Fri, 12 Dec 2025 14:20:07 +0100 Subject: [PATCH 1/4] chore: remove immutable from custom fields --- lib/membership/custom_field.ex | 9 ++------ .../live/custom_field_live/form_component.ex | 4 ++-- ...49_remove_immutable_from_custom_fields.exs | 21 +++++++++++++++++++ priv/repo/seeds.exs | 12 ----------- 4 files changed, 25 insertions(+), 21 deletions(-) create mode 100644 priv/repo/migrations/20251211172549_remove_immutable_from_custom_fields.exs diff --git a/lib/membership/custom_field.ex b/lib/membership/custom_field.ex index 5b7514c..18b8154 100644 --- a/lib/membership/custom_field.ex +++ b/lib/membership/custom_field.ex @@ -12,7 +12,6 @@ defmodule Mv.Membership.CustomField do - `slug` - URL-friendly, immutable identifier automatically generated from name (e.g., "phone-mobile") - `value_type` - Data type constraint (`:string`, `:integer`, `:boolean`, `:date`, `:email`) - `description` - Optional human-readable description - - `immutable` - If true, custom field values cannot be changed after creation - `required` - If true, all members must have this custom field (future feature) - `show_in_overview` - If true, this custom field will be displayed in the member overview table and can be sorted @@ -60,10 +59,10 @@ defmodule Mv.Membership.CustomField do actions do defaults [:read, :update] - default_accept [:name, :value_type, :description, :immutable, :required, :show_in_overview] + default_accept [:name, :value_type, :description, :required, :show_in_overview] create :create do - accept [:name, :value_type, :description, :immutable, :required, :show_in_overview] + accept [:name, :value_type, :description, :required, :show_in_overview] change Mv.Membership.CustomField.Changes.GenerateSlug validate string_length(:slug, min: 1) end @@ -113,10 +112,6 @@ defmodule Mv.Membership.CustomField do trim?: true ] - attribute :immutable, :boolean, - default: false, - allow_nil?: false - attribute :required, :boolean, default: false, allow_nil?: false diff --git a/lib/mv_web/live/custom_field_live/form_component.ex b/lib/mv_web/live/custom_field_live/form_component.ex index 4fe8579..69eb9e9 100644 --- a/lib/mv_web/live/custom_field_live/form_component.ex +++ b/lib/mv_web/live/custom_field_live/form_component.ex @@ -6,7 +6,7 @@ defmodule MvWeb.CustomFieldLive.FormComponent do - Create new custom field definitions - Edit existing custom fields - Select value type from supported types - - Set immutable and required flags + - Set required flag - Real-time validation ## Props @@ -50,10 +50,10 @@ defmodule MvWeb.CustomFieldLive.FormComponent do label={gettext("Value type")} options={ Ash.Resource.Info.attribute(Mv.Membership.CustomField, :value_type).constraints[:one_of] + |> Enum.map(fn type -> {MvWeb.Translations.FieldTypes.label(type), type} end) } /> <.input field={@form[:description]} type="text" label={gettext("Description")} /> - <.input field={@form[:immutable]} type="checkbox" label={gettext("Immutable")} /> <.input field={@form[:required]} type="checkbox" label={gettext("Required")} /> <.input field={@form[:show_in_overview]} diff --git a/priv/repo/migrations/20251211172549_remove_immutable_from_custom_fields.exs b/priv/repo/migrations/20251211172549_remove_immutable_from_custom_fields.exs new file mode 100644 index 0000000..9d25d49 --- /dev/null +++ b/priv/repo/migrations/20251211172549_remove_immutable_from_custom_fields.exs @@ -0,0 +1,21 @@ +defmodule Mv.Repo.Migrations.RemoveImmutableFromCustomFields do + @moduledoc """ + Removes the immutable column from custom_fields table. + + The immutable field is no longer needed in the custom field definition. + """ + + use Ecto.Migration + + def up do + alter table(:custom_fields) do + remove :immutable + end + end + + def down do + alter table(:custom_fields) do + add :immutable, :boolean, null: false, default: false + end + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index bec9006..10af66b 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -12,28 +12,24 @@ for attrs <- [ name: "String Field", value_type: :string, description: "Example for a field of type string", - immutable: true, required: false }, %{ name: "Date Field", value_type: :date, description: "Example for a field of type date", - immutable: true, required: false }, %{ name: "Boolean Field", value_type: :boolean, description: "Example for a field of type boolean", - immutable: true, required: false }, %{ name: "Email Field", value_type: :email, description: "Example for a field of type email", - immutable: true, required: false }, # Realistic custom fields @@ -41,56 +37,48 @@ for attrs <- [ name: "Membership Number", value_type: :string, description: "Unique membership identification number", - immutable: false, required: false }, %{ name: "Emergency Contact", value_type: :string, description: "Emergency contact person name and phone", - immutable: false, required: false }, %{ name: "T-Shirt Size", value_type: :string, description: "T-Shirt size for events (XS, S, M, L, XL, XXL)", - immutable: false, required: false }, %{ name: "Newsletter Subscription", value_type: :boolean, description: "Whether member wants to receive newsletter", - immutable: false, required: false }, %{ name: "Date of Last Medical Check", value_type: :date, description: "Date of last medical examination", - immutable: false, required: false }, %{ name: "Secondary Email", value_type: :email, description: "Alternative email address", - immutable: false, required: false }, %{ name: "Membership Type", value_type: :string, description: "Type of membership (e.g., Regular, Student, Senior)", - immutable: false, required: false }, %{ name: "Parking Permit", value_type: :boolean, description: "Whether member has parking permit", - immutable: false, required: false } ] do -- 2.47.2 From b0daf8f28c634d80a51e1eee13351a14e78473d4 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 15 Dec 2025 08:44:24 +0100 Subject: [PATCH 2/4] feat: disable email buttons instead hide them --- lib/mv_web/components/core_components.ex | 26 +++++++++++++++++++-- lib/mv_web/live/member_live/index.html.heex | 12 +++++++--- test/mv_web/member_live/index_test.exs | 8 ------- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/lib/mv_web/components/core_components.ex b/lib/mv_web/components/core_components.ex index a23381d..f0a9fdb 100644 --- a/lib/mv_web/components/core_components.ex +++ b/lib/mv_web/components/core_components.ex @@ -95,9 +95,11 @@ defmodule MvWeb.CoreComponents do <.button>Send! <.button phx-click="go" variant="primary">Send! <.button navigate={~p"/"}>Home + <.button disabled={true}>Disabled """ attr :rest, :global, include: ~w(href navigate patch method) attr :variant, :string, values: ~w(primary) + attr :disabled, :boolean, default: false, doc: "Whether the button is disabled" slot :inner_block, required: true def button(%{rest: rest} = assigns) do @@ -105,14 +107,34 @@ defmodule MvWeb.CoreComponents do assigns = assign(assigns, :class, Map.fetch!(variants, assigns[:variant])) if rest[:href] || rest[:navigate] || rest[:patch] do + # For links, we can't use disabled attribute, so we use btn-disabled class + # DaisyUI's btn-disabled provides the same styling as :disabled on buttons + link_class = + if assigns[:disabled], + do: ["btn", assigns.class, "btn-disabled"], + else: ["btn", assigns.class] + + # Prevent interaction when disabled + link_attrs = + if assigns[:disabled] do + Map.merge(rest, %{tabindex: "-1", "aria-disabled": "true"}) + else + rest + end + + assigns = + assigns + |> assign(:link_class, link_class) + |> assign(:link_attrs, link_attrs) + ~H""" - <.link class={["btn", @class]} {@rest}> + <.link class={@link_class} {@link_attrs}> {render_slot(@inner_block)} """ else ~H""" - """ diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index fbeb416..8e8d18b 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -3,23 +3,29 @@ {gettext("Members")} <:actions> <.button - :if={Enum.any?(@members, &MapSet.member?(@selected_members, &1.id))} + class="secondary" id="copy-emails-btn" phx-hook="CopyToClipboard" phx-click="copy_emails" + disabled={not Enum.any?(@members, &MapSet.member?(@selected_members, &1.id))} aria-label={gettext("Copy email addresses of selected members")} > <.icon name="hero-clipboard-document" /> - {gettext("Copy emails")} ({Enum.count(@members, &MapSet.member?(@selected_members, &1.id))}) + {gettext("Copy email addresses")} ({Enum.count( + @members, + &MapSet.member?(@selected_members, &1.id) + )}) <.button - :if={Enum.any?(@members, &MapSet.member?(@selected_members, &1.id))} + class="secondary" + id="open-email-btn" href={ "mailto:?bcc=" <> (MvWeb.MemberLive.Index.format_selected_member_emails(@members, @selected_members) |> Enum.join(", ") |> URI.encode()) } + disabled={not Enum.any?(@members, &MapSet.member?(@selected_members, &1.id))} aria-label={gettext("Open email program with BCC recipients")} > <.icon name="hero-envelope" /> diff --git a/test/mv_web/member_live/index_test.exs b/test/mv_web/member_live/index_test.exs index 30b61c7..5b826bd 100644 --- a/test/mv_web/member_live/index_test.exs +++ b/test/mv_web/member_live/index_test.exs @@ -410,14 +410,6 @@ defmodule MvWeb.MemberLive.IndexTest do assert render(view) =~ "1" end - test "copy button is not visible when no members are selected", %{conn: conn} do - conn = conn_with_oidc_user(conn) - {:ok, view, _html} = live(conn, "/members") - - # Ensure no members are selected (default state) - refute has_element?(view, "#copy-emails-btn") - end - test "copy button is visible when members are selected", %{ conn: conn, member1: member1 -- 2.47.2 From 7d1a4fa166cd2a909450cdd249b4f01187de8035 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 15 Dec 2025 08:50:05 +0100 Subject: [PATCH 3/4] chore: change payment filter text --- .../components/payment_filter_component.ex | 4 +- .../live/custom_field_live/index_component.ex | 4 +- priv/gettext/de/LC_MESSAGES/default.po | 44 ++++++++++++------- priv/gettext/default.pot | 27 +++++------- priv/gettext/en/LC_MESSAGES/default.po | 42 +++++++++++------- 5 files changed, 68 insertions(+), 53 deletions(-) diff --git a/lib/mv_web/live/components/payment_filter_component.ex b/lib/mv_web/live/components/payment_filter_component.ex index 47556dd..1ba9d8b 100644 --- a/lib/mv_web/live/components/payment_filter_component.ex +++ b/lib/mv_web/live/components/payment_filter_component.ex @@ -77,7 +77,7 @@ defmodule MvWeb.Components.PaymentFilterComponent do phx-target={@myself} > <.icon name="hero-users" class="h-4 w-4" /> - {gettext("All")} + {gettext("All payment statuses")}
  • @@ -140,7 +140,7 @@ defmodule MvWeb.Components.PaymentFilterComponent do defp parse_filter(_), do: nil # Get display label for current filter - defp filter_label(nil), do: gettext("All") + defp filter_label(nil), do: gettext("All payment statuses") defp filter_label(:paid), do: gettext("Paid") defp filter_label(:not_paid), do: gettext("Not paid") end diff --git a/lib/mv_web/live/custom_field_live/index_component.ex b/lib/mv_web/live/custom_field_live/index_component.ex index 8f63bf8..ccae3c6 100644 --- a/lib/mv_web/live/custom_field_live/index_component.ex +++ b/lib/mv_web/live/custom_field_live/index_component.ex @@ -5,7 +5,7 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do ## Features - List all custom fields - Display type information (name, value type, description) - - Show immutable and required flags + - Show required flag - Create new custom fields - Edit existing custom fields - Delete custom fields with confirmation (cascades to all custom field values) @@ -30,7 +30,7 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do phx-click="new_custom_field" phx-target={@myself} > - <.icon name="hero-plus" /> {gettext("New Custom field")} + <.icon name="hero-plus" /> {gettext("New Custom Field")} diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 25f685d..81653a4 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -282,11 +282,6 @@ msgstr "Benutzer*in bearbeiten" msgid "Enabled" msgstr "Aktiviert" -#: lib/mv_web/live/custom_field_live/form_component.ex -#, elixir-autogen, elixir-format -msgid "Immutable" -msgstr "Unveränderlich" - #: lib/mv_web/components/layouts/navbar.ex #, elixir-autogen, elixir-format msgid "Logout" @@ -760,11 +755,6 @@ msgstr[1] "%{count} E-Mail-Adressen in die Zwischenablage kopiert" msgid "Copy email addresses of selected members" msgstr "E-Mail-Adressen der ausgewählten Mitglieder kopieren" -#: lib/mv_web/live/member_live/index.html.heex -#, elixir-autogen, elixir-format -msgid "Copy emails" -msgstr "E-Mails kopieren" - #: lib/mv_web/live/member_live/index.ex #, elixir-autogen, elixir-format msgid "No email addresses found" @@ -796,7 +786,6 @@ msgid "This field cannot be empty" msgstr "Dieses Feld darf nicht leer bleiben" #: lib/mv_web/components/core_components.ex -#: lib/mv_web/live/components/payment_filter_component.ex #, elixir-autogen, elixir-format msgid "All" msgstr "Alle" @@ -1389,14 +1378,10 @@ msgid "Failed to delete custom field: %{error}" msgstr "Konnte Feld nicht löschen: %{error}" #: lib/mv_web/live/custom_field_live/form_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "New Custom Field" -msgstr "Benutzerdefiniertes Feld speichern" - #: lib/mv_web/live/custom_field_live/index_component.ex #, elixir-autogen, elixir-format, fuzzy -msgid "New Custom field" -msgstr "Benutzerdefiniertes Feld speichern" +msgid "New Custom Field" +msgstr "Neues Benutzerdefiniertes Feld" #: lib/mv_web/live/global_settings_live.ex #, elixir-autogen, elixir-format @@ -1438,6 +1423,16 @@ msgstr "Textfeld" msgid "Yes/No-Selection" msgstr "Ja/Nein-Auswahl" +#: lib/mv_web/live/components/payment_filter_component.ex +#, elixir-autogen, elixir-format +msgid "All payment statuses" +msgstr "Jeder Zahlungs-Zustand" + +#: lib/mv_web/live/member_live/index.html.heex +#, elixir-autogen, elixir-format, fuzzy +msgid "Copy email addresses" +msgstr "E-Mail-Adressen kopieren" + #~ #: lib/mv_web/live/custom_field_live/show.ex #~ #, elixir-autogen, elixir-format #~ msgid "Auto-generated identifier (immutable)" @@ -1449,6 +1444,11 @@ msgstr "Ja/Nein-Auswahl" #~ msgid "Birth Date" #~ msgstr "Geburtsdatum" +#~ #: lib/mv_web/live/member_live/index.html.heex +#~ #, elixir-autogen, elixir-format +#~ msgid "Copy emails" +#~ msgstr "E-Mails kopieren" + #~ #: lib/mv_web/live/member_live/form.ex #~ #: lib/mv_web/live/member_live/show.ex #~ #, elixir-autogen, elixir-format @@ -1471,6 +1471,16 @@ msgstr "Ja/Nein-Auswahl" #~ msgid "Id" #~ msgstr "ID" +#~ #: lib/mv_web/live/custom_field_live/form_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Immutable" +#~ msgstr "Unveränderlich" + +#~ #: lib/mv_web/live/custom_field_live/index_component.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "New Custom field" +#~ msgstr "Benutzerdefiniertes Feld speichern" + #~ #: lib/mv_web/live/user_live/form.ex #~ #: lib/mv_web/live/user_live/show.ex #~ #, elixir-autogen, elixir-format diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index a7ab36b..451e2b5 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -283,11 +283,6 @@ msgstr "" msgid "Enabled" msgstr "" -#: lib/mv_web/live/custom_field_live/form_component.ex -#, elixir-autogen, elixir-format -msgid "Immutable" -msgstr "" - #: lib/mv_web/components/layouts/navbar.ex #, elixir-autogen, elixir-format msgid "Logout" @@ -761,11 +756,6 @@ msgstr[1] "" msgid "Copy email addresses of selected members" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex -#, elixir-autogen, elixir-format -msgid "Copy emails" -msgstr "" - #: lib/mv_web/live/member_live/index.ex #, elixir-autogen, elixir-format msgid "No email addresses found" @@ -797,7 +787,6 @@ msgid "This field cannot be empty" msgstr "" #: lib/mv_web/components/core_components.ex -#: lib/mv_web/live/components/payment_filter_component.ex #, elixir-autogen, elixir-format msgid "All" msgstr "" @@ -1390,13 +1379,9 @@ msgid "Failed to delete custom field: %{error}" msgstr "" #: lib/mv_web/live/custom_field_live/form_component.ex -#, elixir-autogen, elixir-format -msgid "New Custom Field" -msgstr "" - #: lib/mv_web/live/custom_field_live/index_component.ex #, elixir-autogen, elixir-format -msgid "New Custom field" +msgid "New Custom Field" msgstr "" #: lib/mv_web/live/global_settings_live.ex @@ -1438,3 +1423,13 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Yes/No-Selection" msgstr "" + +#: lib/mv_web/live/components/payment_filter_component.ex +#, elixir-autogen, elixir-format +msgid "All payment statuses" +msgstr "" + +#: lib/mv_web/live/member_live/index.html.heex +#, elixir-autogen, elixir-format +msgid "Copy email addresses" +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index e2a1876..5995656 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -283,11 +283,6 @@ msgstr "" msgid "Enabled" msgstr "" -#: lib/mv_web/live/custom_field_live/form_component.ex -#, elixir-autogen, elixir-format -msgid "Immutable" -msgstr "" - #: lib/mv_web/components/layouts/navbar.ex #, elixir-autogen, elixir-format msgid "Logout" @@ -761,11 +756,6 @@ msgstr[1] "" msgid "Copy email addresses of selected members" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex -#, elixir-autogen, elixir-format -msgid "Copy emails" -msgstr "" - #: lib/mv_web/live/member_live/index.ex #, elixir-autogen, elixir-format msgid "No email addresses found" @@ -797,7 +787,6 @@ msgid "This field cannot be empty" msgstr "" #: lib/mv_web/components/core_components.ex -#: lib/mv_web/live/components/payment_filter_component.ex #, elixir-autogen, elixir-format msgid "All" msgstr "" @@ -1390,13 +1379,9 @@ msgid "Failed to delete custom field: %{error}" msgstr "" #: lib/mv_web/live/custom_field_live/form_component.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "New Custom Field" -msgstr "" - #: lib/mv_web/live/custom_field_live/index_component.ex #, elixir-autogen, elixir-format, fuzzy -msgid "New Custom field" +msgid "New Custom Field" msgstr "" #: lib/mv_web/live/global_settings_live.ex @@ -1439,6 +1424,16 @@ msgstr "" msgid "Yes/No-Selection" msgstr "" +#: lib/mv_web/live/components/payment_filter_component.ex +#, elixir-autogen, elixir-format +msgid "All payment statuses" +msgstr "" + +#: lib/mv_web/live/member_live/index.html.heex +#, elixir-autogen, elixir-format, fuzzy +msgid "Copy email addresses" +msgstr "" + #~ #: lib/mv_web/live/custom_field_live/show.ex #~ #, elixir-autogen, elixir-format #~ msgid "Auto-generated identifier (immutable)" @@ -1450,6 +1445,11 @@ msgstr "" #~ msgid "Birth Date" #~ msgstr "" +#~ #: lib/mv_web/live/member_live/index.html.heex +#~ #, elixir-autogen, elixir-format +#~ msgid "Copy emails" +#~ msgstr "" + #~ #: lib/mv_web/live/member_live/form.ex #~ #: lib/mv_web/live/member_live/show.ex #~ #, elixir-autogen, elixir-format @@ -1471,6 +1471,16 @@ msgstr "" #~ msgid "Id" #~ msgstr "" +#~ #: lib/mv_web/live/custom_field_live/form_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Immutable" +#~ msgstr "" + +#~ #: lib/mv_web/live/custom_field_live/index_component.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "New Custom field" +#~ msgstr "" + #~ #: lib/mv_web/live/user_live/show.ex #~ #, elixir-autogen, elixir-format, fuzzy #~ msgid "Not set" -- 2.47.2 From 2fa8f3eb2c783f1ef4bd4199d10a32b95eee2a87 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 15 Dec 2025 08:50:42 +0100 Subject: [PATCH 4/4] fix: update clubname on save --- lib/mv_web/components/layouts.ex | 6 +++++- lib/mv_web/components/layouts/navbar.ex | 9 ++++++--- lib/mv_web/live/global_settings_live.ex | 9 ++++++--- mix.lock | 8 ++++---- .../index_custom_fields_accessibility_test.exs | 9 ++++++++- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/lib/mv_web/components/layouts.ex b/lib/mv_web/components/layouts.ex index 487a01f..86090a8 100644 --- a/lib/mv_web/components/layouts.ex +++ b/lib/mv_web/components/layouts.ex @@ -36,12 +36,16 @@ defmodule MvWeb.Layouts do default: nil, doc: "the current [scope](https://hexdocs.pm/phoenix/scopes.html)" + attr :club_name, :string, + default: nil, + doc: "optional club name to pass to navbar" + slot :inner_block, required: true def app(assigns) do ~H""" <%= if @current_user do %> - <.navbar current_user={@current_user} /> + <.navbar current_user={@current_user} club_name={@club_name} /> <% end %>
    diff --git a/lib/mv_web/components/layouts/navbar.ex b/lib/mv_web/components/layouts/navbar.ex index 4246c99..1ff589b 100644 --- a/lib/mv_web/components/layouts/navbar.ex +++ b/lib/mv_web/components/layouts/navbar.ex @@ -12,15 +12,18 @@ defmodule MvWeb.Layouts.Navbar do required: true, doc: "The current user - navbar is only shown when user is present" - def navbar(assigns) do - club_name = get_club_name() + attr :club_name, :string, + default: nil, + doc: "Optional club name - if not provided, will be loaded from database" + def navbar(assigns) do + club_name = assigns[:club_name] || get_club_name() assigns = assign(assigns, :club_name, club_name) ~H"""