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/components/core_components.ex b/lib/mv_web/components/core_components.ex index a23381d..a1020ef 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,37 @@ 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 + # Remove navigation attributes to prevent "Open in new tab", "Copy link" etc. + link_attrs = + if assigns[:disabled] do + rest + |> Map.drop([:href, :navigate, :patch]) + |> Map.merge(%{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/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 6aee397..c2e28d6 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"""
diff --git a/lib/mv_web/live/custom_field_value_live/form.ex b/lib/mv_web/live/custom_field_value_live/form.ex index 4a7b02d..9663927 100644 --- a/lib/mv_web/live/custom_field_value_live/form.ex +++ b/lib/mv_web/live/custom_field_value_live/form.ex @@ -72,7 +72,7 @@ defmodule MvWeb.CustomFieldValueLive.Form do <% end %> <.button phx-disable-with={gettext("Saving...")} variant="primary"> - {gettext("Save Custom field value")} + {gettext("Save Custom Field Value")} <.button navigate={return_path(@return_to, @custom_field_value)}>{gettext("Cancel")} diff --git a/lib/mv_web/live/global_settings_live.ex b/lib/mv_web/live/global_settings_live.ex index 0b3ec1c..9bce04b 100644 --- a/lib/mv_web/live/global_settings_live.ex +++ b/lib/mv_web/live/global_settings_live.ex @@ -37,7 +37,7 @@ defmodule MvWeb.GlobalSettingsLive do @impl true def render(assigns) do ~H""" - + <.header> {gettext("Settings")} <:subtitle> @@ -80,10 +80,13 @@ defmodule MvWeb.GlobalSettingsLive do @impl true def handle_event("save", %{"setting" => setting_params}, socket) do case AshPhoenix.Form.submit(socket.assigns.form, params: setting_params) do - {:ok, updated_settings} -> + {:ok, _updated_settings} -> + # Reload settings from database to ensure all dependent data is updated + {:ok, fresh_settings} = Membership.get_settings() + socket = socket - |> assign(:settings, updated_settings) + |> assign(:settings, fresh_settings) |> put_flash(:info, gettext("Settings updated successfully")) |> assign_form() diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 8857298..25c23f9 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -145,7 +145,10 @@ defmodule MvWeb.MemberLive.Index do MapSet.put(socket.assigns.selected_members, id) end - {:noreply, assign(socket, :selected_members, selected)} + {:noreply, + socket + |> assign(:selected_members, selected) + |> update_selection_assigns()} end @impl true @@ -159,7 +162,10 @@ defmodule MvWeb.MemberLive.Index do all_ids end - {:noreply, assign(socket, :selected_members, selected)} + {:noreply, + socket + |> assign(:selected_members, selected) + |> update_selection_assigns()} end @impl true @@ -238,6 +244,7 @@ defmodule MvWeb.MemberLive.Index do socket |> assign(:query, q) |> load_members() + |> update_selection_assigns() existing_field_query = socket.assigns.sort_field existing_sort_query = socket.assigns.sort_order @@ -263,6 +270,7 @@ defmodule MvWeb.MemberLive.Index do socket |> assign(:paid_filter, filter) |> load_members() + |> update_selection_assigns() # Build the URL with all params including new filter query_params = @@ -309,6 +317,7 @@ defmodule MvWeb.MemberLive.Index do |> assign(:visible_custom_field_ids, extract_custom_field_ids(visible_custom_fields)) |> load_members() |> prepare_dynamic_cols() + |> update_selection_assigns() |> push_field_selection_url() {:noreply, socket} @@ -338,6 +347,7 @@ defmodule MvWeb.MemberLive.Index do |> assign(:visible_custom_field_ids, extract_custom_field_ids(visible_custom_fields)) |> load_members() |> prepare_dynamic_cols() + |> update_selection_assigns() |> push_field_selection_url() {:noreply, socket} @@ -389,6 +399,7 @@ defmodule MvWeb.MemberLive.Index do |> assign(:visible_custom_field_ids, extract_custom_field_ids(visible_custom_fields)) |> load_members() |> prepare_dynamic_cols() + |> update_selection_assigns() {:noreply, socket} end @@ -1112,4 +1123,34 @@ defmodule MvWeb.MemberLive.Index do # Public helper function to format dates for use in templates def format_date(date), do: DateFormatter.format_date(date) + + # Updates selection-related assigns (selected_count, any_selected?, mailto_bcc) + # to avoid recalculating Enum.any? and Enum.count multiple times in templates. + # + # Note: Mailto URLs have length limits that vary by email client. + # For large selections, consider using export functionality instead. + defp update_selection_assigns(socket) do + members = socket.assigns.members + selected_members = socket.assigns.selected_members + + selected_count = + Enum.count(members, &MapSet.member?(selected_members, &1.id)) + + any_selected? = + Enum.any?(members, &MapSet.member?(selected_members, &1.id)) + + mailto_bcc = + if any_selected? do + format_selected_member_emails(members, selected_members) + |> Enum.join(", ") + |> URI.encode_www_form() + else + "" + end + + socket + |> assign(:selected_count, selected_count) + |> assign(:any_selected?, any_selected?) + |> assign(:mailto_bcc, mailto_bcc) + end end diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index fbeb416..13c4367 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -3,23 +3,21 @@ {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 @any_selected?} 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")} ({@selected_count}) <.button - :if={Enum.any?(@members, &MapSet.member?(@selected_members, &1.id))} - href={ - "mailto:?bcc=" <> - (MvWeb.MemberLive.Index.format_selected_member_emails(@members, @selected_members) - |> Enum.join(", ") - |> URI.encode()) - } + class="secondary" + id="open-email-btn" + href={"mailto:?bcc=" <> @mailto_bcc} + disabled={not @any_selected?} aria-label={gettext("Open email program with BCC recipients")} > <.icon name="hero-envelope" /> diff --git a/mix.lock b/mix.lock index 44dffbf..1dd3d48 100644 --- a/mix.lock +++ b/mix.lock @@ -26,7 +26,7 @@ "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, "ex_phone_number": {:hex, :ex_phone_number, "0.4.8", "c1c5e6f0673822a2a7b439b43af7d3eb1a5c19ae582b772b8b8d12625dd51ec1", [:mix], [{:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}], "hexpm", "43e2357c6b8cfe556bcd417f4ce9aaef267a786e31a2938902daaa0d36f69757"}, - "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, + "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, "fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"}, @@ -39,7 +39,7 @@ "iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"}, - "jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"}, + "jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [:mix, :rebar3], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"}, "lazy_html": {:hex, :lazy_html, "0.1.8", "677a8642e644eef8de98f3040e2520d42d0f0f8bd6c5cd49db36504e34dffe91", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "0d8167d930b704feb94b41414ca7f5779dff9bca7fcf619fcef18de138f08736"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, "live_debugger": {:hex, :live_debugger, "0.5.0", "95e0f7727d61010f7e9053923fb2a9416904a7533c2dfb36120e7684cba4c0af", [:mix], [{:igniter, ">= 0.5.40 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.8 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "73ebe95118d22aa402675f677abd731cb16b136d1b6ae5f4010441fb50753b14"}, @@ -80,7 +80,7 @@ "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, - "thousand_island": {:hex, :thousand_island, "1.4.2", "735fa783005d1703359bbd2d3a5a3a398075ba4456e5afe3c5b7cf4666303d36", [], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1c7637f16558fc1c35746d5ee0e83b18b8e59e18d28affd1f2fa1645f8bc7473"}, + "thousand_island": {:hex, :thousand_island, "1.4.2", "735fa783005d1703359bbd2d3a5a3a398075ba4456e5afe3c5b7cf4666303d36", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1c7637f16558fc1c35746d5ee0e83b18b8e59e18d28affd1f2fa1645f8bc7473"}, "tidewave": {:hex, :tidewave, "0.5.2", "f549acffe9daeed8b6b547c232c60de987770da7f827f9b3300140dfc465b102", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "34ab3ffee7e402f05cd1eae68d0e77ed0e0d1925677971ef83634247553e8afd"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 963f7fe..7020354 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" @@ -612,16 +607,6 @@ msgstr "Benutzerdefinierter Feldwert erfolgreich %{action}" msgid "Please select a custom field first" msgstr "Bitte wähle zuerst ein Benutzerdefiniertes Feld" -#: lib/mv_web/live/custom_field_live/form_component.ex -#, elixir-autogen, elixir-format -msgid "Save Custom field" -msgstr "Benutzerdefiniertes Feld speichern" - -#: lib/mv_web/live/custom_field_value_live/form.ex -#, elixir-autogen, elixir-format -msgid "Save Custom field value" -msgstr "Benutzerdefinierten Feldwert speichern" - #: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/member_live/show.ex @@ -760,11 +745,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 +776,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" @@ -1302,14 +1281,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 @@ -1427,6 +1402,31 @@ msgstr "Jährliches Intervall – Beitrittszeitraum nicht einbezogen" msgid "Yearly Interval - Joining Cycle Included" msgstr "Jährliches Intervall – Beitrittszeitraum einbezogen" +#: 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/form_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Save Custom Field" +msgstr "Benutzerdefiniertes Feld speichern" + +#: lib/mv_web/live/custom_field_value_live/form.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Save Custom Field Value" +msgstr "Benutzerdefinierten Feldwert speichern" + +#~ #: lib/mv_web/live/custom_field_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Auto-generated identifier (immutable)" +#~ msgstr "Automatisch generierter Bezeichner (unveränderlich)" + #~ #: lib/mv_web/live/contribution_settings_live.ex #~ #, elixir-autogen, elixir-format #~ msgid "Configure global settings for membership contributions." @@ -1443,6 +1443,17 @@ msgstr "Jährliches Intervall – Beitrittszeitraum einbezogen" #~ msgid "Contribution start" #~ msgstr "Beitragsbeginn" +#~ #: 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 +#~ msgid "Custom Field Values" +#~ msgstr "Benutzerdefinierte Feldwerte" + #~ #: lib/mv_web/live/contribution_settings_live.ex #~ #, elixir-autogen, elixir-format #~ msgid "Default Contribution Type" @@ -1463,6 +1474,11 @@ msgstr "Jährliches Intervall – Beitrittszeitraum einbezogen" #~ msgid "Generated periods" #~ msgstr "Generierte Zyklen" +#~ #: lib/mv_web/live/custom_field_live/form_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Immutable" +#~ msgstr "Unveränderlich" + #~ #: lib/mv_web/live/contribution_settings_live.ex #~ #, elixir-autogen, elixir-format #~ msgid "Include joining period" @@ -1473,6 +1489,17 @@ msgstr "Jährliches Intervall – Beitrittszeitraum einbezogen" #~ msgid "Monthly Interval - Joining Period Included" #~ msgstr "Monatliches Intervall – Beitrittszeitraum einbezogen" +#~ #: 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 +#~ msgid "Not set" +#~ msgstr "Nicht gesetzt" + #~ #: lib/mv_web/live/contribution_settings_live.ex #~ #, elixir-autogen, elixir-format #~ msgid "Quarterly Interval - Joining Period Excluded" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index c88b92a..83bfbfe 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" @@ -613,16 +608,6 @@ msgstr "" msgid "Please select a custom field first" msgstr "" -#: lib/mv_web/live/custom_field_live/form_component.ex -#, elixir-autogen, elixir-format -msgid "Save Custom field" -msgstr "" - -#: lib/mv_web/live/custom_field_value_live/form.ex -#, elixir-autogen, elixir-format -msgid "Save Custom field value" -msgstr "" - #: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/member_live/show.ex @@ -761,11 +746,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 +777,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 "" @@ -1303,13 +1282,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 @@ -1427,3 +1402,23 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Yearly Interval - Joining Cycle Included" 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 "" + +#: lib/mv_web/live/custom_field_live/form_component.ex +#, elixir-autogen, elixir-format +msgid "Save Custom Field" +msgstr "" + +#: lib/mv_web/live/custom_field_value_live/form.ex +#, elixir-autogen, elixir-format +msgid "Save Custom Field Value" +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 7286772..fd53d03 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" @@ -613,16 +608,6 @@ msgstr "" msgid "Please select a custom field first" msgstr "" -#: lib/mv_web/live/custom_field_live/form_component.ex -#, elixir-autogen, elixir-format -msgid "Save Custom field" -msgstr "" - -#: lib/mv_web/live/custom_field_value_live/form.ex -#, elixir-autogen, elixir-format -msgid "Save Custom field value" -msgstr "" - #: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/member_live/form.ex #: lib/mv_web/live/member_live/show.ex @@ -761,11 +746,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 +777,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 "" @@ -1303,13 +1282,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 @@ -1428,7 +1403,27 @@ msgstr "" msgid "Yearly Interval - Joining Cycle Included" msgstr "" -#~ #: lib/mv_web/live/contribution_settings_live.ex +#: 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/form_component.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Save Custom Field" +msgstr "" + +#: lib/mv_web/live/custom_field_value_live/form.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Save Custom Field Value" +msgstr "" + +#~ #: lib/mv_web/live/custom_field_live/show.ex #~ #, elixir-autogen, elixir-format #~ msgid "Configure global settings for membership contributions." #~ msgstr "" @@ -1439,11 +1434,18 @@ msgstr "" #~ msgid "Contribution Settings" #~ msgstr "" -#~ #: lib/mv_web/live/contribution_settings_live.ex +#~ #: lib/mv_web/live/member_live/form.ex +#~ #: lib/mv_web/live/member_live/show.ex #~ #, elixir-autogen, elixir-format #~ msgid "Contribution start" #~ msgstr "" +#~ #: lib/mv_web/live/contribution_settings_live.ex +#~ #: lib/mv_web/live/member_live/index.html.heex +#~ #, elixir-autogen, elixir-format +#~ msgid "Copy emails" +#~ msgstr "" + #~ #: lib/mv_web/live/contribution_settings_live.ex #~ #, elixir-autogen, elixir-format #~ msgid "Default Contribution Type" @@ -1459,11 +1461,18 @@ msgstr "" #~ msgid "Failed to save settings. Please check the errors below." #~ msgstr "" -#~ #: lib/mv_web/live/contribution_settings_live.ex +#~ #: lib/mv_web/live/user_live/index.html.heex +#~ #: lib/mv_web/live/user_live/show.ex #~ #, elixir-autogen, elixir-format #~ msgid "Generated periods" #~ msgstr "" +#~ #: lib/mv_web/live/contribution_settings_live.ex +#~ #: lib/mv_web/live/custom_field_live/form_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Immutable" +#~ msgstr "" + #~ #: lib/mv_web/live/contribution_settings_live.ex #~ #, elixir-autogen, elixir-format #~ msgid "Include joining period" @@ -1474,6 +1483,16 @@ msgstr "" #~ msgid "Monthly Interval - Joining Period Included" #~ 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" +#~ msgstr "" + #~ #: lib/mv_web/live/contribution_settings_live.ex #~ #, elixir-autogen, elixir-format #~ msgid "Quarterly Interval - Joining Period Excluded" 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 a37743e..2e8694d 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -45,28 +45,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 @@ -74,56 +70,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 diff --git a/test/mv_web/member_live/index_custom_fields_accessibility_test.exs b/test/mv_web/member_live/index_custom_fields_accessibility_test.exs index cfe3145..149d441 100644 --- a/test/mv_web/member_live/index_custom_fields_accessibility_test.exs +++ b/test/mv_web/member_live/index_custom_fields_accessibility_test.exs @@ -52,14 +52,11 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do field: field } do conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members") + {:ok, view, _html} = live(conn, "/members") - # Check that the sort button has aria-label - assert html =~ ~r/aria-label=["']Click to sort["']/i or - html =~ ~r/aria-label=["'].*sort.*["']/i - - # Check that data-testid is present for testing - assert html =~ ~r/data-testid=["']custom_field_#{field.id}["']/ + # Check that the sort button has aria-label and data-testid + test_id = "custom_field_#{field.id}" + assert has_element?(view, "[data-testid='#{test_id}'][aria-label='Click to sort']") end test "sort header component shows correct ARIA label when sorted ascending", %{ @@ -71,10 +68,9 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do {:ok, view, _html} = live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=asc") - html = render(view) - - # Check that aria-label indicates ascending sort - assert html =~ ~r/aria-label=["'].*ascending.*["']/i + # Check that aria-label indicates ascending sort using data-testid + test_id = "custom_field_#{field.id}" + assert has_element?(view, "[data-testid='#{test_id}'][aria-label='ascending']") end test "sort header component shows correct ARIA label when sorted descending", %{ @@ -86,21 +82,21 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do {:ok, view, _html} = live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc") - html = render(view) - - # Check that aria-label indicates descending sort - assert html =~ ~r/aria-label=["'].*descending.*["']/i + # Check that aria-label indicates descending sort using data-testid + test_id = "custom_field_#{field.id}" + assert has_element?(view, "[data-testid='#{test_id}'][aria-label='descending']") end test "custom field column header is keyboard accessible", %{conn: conn, field: field} do conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, "/members") + {:ok, view, _html} = live(conn, "/members") # Check that the sort button is a button element (keyboard accessible) - assert html =~ ~r/]*data-testid=["']custom_field_#{field.id}["']/ + test_id = "custom_field_#{field.id}" + assert has_element?(view, "button[data-testid='#{test_id}']") # Button should not have tabindex="-1" (which would remove from tab order) - refute html =~ ~r/tabindex=["']-1["']/ + refute has_element?(view, "button[data-testid='#{test_id}'][tabindex='-1']") end test "custom field column header has proper semantic structure", %{conn: conn, field: field} do diff --git a/test/mv_web/member_live/index_test.exs b/test/mv_web/member_live/index_test.exs index 30b61c7..3232cc0 100644 --- a/test/mv_web/member_live/index_test.exs +++ b/test/mv_web/member_live/index_test.exs @@ -410,15 +410,17 @@ defmodule MvWeb.MemberLive.IndexTest do assert render(view) =~ "1" end - test "copy button is not visible when no members are selected", %{conn: conn} do + test "copy button is disabled when no members 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") + # Copy button should be disabled (button element) + assert has_element?(view, "#copy-emails-btn[disabled]") + # Open email button should be disabled (link with tabindex and aria-disabled) + assert has_element?(view, "#open-email-btn[tabindex='-1'][aria-disabled='true']") end - test "copy button is visible when members are selected", %{ + test "copy button is enabled after selection", %{ conn: conn, member1: member1 } do @@ -428,8 +430,13 @@ defmodule MvWeb.MemberLive.IndexTest do # Select a member by sending the select_member event directly render_click(view, "select_member", %{"id" => member1.id}) - # Button should now be visible - assert has_element?(view, "#copy-emails-btn") + # Copy button should now be enabled (no disabled attribute) + refute has_element?(view, "#copy-emails-btn[disabled]") + # Open email button should now be enabled (no tabindex=-1 or aria-disabled) + refute has_element?(view, "#open-email-btn[tabindex='-1']") + refute has_element?(view, "#open-email-btn[aria-disabled='true']") + # Counter should show correct count + assert render(view) =~ "1" end test "copy button click triggers event and shows flash", %{