refactor: improve email copy with MapSet, RFC 5322 commas, and cond
All checks were successful
continuous-integration/drone/push Build is passing

Performance optimization, RFC-compliant separator, better tests
This commit is contained in:
Moritz 2025-12-02 12:10:59 +01:00
parent ba78a6ac7a
commit 39d2cb7820
4 changed files with 92 additions and 271 deletions

View file

@ -59,7 +59,7 @@ defmodule MvWeb.MemberLive.Index do
|> assign(:query, "")
|> assign_new(:sort_field, fn -> :first_name end)
|> assign_new(:sort_order, fn -> :asc end)
|> assign(:selected_members, [])
|> assign(:selected_members, MapSet.new())
|> assign(:custom_fields_visible, custom_fields_visible)
# We call handle params to use the query from the URL
@ -92,10 +92,10 @@ defmodule MvWeb.MemberLive.Index do
@impl true
def handle_event("select_member", %{"id" => id}, socket) do
selected =
if id in socket.assigns.selected_members do
List.delete(socket.assigns.selected_members, id)
if MapSet.member?(socket.assigns.selected_members, id) do
MapSet.delete(socket.assigns.selected_members, id)
else
[id | socket.assigns.selected_members]
MapSet.put(socket.assigns.selected_members, id)
end
{:noreply, assign(socket, :selected_members, selected)}
@ -103,13 +103,11 @@ defmodule MvWeb.MemberLive.Index do
@impl true
def handle_event("select_all", _params, socket) do
members = socket.assigns.members
all_ids = Enum.map(members, & &1.id)
all_ids = socket.assigns.members |> Enum.map(& &1.id) |> MapSet.new()
selected =
if Enum.sort(socket.assigns.selected_members) == Enum.sort(all_ids) do
[]
if MapSet.equal?(socket.assigns.selected_members, all_ids) do
MapSet.new()
else
all_ids
end
@ -121,26 +119,26 @@ defmodule MvWeb.MemberLive.Index do
def handle_event("copy_emails", _params, socket) do
selected_ids = socket.assigns.selected_members
if selected_ids == [] do
{:noreply, put_flash(socket, :error, gettext("No members selected"))}
else
# Filter members that are in the selection
selected_members =
socket.assigns.members
|> Enum.filter(fn member -> member.id in selected_ids end)
# Filter members that are in the selection and have email addresses
formatted_emails =
socket.assigns.members
|> Enum.filter(fn member ->
MapSet.member?(selected_ids, member.id) && member.email && member.email != ""
end)
|> Enum.map(&format_member_email/1)
# Format emails and filter out members without email
formatted_emails =
selected_members
|> Enum.filter(fn member -> member.email && member.email != "" end)
|> Enum.map(&format_member_email/1)
email_count = length(formatted_emails)
email_count = length(formatted_emails)
cond do
MapSet.size(selected_ids) == 0 ->
{:noreply, put_flash(socket, :error, gettext("No members selected"))}
if email_count == 0 do
email_count == 0 ->
{:noreply, put_flash(socket, :error, gettext("No email addresses found"))}
else
email_string = Enum.join(formatted_emails, "; ")
true ->
# RFC 5322 uses comma as separator for email address lists
email_string = Enum.join(formatted_emails, ", ")
socket =
socket
@ -160,7 +158,6 @@ defmodule MvWeb.MemberLive.Index do
)
{:noreply, socket}
end
end
end

View file

@ -3,18 +3,18 @@
{gettext("Members")}
<:actions>
<.button
:if={Enum.any?(@members, &(&1.id in @selected_members))}
:if={Enum.any?(@members, &MapSet.member?(@selected_members, &1.id))}
id="copy-emails-btn"
phx-hook="CopyToClipboard"
phx-click="copy_emails"
aria-label={gettext("Copy email addresses of selected members")}
>
<.icon name="hero-clipboard-document" />
{gettext("Copy emails")} ({Enum.count(@members, &(&1.id in @selected_members))})
{gettext("Copy emails")} ({Enum.count(@members, &MapSet.member?(@selected_members, &1.id))})
</.button>
<.button
:if={Enum.any?(@members, &(&1.id in @selected_members))}
href={"mailto:?bcc=#{@members |> Enum.filter(&(&1.id in @selected_members and &1.email)) |> Enum.map(& &1.email) |> Enum.join(",")}"}
:if={Enum.any?(@members, &MapSet.member?(@selected_members, &1.id))}
href={"mailto:?bcc=#{@members |> Enum.filter(&(MapSet.member?(@selected_members, &1.id) && &1.email)) |> Enum.map(& &1.email) |> Enum.join(",")}"}
aria-label={gettext("Open email program with BCC recipients")}
>
<.icon name="hero-envelope" />
@ -51,7 +51,7 @@
type="checkbox"
name="select_all"
phx-click="select_all"
checked={Enum.sort(@selected_members) == Enum.map(@members, & &1.id) |> Enum.sort()}
checked={MapSet.equal?(@selected_members, @members |> Enum.map(& &1.id) |> MapSet.new())}
aria-label={gettext("Select all members")}
role="checkbox"
/>
@ -63,7 +63,7 @@
name={member.id}
phx-click="select_member"
phx-value-id={member.id}
checked={member.id in @selected_members}
checked={MapSet.member?(@selected_members, member.id)}
phx-capture-click
phx-stop-propagation
aria-label={gettext("Select member")}