feat: add bulk email copy for selected members (#230)
All checks were successful
continuous-integration/drone/push Build is passing

Copy selected members' emails to clipboard in 'First Last <email>' format
This commit is contained in:
Moritz 2025-12-02 10:02:58 +01:00
parent e803dbdf8b
commit e2ace3d2a8
11 changed files with 661 additions and 61 deletions

View file

@ -18,6 +18,7 @@ defmodule MvWeb.MemberLive.Index do
- `delete` - Remove a member from the database
- `select_member` - Toggle individual member selection
- `select_all` - Toggle selection of all visible members
- `copy_emails` - Copy email addresses of selected members to clipboard
## Implementation Notes
- Search uses PostgreSQL full-text search (plainto_tsquery)
@ -116,6 +117,49 @@ defmodule MvWeb.MemberLive.Index do
{:noreply, assign(socket, :selected_members, selected)}
end
@impl true
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)
# 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)
if email_count == 0 do
{:noreply, put_flash(socket, :error, gettext("No email addresses found"))}
else
email_string = Enum.join(formatted_emails, "; ")
socket =
socket
|> push_event("copy_to_clipboard", %{text: email_string})
|> put_flash(
:info,
ngettext(
"Copied %{count} email address to clipboard",
"Copied %{count} email addresses to clipboard",
email_count,
count: email_count
)
)
{:noreply, socket}
end
end
end
# -----------------------------------------------------------------
# Handle Infos from Child Components
# -----------------------------------------------------------------
@ -733,4 +777,22 @@ defmodule MvWeb.MemberLive.Index do
nil
end
end
# Formats a member's email in the format "First Last <email>"
# Used for copy_emails feature to create email-client-friendly format.
defp format_member_email(member) do
first_name = member.first_name || ""
last_name = member.last_name || ""
name =
[first_name, last_name]
|> Enum.filter(&(&1 != ""))
|> Enum.join(" ")
if name == "" do
member.email
else
"#{name} <#{member.email}>"
end
end
end

View file

@ -2,6 +2,16 @@
<.header>
{gettext("Members")}
<:actions>
<.button
:if={Enum.any?(@members, &(&1.id in @selected_members))}
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))})
</.button>
<.button variant="primary" navigate={~p"/members/new"}>
<.icon name="hero-plus" /> {gettext("New Member")}
</.button>