Compare commits
35 commits
342ebb73b9
...
3860ec51f2
| Author | SHA1 | Date | |
|---|---|---|---|
| 3860ec51f2 | |||
| 40cdcbe453 | |||
| 4997493139 | |||
| f7c33bfc7d | |||
| 6a91f7c711 | |||
| b517594141 | |||
| 358b1bfdb1 | |||
| b8791cbcfe | |||
| 5b66d49fcd | |||
| 83cf6d7503 | |||
| 6ab7028275 | |||
| 4cdbf5ed4f | |||
| b690d168c4 | |||
| 2704e0887e | |||
| cb37f0c360 | |||
| 355cba6bbc | |||
| 3733ad9d89 | |||
| 4384086245 | |||
| 2d1d650c28 | |||
| cd915531c2 | |||
| 673e90d179 | |||
| 86b6816d9b | |||
| 651f518215 | |||
| 0a07f4f212 | |||
| 1df1b4b238 | |||
| 62d04add8e | |||
| 9f9d888657 | |||
| be6ea56860 | |||
| fb91f748c2 | |||
| 222af635ae | |||
| dd4048669c | |||
| e0712d47bc | |||
| 4e86351e1c | |||
| 8bfa5b7d1d | |||
| cb82c07cbf |
18 changed files with 275 additions and 153 deletions
|
|
@ -12,7 +12,6 @@ defmodule Mv.Membership.CustomField do
|
||||||
- `slug` - URL-friendly, immutable identifier automatically generated from name (e.g., "phone-mobile")
|
- `slug` - URL-friendly, immutable identifier automatically generated from name (e.g., "phone-mobile")
|
||||||
- `value_type` - Data type constraint (`:string`, `:integer`, `:boolean`, `:date`, `:email`)
|
- `value_type` - Data type constraint (`:string`, `:integer`, `:boolean`, `:date`, `:email`)
|
||||||
- `description` - Optional human-readable description
|
- `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)
|
- `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
|
- `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
|
actions do
|
||||||
defaults [:read, :update]
|
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
|
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
|
change Mv.Membership.CustomField.Changes.GenerateSlug
|
||||||
validate string_length(:slug, min: 1)
|
validate string_length(:slug, min: 1)
|
||||||
end
|
end
|
||||||
|
|
@ -113,10 +112,6 @@ defmodule Mv.Membership.CustomField do
|
||||||
trim?: true
|
trim?: true
|
||||||
]
|
]
|
||||||
|
|
||||||
attribute :immutable, :boolean,
|
|
||||||
default: false,
|
|
||||||
allow_nil?: false
|
|
||||||
|
|
||||||
attribute :required, :boolean,
|
attribute :required, :boolean,
|
||||||
default: false,
|
default: false,
|
||||||
allow_nil?: false
|
allow_nil?: false
|
||||||
|
|
|
||||||
|
|
@ -95,9 +95,11 @@ defmodule MvWeb.CoreComponents do
|
||||||
<.button>Send!</.button>
|
<.button>Send!</.button>
|
||||||
<.button phx-click="go" variant="primary">Send!</.button>
|
<.button phx-click="go" variant="primary">Send!</.button>
|
||||||
<.button navigate={~p"/"}>Home</.button>
|
<.button navigate={~p"/"}>Home</.button>
|
||||||
|
<.button disabled={true}>Disabled</.button>
|
||||||
"""
|
"""
|
||||||
attr :rest, :global, include: ~w(href navigate patch method)
|
attr :rest, :global, include: ~w(href navigate patch method)
|
||||||
attr :variant, :string, values: ~w(primary)
|
attr :variant, :string, values: ~w(primary)
|
||||||
|
attr :disabled, :boolean, default: false, doc: "Whether the button is disabled"
|
||||||
slot :inner_block, required: true
|
slot :inner_block, required: true
|
||||||
|
|
||||||
def button(%{rest: rest} = assigns) do
|
def button(%{rest: rest} = assigns) do
|
||||||
|
|
@ -105,14 +107,37 @@ defmodule MvWeb.CoreComponents do
|
||||||
assigns = assign(assigns, :class, Map.fetch!(variants, assigns[:variant]))
|
assigns = assign(assigns, :class, Map.fetch!(variants, assigns[:variant]))
|
||||||
|
|
||||||
if rest[:href] || rest[:navigate] || rest[:patch] do
|
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"""
|
~H"""
|
||||||
<.link class={["btn", @class]} {@rest}>
|
<.link class={@link_class} {@link_attrs}>
|
||||||
{render_slot(@inner_block)}
|
{render_slot(@inner_block)}
|
||||||
</.link>
|
</.link>
|
||||||
"""
|
"""
|
||||||
else
|
else
|
||||||
~H"""
|
~H"""
|
||||||
<button class={["btn", @class]} {@rest}>
|
<button class={["btn", @class]} disabled={@disabled} {@rest}>
|
||||||
{render_slot(@inner_block)}
|
{render_slot(@inner_block)}
|
||||||
</button>
|
</button>
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,16 @@ defmodule MvWeb.Layouts do
|
||||||
default: nil,
|
default: nil,
|
||||||
doc: "the current [scope](https://hexdocs.pm/phoenix/scopes.html)"
|
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
|
slot :inner_block, required: true
|
||||||
|
|
||||||
def app(assigns) do
|
def app(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<%= if @current_user do %>
|
<%= if @current_user do %>
|
||||||
<.navbar current_user={@current_user} />
|
<.navbar current_user={@current_user} club_name={@club_name} />
|
||||||
<% end %>
|
<% end %>
|
||||||
<main class="px-4 py-20 sm:px-6 lg:px-16">
|
<main class="px-4 py-20 sm:px-6 lg:px-16">
|
||||||
<div class="mx-auto max-full space-y-4">
|
<div class="mx-auto max-full space-y-4">
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,18 @@ defmodule MvWeb.Layouts.Navbar do
|
||||||
required: true,
|
required: true,
|
||||||
doc: "The current user - navbar is only shown when user is present"
|
doc: "The current user - navbar is only shown when user is present"
|
||||||
|
|
||||||
def navbar(assigns) do
|
attr :club_name, :string,
|
||||||
club_name = get_club_name()
|
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)
|
assigns = assign(assigns, :club_name, club_name)
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<header class="navbar bg-base-100 shadow-sm">
|
<header class="navbar bg-base-100 shadow-sm">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<a class="btn btn-ghost text-xl">{@club_name}</a>
|
<a href="/members" class="btn btn-ghost text-xl">{@club_name}</a>
|
||||||
<ul class="menu menu-horizontal bg-base-200">
|
<ul class="menu menu-horizontal bg-base-200">
|
||||||
<li><.link navigate="/members">{gettext("Members")}</.link></li>
|
<li><.link navigate="/members">{gettext("Members")}</.link></li>
|
||||||
<li><.link navigate="/settings">{gettext("Settings")}</.link></li>
|
<li><.link navigate="/settings">{gettext("Settings")}</.link></li>
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ defmodule MvWeb.Components.PaymentFilterComponent do
|
||||||
phx-target={@myself}
|
phx-target={@myself}
|
||||||
>
|
>
|
||||||
<.icon name="hero-users" class="h-4 w-4" />
|
<.icon name="hero-users" class="h-4 w-4" />
|
||||||
{gettext("All")}
|
{gettext("All payment statuses")}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li role="none">
|
<li role="none">
|
||||||
|
|
@ -140,7 +140,7 @@ defmodule MvWeb.Components.PaymentFilterComponent do
|
||||||
defp parse_filter(_), do: nil
|
defp parse_filter(_), do: nil
|
||||||
|
|
||||||
# Get display label for current filter
|
# 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(:paid), do: gettext("Paid")
|
||||||
defp filter_label(:not_paid), do: gettext("Not paid")
|
defp filter_label(:not_paid), do: gettext("Not paid")
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ defmodule MvWeb.CustomFieldLive.FormComponent do
|
||||||
- Create new custom field definitions
|
- Create new custom field definitions
|
||||||
- Edit existing custom fields
|
- Edit existing custom fields
|
||||||
- Select value type from supported types
|
- Select value type from supported types
|
||||||
- Set immutable and required flags
|
- Set required flag
|
||||||
- Real-time validation
|
- Real-time validation
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
@ -50,10 +50,10 @@ defmodule MvWeb.CustomFieldLive.FormComponent do
|
||||||
label={gettext("Value type")}
|
label={gettext("Value type")}
|
||||||
options={
|
options={
|
||||||
Ash.Resource.Info.attribute(Mv.Membership.CustomField, :value_type).constraints[:one_of]
|
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[: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[:required]} type="checkbox" label={gettext("Required")} />
|
||||||
<.input
|
<.input
|
||||||
field={@form[:show_in_overview]}
|
field={@form[:show_in_overview]}
|
||||||
|
|
@ -66,7 +66,7 @@ defmodule MvWeb.CustomFieldLive.FormComponent do
|
||||||
{gettext("Cancel")}
|
{gettext("Cancel")}
|
||||||
</.button>
|
</.button>
|
||||||
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
||||||
{gettext("Save Custom field")}
|
{gettext("Save Custom Field")}
|
||||||
</.button>
|
</.button>
|
||||||
</div>
|
</div>
|
||||||
</.form>
|
</.form>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
|
||||||
## Features
|
## Features
|
||||||
- List all custom fields
|
- List all custom fields
|
||||||
- Display type information (name, value type, description)
|
- Display type information (name, value type, description)
|
||||||
- Show immutable and required flags
|
- Show required flag
|
||||||
- Create new custom fields
|
- Create new custom fields
|
||||||
- Edit existing custom fields
|
- Edit existing custom fields
|
||||||
- Delete custom fields with confirmation (cascades to all custom field values)
|
- 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-click="new_custom_field"
|
||||||
phx-target={@myself}
|
phx-target={@myself}
|
||||||
>
|
>
|
||||||
<.icon name="hero-plus" /> {gettext("New Custom field")}
|
<.icon name="hero-plus" /> {gettext("New Custom Field")}
|
||||||
</.button>
|
</.button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ defmodule MvWeb.CustomFieldValueLive.Form do
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
||||||
{gettext("Save Custom field value")}
|
{gettext("Save Custom Field Value")}
|
||||||
</.button>
|
</.button>
|
||||||
<.button navigate={return_path(@return_to, @custom_field_value)}>{gettext("Cancel")}</.button>
|
<.button navigate={return_path(@return_to, @custom_field_value)}>{gettext("Cancel")}</.button>
|
||||||
</.form>
|
</.form>
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ defmodule MvWeb.GlobalSettingsLive do
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
<Layouts.app flash={@flash} current_user={@current_user} club_name={@settings.club_name}>
|
||||||
<.header>
|
<.header>
|
||||||
{gettext("Settings")}
|
{gettext("Settings")}
|
||||||
<:subtitle>
|
<:subtitle>
|
||||||
|
|
@ -80,10 +80,13 @@ defmodule MvWeb.GlobalSettingsLive do
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("save", %{"setting" => setting_params}, socket) do
|
def handle_event("save", %{"setting" => setting_params}, socket) do
|
||||||
case AshPhoenix.Form.submit(socket.assigns.form, params: setting_params) 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 =
|
||||||
socket
|
socket
|
||||||
|> assign(:settings, updated_settings)
|
|> assign(:settings, fresh_settings)
|
||||||
|> put_flash(:info, gettext("Settings updated successfully"))
|
|> put_flash(:info, gettext("Settings updated successfully"))
|
||||||
|> assign_form()
|
|> assign_form()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,10 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
MapSet.put(socket.assigns.selected_members, id)
|
MapSet.put(socket.assigns.selected_members, id)
|
||||||
end
|
end
|
||||||
|
|
||||||
{:noreply, assign(socket, :selected_members, selected)}
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:selected_members, selected)
|
||||||
|
|> update_selection_assigns()}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
@ -159,7 +162,10 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
all_ids
|
all_ids
|
||||||
end
|
end
|
||||||
|
|
||||||
{:noreply, assign(socket, :selected_members, selected)}
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:selected_members, selected)
|
||||||
|
|> update_selection_assigns()}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
@ -238,6 +244,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
socket
|
socket
|
||||||
|> assign(:query, q)
|
|> assign(:query, q)
|
||||||
|> load_members()
|
|> load_members()
|
||||||
|
|> update_selection_assigns()
|
||||||
|
|
||||||
existing_field_query = socket.assigns.sort_field
|
existing_field_query = socket.assigns.sort_field
|
||||||
existing_sort_query = socket.assigns.sort_order
|
existing_sort_query = socket.assigns.sort_order
|
||||||
|
|
@ -263,6 +270,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
socket
|
socket
|
||||||
|> assign(:paid_filter, filter)
|
|> assign(:paid_filter, filter)
|
||||||
|> load_members()
|
|> load_members()
|
||||||
|
|> update_selection_assigns()
|
||||||
|
|
||||||
# Build the URL with all params including new filter
|
# Build the URL with all params including new filter
|
||||||
query_params =
|
query_params =
|
||||||
|
|
@ -309,6 +317,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
|> assign(:visible_custom_field_ids, extract_custom_field_ids(visible_custom_fields))
|
|> assign(:visible_custom_field_ids, extract_custom_field_ids(visible_custom_fields))
|
||||||
|> load_members()
|
|> load_members()
|
||||||
|> prepare_dynamic_cols()
|
|> prepare_dynamic_cols()
|
||||||
|
|> update_selection_assigns()
|
||||||
|> push_field_selection_url()
|
|> push_field_selection_url()
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
|
@ -338,6 +347,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
|> assign(:visible_custom_field_ids, extract_custom_field_ids(visible_custom_fields))
|
|> assign(:visible_custom_field_ids, extract_custom_field_ids(visible_custom_fields))
|
||||||
|> load_members()
|
|> load_members()
|
||||||
|> prepare_dynamic_cols()
|
|> prepare_dynamic_cols()
|
||||||
|
|> update_selection_assigns()
|
||||||
|> push_field_selection_url()
|
|> push_field_selection_url()
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
|
@ -389,6 +399,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
|> assign(:visible_custom_field_ids, extract_custom_field_ids(visible_custom_fields))
|
|> assign(:visible_custom_field_ids, extract_custom_field_ids(visible_custom_fields))
|
||||||
|> load_members()
|
|> load_members()
|
||||||
|> prepare_dynamic_cols()
|
|> prepare_dynamic_cols()
|
||||||
|
|> update_selection_assigns()
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
@ -1112,4 +1123,34 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
|
|
||||||
# Public helper function to format dates for use in templates
|
# Public helper function to format dates for use in templates
|
||||||
def format_date(date), do: DateFormatter.format_date(date)
|
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
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,23 +3,21 @@
|
||||||
{gettext("Members")}
|
{gettext("Members")}
|
||||||
<:actions>
|
<:actions>
|
||||||
<.button
|
<.button
|
||||||
:if={Enum.any?(@members, &MapSet.member?(@selected_members, &1.id))}
|
class="secondary"
|
||||||
id="copy-emails-btn"
|
id="copy-emails-btn"
|
||||||
phx-hook="CopyToClipboard"
|
phx-hook="CopyToClipboard"
|
||||||
phx-click="copy_emails"
|
phx-click="copy_emails"
|
||||||
|
disabled={not @any_selected?}
|
||||||
aria-label={gettext("Copy email addresses of selected members")}
|
aria-label={gettext("Copy email addresses of selected members")}
|
||||||
>
|
>
|
||||||
<.icon name="hero-clipboard-document" />
|
<.icon name="hero-clipboard-document" />
|
||||||
{gettext("Copy emails")} ({Enum.count(@members, &MapSet.member?(@selected_members, &1.id))})
|
{gettext("Copy email addresses")} ({@selected_count})
|
||||||
</.button>
|
</.button>
|
||||||
<.button
|
<.button
|
||||||
:if={Enum.any?(@members, &MapSet.member?(@selected_members, &1.id))}
|
class="secondary"
|
||||||
href={
|
id="open-email-btn"
|
||||||
"mailto:?bcc=" <>
|
href={"mailto:?bcc=" <> @mailto_bcc}
|
||||||
(MvWeb.MemberLive.Index.format_selected_member_emails(@members, @selected_members)
|
disabled={not @any_selected?}
|
||||||
|> Enum.join(", ")
|
|
||||||
|> URI.encode())
|
|
||||||
}
|
|
||||||
aria-label={gettext("Open email program with BCC recipients")}
|
aria-label={gettext("Open email program with BCC recipients")}
|
||||||
>
|
>
|
||||||
<.icon name="hero-envelope" />
|
<.icon name="hero-envelope" />
|
||||||
|
|
|
||||||
|
|
@ -282,11 +282,6 @@ msgstr "Benutzer*in bearbeiten"
|
||||||
msgid "Enabled"
|
msgid "Enabled"
|
||||||
msgstr "Aktiviert"
|
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
|
#: lib/mv_web/components/layouts/navbar.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Logout"
|
msgid "Logout"
|
||||||
|
|
@ -612,16 +607,6 @@ msgstr "Benutzerdefinierter Feldwert erfolgreich %{action}"
|
||||||
msgid "Please select a custom field first"
|
msgid "Please select a custom field first"
|
||||||
msgstr "Bitte wähle zuerst ein Benutzerdefiniertes Feld"
|
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/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: lib/mv_web/live/member_live/show.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"
|
msgid "Copy email addresses of selected members"
|
||||||
msgstr "E-Mail-Adressen der ausgewählten Mitglieder kopieren"
|
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
|
#: lib/mv_web/live/member_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No email addresses found"
|
msgid "No email addresses found"
|
||||||
|
|
@ -796,7 +776,6 @@ msgid "This field cannot be empty"
|
||||||
msgstr "Dieses Feld darf nicht leer bleiben"
|
msgstr "Dieses Feld darf nicht leer bleiben"
|
||||||
|
|
||||||
#: lib/mv_web/components/core_components.ex
|
#: lib/mv_web/components/core_components.ex
|
||||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr "Alle"
|
msgstr "Alle"
|
||||||
|
|
@ -1302,14 +1281,10 @@ msgid "Failed to delete custom field: %{error}"
|
||||||
msgstr "Konnte Feld nicht löschen: %{error}"
|
msgstr "Konnte Feld nicht löschen: %{error}"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: 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
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "New Custom field"
|
msgid "New Custom Field"
|
||||||
msgstr "Benutzerdefiniertes Feld speichern"
|
msgstr "Neues Benutzerdefiniertes Feld"
|
||||||
|
|
||||||
#: lib/mv_web/live/global_settings_live.ex
|
#: lib/mv_web/live/global_settings_live.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -1427,6 +1402,31 @@ msgstr "Jährliches Intervall – Beitrittszeitraum nicht einbezogen"
|
||||||
msgid "Yearly Interval - Joining Cycle Included"
|
msgid "Yearly Interval - Joining Cycle Included"
|
||||||
msgstr "Jährliches Intervall – Beitrittszeitraum einbezogen"
|
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
|
#~ #: lib/mv_web/live/contribution_settings_live.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Configure global settings for membership contributions."
|
#~ msgid "Configure global settings for membership contributions."
|
||||||
|
|
@ -1443,6 +1443,17 @@ msgstr "Jährliches Intervall – Beitrittszeitraum einbezogen"
|
||||||
#~ msgid "Contribution start"
|
#~ msgid "Contribution start"
|
||||||
#~ msgstr "Beitragsbeginn"
|
#~ 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
|
#~ #: lib/mv_web/live/contribution_settings_live.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Default Contribution Type"
|
#~ msgid "Default Contribution Type"
|
||||||
|
|
@ -1463,6 +1474,11 @@ msgstr "Jährliches Intervall – Beitrittszeitraum einbezogen"
|
||||||
#~ msgid "Generated periods"
|
#~ msgid "Generated periods"
|
||||||
#~ msgstr "Generierte Zyklen"
|
#~ 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
|
#~ #: lib/mv_web/live/contribution_settings_live.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Include joining period"
|
#~ msgid "Include joining period"
|
||||||
|
|
@ -1473,6 +1489,17 @@ msgstr "Jährliches Intervall – Beitrittszeitraum einbezogen"
|
||||||
#~ msgid "Monthly Interval - Joining Period Included"
|
#~ msgid "Monthly Interval - Joining Period Included"
|
||||||
#~ msgstr "Monatliches Intervall – Beitrittszeitraum einbezogen"
|
#~ 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
|
#~ #: lib/mv_web/live/contribution_settings_live.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Quarterly Interval - Joining Period Excluded"
|
#~ msgid "Quarterly Interval - Joining Period Excluded"
|
||||||
|
|
|
||||||
|
|
@ -283,11 +283,6 @@ msgstr ""
|
||||||
msgid "Enabled"
|
msgid "Enabled"
|
||||||
msgstr ""
|
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
|
#: lib/mv_web/components/layouts/navbar.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Logout"
|
msgid "Logout"
|
||||||
|
|
@ -613,16 +608,6 @@ msgstr ""
|
||||||
msgid "Please select a custom field first"
|
msgid "Please select a custom field first"
|
||||||
msgstr ""
|
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/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
|
|
@ -761,11 +746,6 @@ msgstr[1] ""
|
||||||
msgid "Copy email addresses of selected members"
|
msgid "Copy email addresses of selected members"
|
||||||
msgstr ""
|
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
|
#: lib/mv_web/live/member_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No email addresses found"
|
msgid "No email addresses found"
|
||||||
|
|
@ -797,7 +777,6 @@ msgid "This field cannot be empty"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/core_components.ex
|
#: lib/mv_web/components/core_components.ex
|
||||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -1303,13 +1282,9 @@ msgid "Failed to delete custom field: %{error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: 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
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "New Custom field"
|
msgid "New Custom Field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/global_settings_live.ex
|
#: lib/mv_web/live/global_settings_live.ex
|
||||||
|
|
@ -1427,3 +1402,23 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Yearly Interval - Joining Cycle Included"
|
msgid "Yearly Interval - Joining Cycle Included"
|
||||||
msgstr ""
|
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 ""
|
||||||
|
|
|
||||||
|
|
@ -283,11 +283,6 @@ msgstr ""
|
||||||
msgid "Enabled"
|
msgid "Enabled"
|
||||||
msgstr ""
|
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
|
#: lib/mv_web/components/layouts/navbar.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Logout"
|
msgid "Logout"
|
||||||
|
|
@ -613,16 +608,6 @@ msgstr ""
|
||||||
msgid "Please select a custom field first"
|
msgid "Please select a custom field first"
|
||||||
msgstr ""
|
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/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
|
|
@ -761,11 +746,6 @@ msgstr[1] ""
|
||||||
msgid "Copy email addresses of selected members"
|
msgid "Copy email addresses of selected members"
|
||||||
msgstr ""
|
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
|
#: lib/mv_web/live/member_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No email addresses found"
|
msgid "No email addresses found"
|
||||||
|
|
@ -797,7 +777,6 @@ msgid "This field cannot be empty"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/core_components.ex
|
#: lib/mv_web/components/core_components.ex
|
||||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -1303,13 +1282,9 @@ msgid "Failed to delete custom field: %{error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: 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
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "New Custom field"
|
msgid "New Custom Field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/global_settings_live.ex
|
#: lib/mv_web/live/global_settings_live.ex
|
||||||
|
|
@ -1428,7 +1403,27 @@ msgstr ""
|
||||||
msgid "Yearly Interval - Joining Cycle Included"
|
msgid "Yearly Interval - Joining Cycle Included"
|
||||||
msgstr ""
|
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
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Configure global settings for membership contributions."
|
#~ msgid "Configure global settings for membership contributions."
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
@ -1439,11 +1434,18 @@ msgstr ""
|
||||||
#~ msgid "Contribution Settings"
|
#~ msgid "Contribution Settings"
|
||||||
#~ msgstr ""
|
#~ 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
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Contribution start"
|
#~ msgid "Contribution start"
|
||||||
#~ msgstr ""
|
#~ 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
|
#~ #: lib/mv_web/live/contribution_settings_live.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Default Contribution Type"
|
#~ msgid "Default Contribution Type"
|
||||||
|
|
@ -1459,11 +1461,18 @@ msgstr ""
|
||||||
#~ msgid "Failed to save settings. Please check the errors below."
|
#~ msgid "Failed to save settings. Please check the errors below."
|
||||||
#~ msgstr ""
|
#~ 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
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Generated periods"
|
#~ msgid "Generated periods"
|
||||||
#~ msgstr ""
|
#~ 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
|
#~ #: lib/mv_web/live/contribution_settings_live.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Include joining period"
|
#~ msgid "Include joining period"
|
||||||
|
|
@ -1474,6 +1483,16 @@ msgstr ""
|
||||||
#~ msgid "Monthly Interval - Joining Period Included"
|
#~ msgid "Monthly Interval - Joining Period Included"
|
||||||
#~ msgstr ""
|
#~ 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
|
#~ #: lib/mv_web/live/contribution_settings_live.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Quarterly Interval - Joining Period Excluded"
|
#~ msgid "Quarterly Interval - Joining Period Excluded"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -45,28 +45,24 @@ for attrs <- [
|
||||||
name: "String Field",
|
name: "String Field",
|
||||||
value_type: :string,
|
value_type: :string,
|
||||||
description: "Example for a field of type string",
|
description: "Example for a field of type string",
|
||||||
immutable: true,
|
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
name: "Date Field",
|
name: "Date Field",
|
||||||
value_type: :date,
|
value_type: :date,
|
||||||
description: "Example for a field of type date",
|
description: "Example for a field of type date",
|
||||||
immutable: true,
|
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
name: "Boolean Field",
|
name: "Boolean Field",
|
||||||
value_type: :boolean,
|
value_type: :boolean,
|
||||||
description: "Example for a field of type boolean",
|
description: "Example for a field of type boolean",
|
||||||
immutable: true,
|
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
name: "Email Field",
|
name: "Email Field",
|
||||||
value_type: :email,
|
value_type: :email,
|
||||||
description: "Example for a field of type email",
|
description: "Example for a field of type email",
|
||||||
immutable: true,
|
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
# Realistic custom fields
|
# Realistic custom fields
|
||||||
|
|
@ -74,56 +70,48 @@ for attrs <- [
|
||||||
name: "Membership Number",
|
name: "Membership Number",
|
||||||
value_type: :string,
|
value_type: :string,
|
||||||
description: "Unique membership identification number",
|
description: "Unique membership identification number",
|
||||||
immutable: false,
|
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
name: "Emergency Contact",
|
name: "Emergency Contact",
|
||||||
value_type: :string,
|
value_type: :string,
|
||||||
description: "Emergency contact person name and phone",
|
description: "Emergency contact person name and phone",
|
||||||
immutable: false,
|
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
name: "T-Shirt Size",
|
name: "T-Shirt Size",
|
||||||
value_type: :string,
|
value_type: :string,
|
||||||
description: "T-Shirt size for events (XS, S, M, L, XL, XXL)",
|
description: "T-Shirt size for events (XS, S, M, L, XL, XXL)",
|
||||||
immutable: false,
|
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
name: "Newsletter Subscription",
|
name: "Newsletter Subscription",
|
||||||
value_type: :boolean,
|
value_type: :boolean,
|
||||||
description: "Whether member wants to receive newsletter",
|
description: "Whether member wants to receive newsletter",
|
||||||
immutable: false,
|
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
name: "Date of Last Medical Check",
|
name: "Date of Last Medical Check",
|
||||||
value_type: :date,
|
value_type: :date,
|
||||||
description: "Date of last medical examination",
|
description: "Date of last medical examination",
|
||||||
immutable: false,
|
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
name: "Secondary Email",
|
name: "Secondary Email",
|
||||||
value_type: :email,
|
value_type: :email,
|
||||||
description: "Alternative email address",
|
description: "Alternative email address",
|
||||||
immutable: false,
|
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
name: "Membership Type",
|
name: "Membership Type",
|
||||||
value_type: :string,
|
value_type: :string,
|
||||||
description: "Type of membership (e.g., Regular, Student, Senior)",
|
description: "Type of membership (e.g., Regular, Student, Senior)",
|
||||||
immutable: false,
|
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
name: "Parking Permit",
|
name: "Parking Permit",
|
||||||
value_type: :boolean,
|
value_type: :boolean,
|
||||||
description: "Whether member has parking permit",
|
description: "Whether member has parking permit",
|
||||||
immutable: false,
|
|
||||||
required: false
|
required: false
|
||||||
}
|
}
|
||||||
] do
|
] do
|
||||||
|
|
|
||||||
|
|
@ -52,14 +52,11 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do
|
||||||
field: field
|
field: field
|
||||||
} do
|
} do
|
||||||
conn = conn_with_oidc_user(conn)
|
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
|
# Check that the sort button has aria-label and data-testid
|
||||||
assert html =~ ~r/aria-label=["']Click to sort["']/i or
|
test_id = "custom_field_#{field.id}"
|
||||||
html =~ ~r/aria-label=["'].*sort.*["']/i
|
assert has_element?(view, "[data-testid='#{test_id}'][aria-label='Click to sort']")
|
||||||
|
|
||||||
# Check that data-testid is present for testing
|
|
||||||
assert html =~ ~r/data-testid=["']custom_field_#{field.id}["']/
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "sort header component shows correct ARIA label when sorted ascending", %{
|
test "sort header component shows correct ARIA label when sorted ascending", %{
|
||||||
|
|
@ -71,10 +68,9 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do
|
||||||
{:ok, view, _html} =
|
{:ok, view, _html} =
|
||||||
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=asc")
|
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=asc")
|
||||||
|
|
||||||
html = render(view)
|
# Check that aria-label indicates ascending sort using data-testid
|
||||||
|
test_id = "custom_field_#{field.id}"
|
||||||
# Check that aria-label indicates ascending sort
|
assert has_element?(view, "[data-testid='#{test_id}'][aria-label='ascending']")
|
||||||
assert html =~ ~r/aria-label=["'].*ascending.*["']/i
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "sort header component shows correct ARIA label when sorted descending", %{
|
test "sort header component shows correct ARIA label when sorted descending", %{
|
||||||
|
|
@ -86,21 +82,21 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do
|
||||||
{:ok, view, _html} =
|
{:ok, view, _html} =
|
||||||
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
|
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
|
||||||
|
|
||||||
html = render(view)
|
# Check that aria-label indicates descending sort using data-testid
|
||||||
|
test_id = "custom_field_#{field.id}"
|
||||||
# Check that aria-label indicates descending sort
|
assert has_element?(view, "[data-testid='#{test_id}'][aria-label='descending']")
|
||||||
assert html =~ ~r/aria-label=["'].*descending.*["']/i
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "custom field column header is keyboard accessible", %{conn: conn, field: field} do
|
test "custom field column header is keyboard accessible", %{conn: conn, field: field} do
|
||||||
conn = conn_with_oidc_user(conn)
|
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)
|
# Check that the sort button is a button element (keyboard accessible)
|
||||||
assert html =~ ~r/<button[^>]*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)
|
# 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
|
end
|
||||||
|
|
||||||
test "custom field column header has proper semantic structure", %{conn: conn, field: field} do
|
test "custom field column header has proper semantic structure", %{conn: conn, field: field} do
|
||||||
|
|
|
||||||
|
|
@ -410,15 +410,17 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
assert render(view) =~ "1"
|
assert render(view) =~ "1"
|
||||||
end
|
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)
|
conn = conn_with_oidc_user(conn)
|
||||||
{:ok, view, _html} = live(conn, "/members")
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
# Ensure no members are selected (default state)
|
# Copy button should be disabled (button element)
|
||||||
refute has_element?(view, "#copy-emails-btn")
|
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
|
end
|
||||||
|
|
||||||
test "copy button is visible when members are selected", %{
|
test "copy button is enabled after selection", %{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
member1: member1
|
member1: member1
|
||||||
} do
|
} do
|
||||||
|
|
@ -428,8 +430,13 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
# Select a member by sending the select_member event directly
|
# Select a member by sending the select_member event directly
|
||||||
render_click(view, "select_member", %{"id" => member1.id})
|
render_click(view, "select_member", %{"id" => member1.id})
|
||||||
|
|
||||||
# Button should now be visible
|
# Copy button should now be enabled (no disabled attribute)
|
||||||
assert has_element?(view, "#copy-emails-btn")
|
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
|
end
|
||||||
|
|
||||||
test "copy button click triggers event and shows flash", %{
|
test "copy button click triggers event and shows flash", %{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue