feat: add bulk email copy for selected members (#230)
All checks were successful
continuous-integration/drone/push Build is passing
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:
parent
e803dbdf8b
commit
e2ace3d2a8
11 changed files with 661 additions and 61 deletions
|
|
@ -12,8 +12,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- PostgreSQL trigram-based member search with typo tolerance
|
- PostgreSQL trigram-based member search with typo tolerance
|
||||||
- WCAG 2.1 AA compliant autocomplete dropdown with ARIA support
|
- WCAG 2.1 AA compliant autocomplete dropdown with ARIA support
|
||||||
- Bilingual UI (German/English) for member linking workflow
|
- Bilingual UI (German/English) for member linking workflow
|
||||||
|
- **Bulk email copy feature** - Copy email addresses of selected members to clipboard (#230)
|
||||||
|
- Email format: "First Last <email>" with semicolon separator (compatible with email clients)
|
||||||
|
- CopyToClipboard JavaScript hook with fallback for older browsers
|
||||||
|
- Button shows count of visible selected members (respects search/filter)
|
||||||
|
- German/English translations
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Email validation false positive when linking user and member with identical emails (#168 Problem #4)
|
- Email validation false positive when linking user and member with identical emails (#168 Problem #4)
|
||||||
- Relationship data extraction from Ash manage_relationship during validation
|
- Relationship data extraction from Ash manage_relationship during validation
|
||||||
|
- Copy button count now shows only visible selected members when filtering
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,33 @@ let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("
|
||||||
// Hooks for LiveView components
|
// Hooks for LiveView components
|
||||||
let Hooks = {}
|
let Hooks = {}
|
||||||
|
|
||||||
|
// CopyToClipboard hook: Copies text to clipboard when triggered by server event
|
||||||
|
Hooks.CopyToClipboard = {
|
||||||
|
mounted() {
|
||||||
|
this.handleEvent("copy_to_clipboard", ({text}) => {
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(text).catch(err => {
|
||||||
|
console.error("Clipboard write failed:", err)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Fallback for older browsers
|
||||||
|
const textArea = document.createElement("textarea")
|
||||||
|
textArea.value = text
|
||||||
|
textArea.style.position = "fixed"
|
||||||
|
textArea.style.left = "-999999px"
|
||||||
|
document.body.appendChild(textArea)
|
||||||
|
textArea.select()
|
||||||
|
try {
|
||||||
|
document.execCommand("copy")
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Fallback clipboard copy failed:", err)
|
||||||
|
}
|
||||||
|
document.body.removeChild(textArea)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ComboBox hook: Prevents form submission when Enter is pressed in dropdown
|
// ComboBox hook: Prevents form submission when Enter is pressed in dropdown
|
||||||
Hooks.ComboBox = {
|
Hooks.ComboBox = {
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
|
||||||
|
|
@ -1327,6 +1327,33 @@ end
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Session: Bulk Email Copy Feature (2025-12-02)
|
||||||
|
|
||||||
|
### Feature Summary
|
||||||
|
Implemented bulk email copy functionality for selected members (#230). Users can select members and copy their email addresses to clipboard.
|
||||||
|
|
||||||
|
**Key Features:**
|
||||||
|
- Copy button appears only when visible members are selected
|
||||||
|
- Email format: `First Last <email>` with semicolon separator (email client compatible)
|
||||||
|
- Button shows count of visible selected members (respects search/filter)
|
||||||
|
- CopyToClipboard JavaScript hook with clipboard API + fallback for older browsers
|
||||||
|
- Bilingual UI (English/German)
|
||||||
|
|
||||||
|
### Key Decisions
|
||||||
|
|
||||||
|
1. **Email Format:** "First Last <email>" with semicolon - standard for all major email clients
|
||||||
|
2. **Visible Member Count:** Button shows only visible selected members, not total selected (better UX when filtering)
|
||||||
|
3. **Server→Client:** Used `push_event/3` - server formats data, client handles clipboard
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `lib/mv_web/live/member_live/index.ex` - Event handler, helper function
|
||||||
|
- `lib/mv_web/live/member_live/index.html.heex` - Copy button
|
||||||
|
- `assets/js/app.js` - CopyToClipboard hook
|
||||||
|
- `test/mv_web/member_live/index_test.exs` - 9 new tests
|
||||||
|
- `priv/gettext/de/LC_MESSAGES/default.po` - German translations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Session: User-Member Linking UI Enhancement (2025-01-13)
|
## Session: User-Member Linking UI Enhancement (2025-01-13)
|
||||||
|
|
||||||
### Feature Summary
|
### Feature Summary
|
||||||
|
|
@ -1559,8 +1586,8 @@ This project demonstrates a modern Phoenix application built with:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Document Version:** 1.2
|
**Document Version:** 1.3
|
||||||
**Last Updated:** 2025-11-27
|
**Last Updated:** 2025-12-02
|
||||||
**Maintainer:** Development Team
|
**Maintainer:** Development Team
|
||||||
**Status:** Living Document (update as project evolves)
|
**Status:** Living Document (update as project evolves)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@
|
||||||
- ✅ Sorting by basic fields
|
- ✅ Sorting by basic fields
|
||||||
- ✅ User-Member linking (optional 1:1)
|
- ✅ User-Member linking (optional 1:1)
|
||||||
- ✅ Email synchronization between User and Member
|
- ✅ Email synchronization between User and Member
|
||||||
|
- ✅ **Bulk email copy** - Copy selected members' email addresses to clipboard (Issue #230)
|
||||||
|
|
||||||
**Closed Issues:**
|
**Closed Issues:**
|
||||||
- ✅ [#162](https://git.local-it.org/local-it/mitgliederverwaltung/issues/162) - Fuzzy and substring search (closed 2025-11-12)
|
- ✅ [#162](https://git.local-it.org/local-it/mitgliederverwaltung/issues/162) - Fuzzy and substring search (closed 2025-11-12)
|
||||||
|
|
|
||||||
235
email-copy-feature.plan.md
Normal file
235
email-copy-feature.plan.md
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
# Bulk Email Copy Feature - Detaillierter Implementierungsplan
|
||||||
|
|
||||||
|
## Aktueller Stand
|
||||||
|
|
||||||
|
Die Checkbox-Funktionalität existiert bereits vollständig:
|
||||||
|
|
||||||
|
- `select_member` und `select_all` Events in [`lib/mv_web/live/member_live/index.ex`](lib/mv_web/live/member_live/index.ex) (Zeilen 91-117)
|
||||||
|
- Checkboxen im Template [`lib/mv_web/live/member_live/index.html.heex`](lib/mv_web/live/member_live/index.html.heex) (Zeilen 28-54)
|
||||||
|
- `@selected_members` enthält die UUIDs der ausgewählten Mitglieder als Liste
|
||||||
|
|
||||||
|
## Gewählte Implementierung: JavaScript Hook mit LiveView Event
|
||||||
|
|
||||||
|
**Ablauf:**
|
||||||
|
|
||||||
|
1. User wählt Mitglieder über Checkboxen aus
|
||||||
|
2. User klickt "E-Mail-Adressen kopieren" Button
|
||||||
|
3. LiveView Event `copy_emails` wird ausgelöst
|
||||||
|
4. Server filtert Member aus `@members` nach `@selected_members`
|
||||||
|
5. Server formatiert E-Mails im Format `Vorname Nachname <email>`
|
||||||
|
6. Server pusht `copy_to_clipboard` Event mit formatiertem String an Client
|
||||||
|
7. JavaScript Hook empfängt Event und kopiert via `navigator.clipboard.writeText()`
|
||||||
|
8. Server zeigt Flash-Nachricht mit Erfolgsbestätigung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementierungsschritte
|
||||||
|
|
||||||
|
### Schritt 1: JavaScript Hook erstellen
|
||||||
|
|
||||||
|
**Datei:** `assets/js/app.js`
|
||||||
|
|
||||||
|
- Neuen Hook `CopyToClipboard` zum bestehenden `Hooks` Objekt hinzufügen
|
||||||
|
- Hook lauscht auf `copy_to_clipboard` Event vom Server
|
||||||
|
- Nutzt `navigator.clipboard.writeText()` API für das Kopieren
|
||||||
|
- Fallback-Behandlung für Browser ohne Clipboard API (ältere Browser)
|
||||||
|
- Fehlerbehandlung bei fehlgeschlagenem Kopieren
|
||||||
|
|
||||||
|
### Schritt 2: LiveView Event Handler implementieren
|
||||||
|
|
||||||
|
**Datei:** `lib/mv_web/live/member_live/index.ex`
|
||||||
|
|
||||||
|
- Neuen `handle_event("copy_emails", ...)` Callback hinzufügen
|
||||||
|
- Member aus `@members` filtern, deren ID in `@selected_members` enthalten ist
|
||||||
|
- Jeden Member im Format `"Vorname Nachname <email>"` formatieren
|
||||||
|
- Formatierte Strings mit `"; "` (Semikolon + Leerzeichen) verbinden
|
||||||
|
- `push_event/3` nutzen um `copy_to_clipboard` Event zu senden
|
||||||
|
- `put_flash/3` für Erfolgsbestätigung mit Anzahl der kopierten Adressen
|
||||||
|
- Private Helper-Funktion für die E-Mail-Formatierung
|
||||||
|
|
||||||
|
### Schritt 3: UI Button hinzufügen
|
||||||
|
|
||||||
|
**Datei:** `lib/mv_web/live/member_live/index.html.heex`
|
||||||
|
|
||||||
|
- Button im Header-Bereich neben "New Member" Button platzieren
|
||||||
|
- Button nur anzeigen wenn mindestens ein Mitglied ausgewählt ist (`:if` Bedingung)
|
||||||
|
- `phx-hook="CopyToClipboard"` Attribut für JavaScript Hook Anbindung
|
||||||
|
- `phx-click="copy_emails"` für Event-Auslösung
|
||||||
|
- Icon: `hero-clipboard-document` oder `hero-envelope`
|
||||||
|
- Button-Text mit Anzahl der ausgewählten Mitglieder anzeigen
|
||||||
|
- Accessibility: `aria-label` für Screen Reader
|
||||||
|
|
||||||
|
### Schritt 4: Gettext Übersetzungen hinzufügen
|
||||||
|
|
||||||
|
**Dateien:**
|
||||||
|
|
||||||
|
- `priv/gettext/default.pot` - Template aktualisieren via `mix gettext.extract`
|
||||||
|
- `priv/gettext/de/LC_MESSAGES/default.po` - Deutsche Übersetzungen
|
||||||
|
- `priv/gettext/en/LC_MESSAGES/default.po` - Englische Übersetzungen (falls vorhanden)
|
||||||
|
|
||||||
|
**Zu übersetzende Strings:**
|
||||||
|
|
||||||
|
- Button-Text: "Copy Email Addresses"
|
||||||
|
- Flash-Nachricht Erfolg: "Copied %{count} email address(es) to clipboard"
|
||||||
|
- Flash-Nachricht Fehler: "No members selected"
|
||||||
|
|
||||||
|
### Schritt 5: Moduledoc aktualisieren
|
||||||
|
|
||||||
|
**Datei:** `lib/mv_web/live/member_live/index.ex`
|
||||||
|
|
||||||
|
- `@moduledoc` um neues Event `copy_emails` erweitern
|
||||||
|
- Dokumentation der Funktionalität hinzufügen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Edge Cases
|
||||||
|
|
||||||
|
### E1: Keine Mitglieder ausgewählt
|
||||||
|
|
||||||
|
- Button wird nicht angezeigt (UI-seitig gelöst)
|
||||||
|
- Falls Event dennoch ausgelöst wird: Error-Flash anzeigen, nichts kopieren
|
||||||
|
|
||||||
|
### E2: Ausgewählte Mitglieder nicht mehr in `@members` Liste
|
||||||
|
|
||||||
|
- Kann passieren wenn Member zwischenzeitlich gelöscht wurde
|
||||||
|
- Nur vorhandene Member verarbeiten, keine Fehler werfen
|
||||||
|
- Flash zeigt tatsächliche Anzahl kopierter Adressen
|
||||||
|
|
||||||
|
### E3: Member ohne E-Mail-Adresse
|
||||||
|
|
||||||
|
- Defensive Programmierung: Member ohne E-Mail überspringen
|
||||||
|
|
||||||
|
### E4: Member mit leerem Vor- oder Nachnamen
|
||||||
|
|
||||||
|
- Defensive Programmierung: Leere Namen graceful behandeln
|
||||||
|
|
||||||
|
### E5: Sonderzeichen in Namen
|
||||||
|
|
||||||
|
- Namen können Umlaute, Akzente, etc. enthalten
|
||||||
|
- Keine Escaping nötig, da Text direkt in Zwischenablage kopiert wird
|
||||||
|
- E-Mail-Clients verarbeiten Unicode korrekt
|
||||||
|
|
||||||
|
### E6: Sehr lange Liste (100+ Mitglieder)
|
||||||
|
|
||||||
|
- String kann sehr lang werden
|
||||||
|
- Clipboard API hat kein praktisches Limit
|
||||||
|
- Kein spezielles Handling nötig
|
||||||
|
|
||||||
|
### E7: Browser unterstützt Clipboard API nicht
|
||||||
|
|
||||||
|
- `navigator.clipboard` ist nicht in allen Browsern verfügbar
|
||||||
|
- Fallback: `document.execCommand('copy')` (deprecated aber breit unterstützt)
|
||||||
|
- Oder: Fehler-Flash anzeigen
|
||||||
|
|
||||||
|
### E8: Clipboard-Zugriff vom Browser blockiert
|
||||||
|
|
||||||
|
- Moderne Browser können Clipboard-Zugriff einschränken
|
||||||
|
- HTTPS erforderlich (in Produktion gegeben)
|
||||||
|
- User muss ggf. Berechtigung erteilen
|
||||||
|
- Fehlerbehandlung im Hook nötig
|
||||||
|
|
||||||
|
### E9: Parallel laufende Suche/Filter ändert `@members`
|
||||||
|
|
||||||
|
- User wählt Mitglieder, dann ändert Suche die Liste
|
||||||
|
- `@selected_members` bleibt erhalten, aber IDs passen nicht mehr zu `@members`
|
||||||
|
- Nur noch vorhandene (angezeigte) Members werden kopiert
|
||||||
|
- Entscheidung: Selection bei Suche beibehalten?
|
||||||
|
|
||||||
|
### E10: "Select All" nach Filterung
|
||||||
|
|
||||||
|
- Wenn gefiltert und "Select All" geklickt, werden nur sichtbare Members ausgewählt
|
||||||
|
- Bestehendes Verhalten, kein neues Problem
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testplan
|
||||||
|
|
||||||
|
### Unit Tests (index.ex)
|
||||||
|
|
||||||
|
**T1: copy_emails Event - Erfolgsfall**
|
||||||
|
|
||||||
|
- Setup: 3 Members in `@members`, 2 davon in `@selected_members`
|
||||||
|
- Assert: `push_event` wird mit korrektem String aufgerufen
|
||||||
|
- Assert: Flash-Nachricht mit count=2
|
||||||
|
|
||||||
|
**T2: copy_emails Event - Keine Auswahl**
|
||||||
|
|
||||||
|
- Setup: `@selected_members` ist leer
|
||||||
|
- Assert: Kein `push_event`
|
||||||
|
- Assert: Error-Flash oder keine Aktion
|
||||||
|
|
||||||
|
**T3: copy_emails Event - Alle ausgewählt**
|
||||||
|
|
||||||
|
- Setup: Alle Members in `@selected_members`
|
||||||
|
- Assert: Alle E-Mails im Output-String
|
||||||
|
|
||||||
|
**T4: E-Mail Formatierung**
|
||||||
|
|
||||||
|
- Assert: Format ist `"Vorname Nachname <email>"`
|
||||||
|
- Assert: Mehrere E-Mails mit `"; "` getrennt
|
||||||
|
|
||||||
|
**T5: Member mit Sonderzeichen im Namen**
|
||||||
|
|
||||||
|
- Setup: Member mit Name "Müller-Lüdenscheidt"
|
||||||
|
- Assert: Name wird korrekt übernommen
|
||||||
|
|
||||||
|
**T6: Teilweise nicht vorhandene Member**
|
||||||
|
|
||||||
|
- Setup: `@selected_members` enthält ID die nicht in `@members` ist
|
||||||
|
- Assert: Nur vorhandene Members werden verarbeitet, kein Crash
|
||||||
|
|
||||||
|
### LiveView Integration Tests
|
||||||
|
|
||||||
|
**T7: Button Sichtbarkeit**
|
||||||
|
|
||||||
|
- Assert: Button nicht sichtbar wenn `@selected_members` leer
|
||||||
|
- Assert: Button sichtbar wenn mindestens 1 Member ausgewählt
|
||||||
|
|
||||||
|
**T8: Button zeigt korrekte Anzahl**
|
||||||
|
|
||||||
|
- Setup: 3 Members ausgewählt
|
||||||
|
- Assert: Button-Text enthält "(3)"
|
||||||
|
|
||||||
|
**T9: Click löst Event aus**
|
||||||
|
|
||||||
|
- Action: Click auf Copy-Button
|
||||||
|
- Assert: `copy_emails` Event wird gesendet
|
||||||
|
|
||||||
|
**T10: Vollständiger Flow**
|
||||||
|
|
||||||
|
- Action: Member auswählen, Button klicken
|
||||||
|
- Assert: Flash-Nachricht erscheint
|
||||||
|
|
||||||
|
## Zu ändernde Dateien
|
||||||
|
|
||||||
|
| Datei | Änderungstyp |
|
||||||
|
|
||||||
|
|-------|--------------|
|
||||||
|
|
||||||
|
| `assets/js/app.js` | Hook hinzufügen |
|
||||||
|
|
||||||
|
| `lib/mv_web/live/member_live/index.ex` | Event Handler + Helper |
|
||||||
|
|
||||||
|
| `lib/mv_web/live/member_live/index.html.heex` | Button UI |
|
||||||
|
|
||||||
|
| `priv/gettext/de/LC_MESSAGES/default.po` | Übersetzungen |
|
||||||
|
|
||||||
|
| `test/mv_web/member_live/index_test.exs` | Tests |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E-Mail Output Format
|
||||||
|
|
||||||
|
**Einzelne E-Mail:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Max Mustermann <max@example.com>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mehrere E-Mails:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Max Mustermann <max@example.com>; Erika Musterfrau <erika@example.com>; Hans Müller <hans@example.com>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hinweis:** Semikolon als Trennzeichen ist Standard für E-Mail-Clients (Outlook, Thunderbird, etc.)
|
||||||
|
|
@ -18,6 +18,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
- `delete` - Remove a member from the database
|
- `delete` - Remove a member from the database
|
||||||
- `select_member` - Toggle individual member selection
|
- `select_member` - Toggle individual member selection
|
||||||
- `select_all` - Toggle selection of all visible members
|
- `select_all` - Toggle selection of all visible members
|
||||||
|
- `copy_emails` - Copy email addresses of selected members to clipboard
|
||||||
|
|
||||||
## Implementation Notes
|
## Implementation Notes
|
||||||
- Search uses PostgreSQL full-text search (plainto_tsquery)
|
- Search uses PostgreSQL full-text search (plainto_tsquery)
|
||||||
|
|
@ -116,6 +117,49 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
{:noreply, assign(socket, :selected_members, selected)}
|
{:noreply, assign(socket, :selected_members, selected)}
|
||||||
end
|
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
|
# Handle Infos from Child Components
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
|
|
@ -733,4 +777,22 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,16 @@
|
||||||
<.header>
|
<.header>
|
||||||
{gettext("Members")}
|
{gettext("Members")}
|
||||||
<:actions>
|
<: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"}>
|
<.button variant="primary" navigate={~p"/members/new"}>
|
||||||
<.icon name="hero-plus" /> {gettext("New Member")}
|
<.icon name="hero-plus" /> {gettext("New Member")}
|
||||||
</.button>
|
</.button>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ msgstr ""
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr "Aktionen"
|
msgstr "Aktionen"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:202
|
#: lib/mv_web/live/member_live/index.html.heex:212
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:72
|
#: lib/mv_web/live/user_live/index.html.heex:72
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Are you sure?"
|
msgid "Are you sure?"
|
||||||
|
|
@ -28,19 +28,19 @@ msgid "Attempting to reconnect"
|
||||||
msgstr "Verbindung wird wiederhergestellt"
|
msgstr "Verbindung wird wiederhergestellt"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:54
|
#: lib/mv_web/live/member_live/form.ex:54
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:148
|
#: lib/mv_web/live/member_live/index.html.heex:158
|
||||||
#: lib/mv_web/live/member_live/show.ex:59
|
#: lib/mv_web/live/member_live/show.ex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "City"
|
msgid "City"
|
||||||
msgstr "Stadt"
|
msgstr "Stadt"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:204
|
#: lib/mv_web/live/member_live/index.html.heex:214
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:74
|
#: lib/mv_web/live/user_live/index.html.heex:74
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr "Löschen"
|
msgstr "Löschen"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:196
|
#: lib/mv_web/live/member_live/index.html.heex:206
|
||||||
#: lib/mv_web/live/user_live/form.ex:265
|
#: lib/mv_web/live/user_live/form.ex:265
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:66
|
#: lib/mv_web/live/user_live/index.html.heex:66
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -54,7 +54,7 @@ msgid "Edit Member"
|
||||||
msgstr "Mitglied bearbeiten"
|
msgstr "Mitglied bearbeiten"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:47
|
#: lib/mv_web/live/member_live/form.ex:47
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:80
|
#: lib/mv_web/live/member_live/index.html.heex:90
|
||||||
#: lib/mv_web/live/member_live/show.ex:50
|
#: lib/mv_web/live/member_live/show.ex:50
|
||||||
#: lib/mv_web/live/user_live/form.ex:46
|
#: lib/mv_web/live/user_live/form.ex:46
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:44
|
#: lib/mv_web/live/user_live/index.html.heex:44
|
||||||
|
|
@ -70,7 +70,7 @@ msgid "First Name"
|
||||||
msgstr "Vorname"
|
msgstr "Vorname"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:51
|
#: lib/mv_web/live/member_live/form.ex:51
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:182
|
#: lib/mv_web/live/member_live/index.html.heex:192
|
||||||
#: lib/mv_web/live/member_live/show.ex:56
|
#: lib/mv_web/live/member_live/show.ex:56
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Join Date"
|
msgid "Join Date"
|
||||||
|
|
@ -82,12 +82,12 @@ msgstr "Beitrittsdatum"
|
||||||
msgid "Last Name"
|
msgid "Last Name"
|
||||||
msgstr "Nachname"
|
msgstr "Nachname"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:6
|
#: lib/mv_web/live/member_live/index.html.heex:16
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "New Member"
|
msgid "New Member"
|
||||||
msgstr "Neues Mitglied"
|
msgstr "Neues Mitglied"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:193
|
#: lib/mv_web/live/member_live/index.html.heex:203
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:63
|
#: lib/mv_web/live/user_live/index.html.heex:63
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Show"
|
msgid "Show"
|
||||||
|
|
@ -121,7 +121,7 @@ msgid "Exit Date"
|
||||||
msgstr "Austrittsdatum"
|
msgstr "Austrittsdatum"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:56
|
#: lib/mv_web/live/member_live/form.ex:56
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:114
|
#: lib/mv_web/live/member_live/index.html.heex:124
|
||||||
#: lib/mv_web/live/member_live/show.ex:61
|
#: lib/mv_web/live/member_live/show.ex:61
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "House Number"
|
msgid "House Number"
|
||||||
|
|
@ -140,14 +140,14 @@ msgid "Paid"
|
||||||
msgstr "Bezahlt"
|
msgstr "Bezahlt"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:50
|
#: lib/mv_web/live/member_live/form.ex:50
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:165
|
#: lib/mv_web/live/member_live/index.html.heex:175
|
||||||
#: lib/mv_web/live/member_live/show.ex:55
|
#: lib/mv_web/live/member_live/show.ex:55
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Phone Number"
|
msgid "Phone Number"
|
||||||
msgstr "Telefonnummer"
|
msgstr "Telefonnummer"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:57
|
#: lib/mv_web/live/member_live/form.ex:57
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:131
|
#: lib/mv_web/live/member_live/index.html.heex:141
|
||||||
#: lib/mv_web/live/member_live/show.ex:62
|
#: lib/mv_web/live/member_live/show.ex:62
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Postal Code"
|
msgid "Postal Code"
|
||||||
|
|
@ -168,7 +168,7 @@ msgid "Saving..."
|
||||||
msgstr "Speichern..."
|
msgstr "Speichern..."
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:55
|
#: lib/mv_web/live/member_live/form.ex:55
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:97
|
#: lib/mv_web/live/member_live/index.html.heex:107
|
||||||
#: lib/mv_web/live/member_live/show.ex:60
|
#: lib/mv_web/live/member_live/show.ex:60
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Street"
|
msgid "Street"
|
||||||
|
|
@ -311,7 +311,7 @@ msgid "Member"
|
||||||
msgstr "Mitglied"
|
msgstr "Mitglied"
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:25
|
#: lib/mv_web/components/layouts/navbar.ex:25
|
||||||
#: lib/mv_web/live/member_live/index.ex:57
|
#: lib/mv_web/live/member_live/index.ex:58
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:3
|
#: lib/mv_web/live/member_live/index.html.heex:3
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Members"
|
msgid "Members"
|
||||||
|
|
@ -365,12 +365,12 @@ msgstr "Profil"
|
||||||
msgid "Required"
|
msgid "Required"
|
||||||
msgstr "Erforderlich"
|
msgstr "Erforderlich"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:37
|
#: lib/mv_web/live/member_live/index.html.heex:47
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select all members"
|
msgid "Select all members"
|
||||||
msgstr "Alle Mitglieder auswählen"
|
msgstr "Alle Mitglieder auswählen"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:51
|
#: lib/mv_web/live/member_live/index.html.heex:61
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select member"
|
msgid "Select member"
|
||||||
msgstr "Mitglied auswählen"
|
msgstr "Mitglied auswählen"
|
||||||
|
|
@ -556,7 +556,7 @@ msgid "Toggle dark mode"
|
||||||
msgstr "Dunklen Modus umschalten"
|
msgstr "Dunklen Modus umschalten"
|
||||||
|
|
||||||
#: lib/mv_web/live/components/search_bar_component.ex:15
|
#: lib/mv_web/live/components/search_bar_component.ex:15
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:15
|
#: lib/mv_web/live/member_live/index.html.heex:25
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Search..."
|
msgid "Search..."
|
||||||
msgstr "Suchen..."
|
msgstr "Suchen..."
|
||||||
|
|
@ -572,7 +572,7 @@ msgstr "Benutzer*innen"
|
||||||
msgid "Click to sort"
|
msgid "Click to sort"
|
||||||
msgstr "Klicke um zu sortieren"
|
msgstr "Klicke um zu sortieren"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:63
|
#: lib/mv_web/live/member_live/index.html.heex:73
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "First name"
|
msgid "First name"
|
||||||
msgstr "Vorname"
|
msgstr "Vorname"
|
||||||
|
|
@ -782,7 +782,29 @@ msgstr "Mitglied entverknüpfen"
|
||||||
msgid "Unlinking scheduled"
|
msgid "Unlinking scheduled"
|
||||||
msgstr "Entverknüpfung geplant"
|
msgstr "Entverknüpfung geplant"
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/custom_field_live/index.ex:97
|
#: lib/mv_web/live/member_live/index.ex:150
|
||||||
#~ #, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
#~ msgid "To confirm deletion, please enter the custom field slug:"
|
msgid "Copied %{count} email address to clipboard"
|
||||||
#~ msgstr "Um die Löschung zu bestätigen, gib bitte den Slug des benutzerdefinierten Feldes ein:"
|
msgid_plural "Copied %{count} email addresses to clipboard"
|
||||||
|
msgstr[0] "%{count} E-Mail-Adresse in die Zwischenablage kopiert"
|
||||||
|
msgstr[1] "%{count} E-Mail-Adressen in die Zwischenablage kopiert"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:10
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
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:13
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Copy emails"
|
||||||
|
msgstr "E-Mails kopieren"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.ex:141
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "No email addresses found"
|
||||||
|
msgstr "Keine E-Mail-Adressen gefunden"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.ex:125
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "No members selected"
|
||||||
|
msgstr "Keine Mitglieder ausgewählt"
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ msgstr ""
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:202
|
#: lib/mv_web/live/member_live/index.html.heex:212
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:72
|
#: lib/mv_web/live/user_live/index.html.heex:72
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Are you sure?"
|
msgid "Are you sure?"
|
||||||
|
|
@ -29,19 +29,19 @@ msgid "Attempting to reconnect"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:54
|
#: lib/mv_web/live/member_live/form.ex:54
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:148
|
#: lib/mv_web/live/member_live/index.html.heex:158
|
||||||
#: lib/mv_web/live/member_live/show.ex:59
|
#: lib/mv_web/live/member_live/show.ex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "City"
|
msgid "City"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:204
|
#: lib/mv_web/live/member_live/index.html.heex:214
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:74
|
#: lib/mv_web/live/user_live/index.html.heex:74
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:196
|
#: lib/mv_web/live/member_live/index.html.heex:206
|
||||||
#: lib/mv_web/live/user_live/form.ex:265
|
#: lib/mv_web/live/user_live/form.ex:265
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:66
|
#: lib/mv_web/live/user_live/index.html.heex:66
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -55,7 +55,7 @@ msgid "Edit Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:47
|
#: lib/mv_web/live/member_live/form.ex:47
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:80
|
#: lib/mv_web/live/member_live/index.html.heex:90
|
||||||
#: lib/mv_web/live/member_live/show.ex:50
|
#: lib/mv_web/live/member_live/show.ex:50
|
||||||
#: lib/mv_web/live/user_live/form.ex:46
|
#: lib/mv_web/live/user_live/form.ex:46
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:44
|
#: lib/mv_web/live/user_live/index.html.heex:44
|
||||||
|
|
@ -71,7 +71,7 @@ msgid "First Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:51
|
#: lib/mv_web/live/member_live/form.ex:51
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:182
|
#: lib/mv_web/live/member_live/index.html.heex:192
|
||||||
#: lib/mv_web/live/member_live/show.ex:56
|
#: lib/mv_web/live/member_live/show.ex:56
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Join Date"
|
msgid "Join Date"
|
||||||
|
|
@ -83,12 +83,12 @@ msgstr ""
|
||||||
msgid "Last Name"
|
msgid "Last Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:6
|
#: lib/mv_web/live/member_live/index.html.heex:16
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "New Member"
|
msgid "New Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:193
|
#: lib/mv_web/live/member_live/index.html.heex:203
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:63
|
#: lib/mv_web/live/user_live/index.html.heex:63
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Show"
|
msgid "Show"
|
||||||
|
|
@ -122,7 +122,7 @@ msgid "Exit Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:56
|
#: lib/mv_web/live/member_live/form.ex:56
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:114
|
#: lib/mv_web/live/member_live/index.html.heex:124
|
||||||
#: lib/mv_web/live/member_live/show.ex:61
|
#: lib/mv_web/live/member_live/show.ex:61
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "House Number"
|
msgid "House Number"
|
||||||
|
|
@ -141,14 +141,14 @@ msgid "Paid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:50
|
#: lib/mv_web/live/member_live/form.ex:50
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:165
|
#: lib/mv_web/live/member_live/index.html.heex:175
|
||||||
#: lib/mv_web/live/member_live/show.ex:55
|
#: lib/mv_web/live/member_live/show.ex:55
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Phone Number"
|
msgid "Phone Number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:57
|
#: lib/mv_web/live/member_live/form.ex:57
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:131
|
#: lib/mv_web/live/member_live/index.html.heex:141
|
||||||
#: lib/mv_web/live/member_live/show.ex:62
|
#: lib/mv_web/live/member_live/show.ex:62
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Postal Code"
|
msgid "Postal Code"
|
||||||
|
|
@ -169,7 +169,7 @@ msgid "Saving..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:55
|
#: lib/mv_web/live/member_live/form.ex:55
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:97
|
#: lib/mv_web/live/member_live/index.html.heex:107
|
||||||
#: lib/mv_web/live/member_live/show.ex:60
|
#: lib/mv_web/live/member_live/show.ex:60
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Street"
|
msgid "Street"
|
||||||
|
|
@ -312,7 +312,7 @@ msgid "Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:25
|
#: lib/mv_web/components/layouts/navbar.ex:25
|
||||||
#: lib/mv_web/live/member_live/index.ex:57
|
#: lib/mv_web/live/member_live/index.ex:58
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:3
|
#: lib/mv_web/live/member_live/index.html.heex:3
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Members"
|
msgid "Members"
|
||||||
|
|
@ -366,12 +366,12 @@ msgstr ""
|
||||||
msgid "Required"
|
msgid "Required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:37
|
#: lib/mv_web/live/member_live/index.html.heex:47
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select all members"
|
msgid "Select all members"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:51
|
#: lib/mv_web/live/member_live/index.html.heex:61
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select member"
|
msgid "Select member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -557,7 +557,7 @@ msgid "Toggle dark mode"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/components/search_bar_component.ex:15
|
#: lib/mv_web/live/components/search_bar_component.ex:15
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:15
|
#: lib/mv_web/live/member_live/index.html.heex:25
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Search..."
|
msgid "Search..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -573,7 +573,7 @@ msgstr ""
|
||||||
msgid "Click to sort"
|
msgid "Click to sort"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:63
|
#: lib/mv_web/live/member_live/index.html.heex:73
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "First name"
|
msgid "First name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -782,3 +782,30 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Unlinking scheduled"
|
msgid "Unlinking scheduled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.ex:150
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Copied %{count} email address to clipboard"
|
||||||
|
msgid_plural "Copied %{count} email addresses to clipboard"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:10
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Copy email addresses of selected members"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:13
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Copy emails"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.ex:141
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "No email addresses found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.ex:125
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "No members selected"
|
||||||
|
msgstr ""
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ msgstr ""
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:202
|
#: lib/mv_web/live/member_live/index.html.heex:212
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:72
|
#: lib/mv_web/live/user_live/index.html.heex:72
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Are you sure?"
|
msgid "Are you sure?"
|
||||||
|
|
@ -29,19 +29,19 @@ msgid "Attempting to reconnect"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:54
|
#: lib/mv_web/live/member_live/form.ex:54
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:148
|
#: lib/mv_web/live/member_live/index.html.heex:158
|
||||||
#: lib/mv_web/live/member_live/show.ex:59
|
#: lib/mv_web/live/member_live/show.ex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "City"
|
msgid "City"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:204
|
#: lib/mv_web/live/member_live/index.html.heex:214
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:74
|
#: lib/mv_web/live/user_live/index.html.heex:74
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:196
|
#: lib/mv_web/live/member_live/index.html.heex:206
|
||||||
#: lib/mv_web/live/user_live/form.ex:265
|
#: lib/mv_web/live/user_live/form.ex:265
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:66
|
#: lib/mv_web/live/user_live/index.html.heex:66
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -55,7 +55,7 @@ msgid "Edit Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:47
|
#: lib/mv_web/live/member_live/form.ex:47
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:80
|
#: lib/mv_web/live/member_live/index.html.heex:90
|
||||||
#: lib/mv_web/live/member_live/show.ex:50
|
#: lib/mv_web/live/member_live/show.ex:50
|
||||||
#: lib/mv_web/live/user_live/form.ex:46
|
#: lib/mv_web/live/user_live/form.ex:46
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:44
|
#: lib/mv_web/live/user_live/index.html.heex:44
|
||||||
|
|
@ -71,7 +71,7 @@ msgid "First Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:51
|
#: lib/mv_web/live/member_live/form.ex:51
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:182
|
#: lib/mv_web/live/member_live/index.html.heex:192
|
||||||
#: lib/mv_web/live/member_live/show.ex:56
|
#: lib/mv_web/live/member_live/show.ex:56
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Join Date"
|
msgid "Join Date"
|
||||||
|
|
@ -83,12 +83,12 @@ msgstr ""
|
||||||
msgid "Last Name"
|
msgid "Last Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:6
|
#: lib/mv_web/live/member_live/index.html.heex:16
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "New Member"
|
msgid "New Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:193
|
#: lib/mv_web/live/member_live/index.html.heex:203
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:63
|
#: lib/mv_web/live/user_live/index.html.heex:63
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Show"
|
msgid "Show"
|
||||||
|
|
@ -122,7 +122,7 @@ msgid "Exit Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:56
|
#: lib/mv_web/live/member_live/form.ex:56
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:114
|
#: lib/mv_web/live/member_live/index.html.heex:124
|
||||||
#: lib/mv_web/live/member_live/show.ex:61
|
#: lib/mv_web/live/member_live/show.ex:61
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "House Number"
|
msgid "House Number"
|
||||||
|
|
@ -141,14 +141,14 @@ msgid "Paid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:50
|
#: lib/mv_web/live/member_live/form.ex:50
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:165
|
#: lib/mv_web/live/member_live/index.html.heex:175
|
||||||
#: lib/mv_web/live/member_live/show.ex:55
|
#: lib/mv_web/live/member_live/show.ex:55
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Phone Number"
|
msgid "Phone Number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:57
|
#: lib/mv_web/live/member_live/form.ex:57
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:131
|
#: lib/mv_web/live/member_live/index.html.heex:141
|
||||||
#: lib/mv_web/live/member_live/show.ex:62
|
#: lib/mv_web/live/member_live/show.ex:62
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Postal Code"
|
msgid "Postal Code"
|
||||||
|
|
@ -169,7 +169,7 @@ msgid "Saving..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:55
|
#: lib/mv_web/live/member_live/form.ex:55
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:97
|
#: lib/mv_web/live/member_live/index.html.heex:107
|
||||||
#: lib/mv_web/live/member_live/show.ex:60
|
#: lib/mv_web/live/member_live/show.ex:60
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Street"
|
msgid "Street"
|
||||||
|
|
@ -312,7 +312,7 @@ msgid "Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:25
|
#: lib/mv_web/components/layouts/navbar.ex:25
|
||||||
#: lib/mv_web/live/member_live/index.ex:57
|
#: lib/mv_web/live/member_live/index.ex:58
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:3
|
#: lib/mv_web/live/member_live/index.html.heex:3
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Members"
|
msgid "Members"
|
||||||
|
|
@ -366,12 +366,12 @@ msgstr ""
|
||||||
msgid "Required"
|
msgid "Required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:37
|
#: lib/mv_web/live/member_live/index.html.heex:47
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select all members"
|
msgid "Select all members"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:51
|
#: lib/mv_web/live/member_live/index.html.heex:61
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select member"
|
msgid "Select member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -557,7 +557,7 @@ msgid "Toggle dark mode"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/components/search_bar_component.ex:15
|
#: lib/mv_web/live/components/search_bar_component.ex:15
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:15
|
#: lib/mv_web/live/member_live/index.html.heex:25
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Search..."
|
msgid "Search..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -573,7 +573,7 @@ msgstr ""
|
||||||
msgid "Click to sort"
|
msgid "Click to sort"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:63
|
#: lib/mv_web/live/member_live/index.html.heex:73
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "First name"
|
msgid "First name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -783,7 +783,29 @@ msgstr ""
|
||||||
msgid "Unlinking scheduled"
|
msgid "Unlinking scheduled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/custom_field_live/index.ex:97
|
#: lib/mv_web/live/member_live/index.ex:150
|
||||||
#~ #, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
#~ msgid "To confirm deletion, please enter the custom field slug:"
|
msgid "Copied %{count} email address to clipboard"
|
||||||
#~ msgstr ""
|
msgid_plural "Copied %{count} email addresses to clipboard"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:10
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Copy email addresses of selected members"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:13
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Copy emails"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.ex:141
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "No email addresses found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.ex:125
|
||||||
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
msgid "No members selected"
|
||||||
|
msgstr ""
|
||||||
|
|
|
||||||
|
|
@ -249,4 +249,165 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
# Verify the member was actually deleted from the database
|
# Verify the member was actually deleted from the database
|
||||||
assert not (Mv.Membership.Member |> Ash.Query.filter(id == ^member.id) |> Ash.exists?())
|
assert not (Mv.Membership.Member |> Ash.Query.filter(id == ^member.id) |> Ash.exists?())
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "copy_emails feature" do
|
||||||
|
setup do
|
||||||
|
# Create test members
|
||||||
|
{:ok, member1} =
|
||||||
|
Mv.Membership.create_member(%{
|
||||||
|
first_name: "Max",
|
||||||
|
last_name: "Mustermann",
|
||||||
|
email: "max@example.com"
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, member2} =
|
||||||
|
Mv.Membership.create_member(%{
|
||||||
|
first_name: "Erika",
|
||||||
|
last_name: "Musterfrau",
|
||||||
|
email: "erika@example.com"
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, member3} =
|
||||||
|
Mv.Membership.create_member(%{
|
||||||
|
first_name: "Hans",
|
||||||
|
last_name: "Müller-Lüdenscheidt",
|
||||||
|
email: "hans@example.com"
|
||||||
|
})
|
||||||
|
|
||||||
|
%{member1: member1, member2: member2, member3: member3}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "copy_emails event formats selected members correctly", %{
|
||||||
|
conn: conn,
|
||||||
|
member1: member1,
|
||||||
|
member2: member2
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Select two members
|
||||||
|
view
|
||||||
|
|> element("[phx-click='select_member'][phx-value-id='#{member1.id}']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
view
|
||||||
|
|> element("[phx-click='select_member'][phx-value-id='#{member2.id}']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Trigger copy_emails event
|
||||||
|
view |> element("#copy-emails-btn") |> render_click()
|
||||||
|
|
||||||
|
# Verify flash message shows correct count
|
||||||
|
assert render(view) =~ "2"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "copy_emails event with no selection shows error flash", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Trigger copy_emails event directly (button not visible when no selection)
|
||||||
|
# This tests the edge case where event is triggered without selection
|
||||||
|
result = render_hook(view, "copy_emails", %{})
|
||||||
|
|
||||||
|
# Should show error flash
|
||||||
|
assert result =~ "No members selected" or result =~ "Keine Mitglieder"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "copy_emails event with all members selected formats all emails", %{
|
||||||
|
conn: conn
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Select all members via select_all
|
||||||
|
view |> element("[phx-click='select_all']") |> render_click()
|
||||||
|
|
||||||
|
# Trigger copy_emails event
|
||||||
|
view |> element("#copy-emails-btn") |> render_click()
|
||||||
|
|
||||||
|
# Verify flash message shows correct count (3 members)
|
||||||
|
assert render(view) =~ "3"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "copy_emails handles members with special characters in names", %{
|
||||||
|
conn: conn,
|
||||||
|
member3: member3
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Select member with umlauts
|
||||||
|
view
|
||||||
|
|> element("[phx-click='select_member'][phx-value-id='#{member3.id}']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Trigger copy_emails event - should not crash
|
||||||
|
view |> element("#copy-emails-btn") |> render_click()
|
||||||
|
|
||||||
|
# Verify flash message shows success
|
||||||
|
assert render(view) =~ "1"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "copy_emails handles case where selected members are deleted", %{
|
||||||
|
conn: conn,
|
||||||
|
member1: member1
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Select a member
|
||||||
|
view
|
||||||
|
|> element("[phx-click='select_member'][phx-value-id='#{member1.id}']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Click copy button - should work correctly
|
||||||
|
view |> element("#copy-emails-btn") |> render_click()
|
||||||
|
|
||||||
|
# Should show count of actual members found (1)
|
||||||
|
assert render(view) =~ "1"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "copy button is not visible when no members are selected", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Ensure no members are selected (default state)
|
||||||
|
refute has_element?(view, "#copy-emails-btn")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "copy button is visible when members are selected", %{
|
||||||
|
conn: conn,
|
||||||
|
member1: member1
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Select a member
|
||||||
|
view
|
||||||
|
|> element("[phx-click='select_member'][phx-value-id='#{member1.id}']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Button should now be visible
|
||||||
|
assert has_element?(view, "#copy-emails-btn")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "copy button click triggers event and shows flash", %{
|
||||||
|
conn: conn,
|
||||||
|
member1: member1
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Select a member
|
||||||
|
view
|
||||||
|
|> element("[phx-click='select_member'][phx-value-id='#{member1.id}']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Click copy button
|
||||||
|
view |> element("#copy-emails-btn") |> render_click()
|
||||||
|
|
||||||
|
# Flash message should appear
|
||||||
|
assert has_element?(view, "#flash-group")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue