diff --git a/.drone.yml b/.drone.yml index 8bcd48e..ed66e3e 100644 --- a/.drone.yml +++ b/.drone.yml @@ -273,7 +273,7 @@ environment: steps: - name: renovate - image: renovate/renovate:43.24 + image: renovate/renovate:43.19 environment: RENOVATE_CONFIG_FILE: "renovate_backend_config.js" RENOVATE_TOKEN: diff --git a/docs/groups-architecture.md b/docs/groups-architecture.md index 0e59409..27d9d18 100644 --- a/docs/groups-architecture.md +++ b/docs/groups-architecture.md @@ -356,9 +356,9 @@ lib/ - Screen readers must be able to navigate and understand the interface - ARIA labels and roles must be properly set -**Group Badges and Links in Member Overview / Detail:** -- Use `aria-label` to indicate group membership (e.g. "Member of group X"). Do not use `role="status"` on badges or links—that role is for live regions (screen reader announcements), not for navigation or static labels. -- Badge/link text or title should indicate group membership for screen readers. +**Group Badges in Member Overview:** +- Badges must have `role="status"` and appropriate `aria-label` attributes +- Badge title should indicate group membership **Clickable Group Badge (for filtering) - Optional:** @@ -961,8 +961,6 @@ Each functional unit can be implemented as a **separate issue**: ### Issue 4: Member Detail - Groups Display **Type:** Frontend **Estimation:** 1-2h -**Status:** Implemented (Groups as data field in Personal Data, below Linked User; button-style links to `/groups/:slug`). - **Tasks:** - Add groups section to member show - Display group badges diff --git a/lib/mv/membership/import/import_runner.ex b/lib/mv/membership/import/import_runner.ex index eccd75f..5ff846b 100644 --- a/lib/mv/membership/import/import_runner.ex +++ b/lib/mv/membership/import/import_runner.ex @@ -1,7 +1,7 @@ defmodule Mv.Membership.Import.ImportRunner do @moduledoc """ Orchestrates CSV member import: file reading, progress tracking, chunk processing, - and error formatting. Used by `MvWeb.ImportLive` to keep LiveView thin. + and error formatting. Used by `MvWeb.ImportExportLive` to keep LiveView thin. This module does not depend on Phoenix or LiveView. It provides pure functions for progress/merge and side-effectful helpers (read_file_entry, process_chunk) that diff --git a/lib/mv_web/components/layouts/sidebar.ex b/lib/mv_web/components/layouts/sidebar.ex index 8ed7f03..1896f24 100644 --- a/lib/mv_web/components/layouts/sidebar.ex +++ b/lib/mv_web/components/layouts/sidebar.ex @@ -118,7 +118,7 @@ defmodule MvWeb.Layouts.Sidebar do /> <% end %> <%= if can_access_page?(@current_user, PagePaths.settings()) do %> - <.menu_subitem href={~p"/admin/import"} label={gettext("Import")} /> + <.menu_subitem href={~p"/admin/import-export"} label={gettext("Import/Export")} /> <.menu_subitem href={~p"/settings"} label={gettext("Settings")} /> <% end %> diff --git a/lib/mv_web/live/custom_field_live/index_component.ex b/lib/mv_web/live/custom_field_live/index_component.ex index a670a3e..5cf0f6b 100644 --- a/lib/mv_web/live/custom_field_live/index_component.ex +++ b/lib/mv_web/live/custom_field_live/index_component.ex @@ -219,9 +219,6 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do send(self(), {:editing_section_changed, nil}) end - # Get actor from assigns or fall back to socket assigns - actor = Map.get(assigns, :actor, socket.assigns[:actor]) - {:ok, socket |> assign(assigns) @@ -231,7 +228,7 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do |> assign_new(:show_delete_modal, fn -> false end) |> assign_new(:custom_field_to_delete, fn -> nil end) |> assign_new(:slug_confirmation, fn -> "" end) - |> stream(:custom_fields, stream_custom_fields(actor, self()), reset: true)} + |> stream(:custom_fields, stream_custom_fields(assigns[:actor], self()), reset: true)} end @impl true diff --git a/lib/mv_web/live/import_live.ex b/lib/mv_web/live/import_export_live.ex similarity index 94% rename from lib/mv_web/live/import_live.ex rename to lib/mv_web/live/import_export_live.ex index e97ecd7..fe5484c 100644 --- a/lib/mv_web/live/import_live.ex +++ b/lib/mv_web/live/import_export_live.ex @@ -1,6 +1,6 @@ -defmodule MvWeb.ImportLive do +defmodule MvWeb.ImportExportLive do @moduledoc """ - LiveView for importing members via CSV. + LiveView for importing and exporting members via CSV. ## Features - CSV member import (admin only) @@ -38,7 +38,7 @@ defmodule MvWeb.ImportLive do alias Mv.Membership.Import.ImportRunner alias Mv.Membership.Import.MemberCSV alias MvWeb.Authorization - alias MvWeb.ImportLive.Components + alias MvWeb.ImportExportLive.Components on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded} @@ -65,7 +65,7 @@ defmodule MvWeb.ImportLive do socket = socket - |> assign(:page_title, gettext("Import")) + |> assign(:page_title, gettext("Import/Export")) |> assign(:club_name, club_name) |> assign(:import_state, nil) |> assign(:import_progress, nil) @@ -90,6 +90,13 @@ defmodule MvWeb.ImportLive do def render(assigns) do ~H""" + <.header> + {gettext("Import/Export")} + <:subtitle> + {gettext("Import members from CSV files or export member data.")} + + + <%= if Authorization.can?(@current_user, :create, Mv.Membership.Member) do %> <%!-- CSV Import Section --%> <.form_section title={gettext("Import Members (CSV)")}> @@ -100,6 +107,18 @@ defmodule MvWeb.ImportLive do <% end %> + + <%!-- Export Section (Placeholder) --%> + <.form_section title={gettext("Export Members (CSV)")}> +
+ <.icon name="hero-information-circle" class="size-5" aria-hidden="true" /> +
+

+ {gettext("Export functionality will be available in a future release.")} +

+
+
+ <% else %> <% end %> - <%!-- Groups (in Personal Data) --%> - <% groups = @member.groups || [] %> -
- <.data_field label={gettext("Groups")}> - <%= if Enum.empty?(groups) do %> - {gettext("No groups")} - <% else %> -
- <%= for group <- groups do %> - <.link - navigate={~p"/groups/#{group.slug}"} - class="btn btn-xs btn-outline btn-primary" - aria-label={gettext("Member of group %{name}", name: group.name)} - > - {group.name} - - <% end %> -
- <% end %> - -
- <%!-- Notes --%> <%= if @member.notes && String.trim(@member.notes) != "" do %>
@@ -286,8 +262,7 @@ defmodule MvWeb.MemberLive.Show do :user, :membership_fee_type, custom_field_values: [:custom_field], - membership_fee_cycles: [:membership_fee_type], - groups: [:id, :name, :slug] + membership_fee_cycles: [:membership_fee_type] ]) member = Ash.read_one!(query, actor: actor) diff --git a/lib/mv_web/router.ex b/lib/mv_web/router.ex index ec90f1b..61532ff 100644 --- a/lib/mv_web/router.ex +++ b/lib/mv_web/router.ex @@ -91,8 +91,8 @@ defmodule MvWeb.Router do live "/admin/roles/:id", RoleLive.Show, :show live "/admin/roles/:id/edit", RoleLive.Form, :edit - # Import (Admin only) - live "/admin/import", ImportLive + # Import/Export (Admin only) + live "/admin/import-export", ImportExportLive post "/members/export.csv", MemberExportController, :export post "/members/export.pdf", MemberPdfExportController, :export diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 6dbb732..1784d4b 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -144,7 +144,6 @@ msgid "House Number" msgstr "Hausnummer" #: lib/mv_web/live/member_live/form.ex -#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/show.ex #: lib/mv_web/translations/member_fields.ex #, elixir-autogen, elixir-format @@ -1543,7 +1542,6 @@ msgstr "Du bist dabei alle %{count} Zyklen für dieses Mitglied zu löschen." msgid "Delete Membership Fee Type" msgstr "Mitgliedsbeitragsart löschen" -#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/translations/member_fields.ex #, elixir-autogen, elixir-format, fuzzy msgid "Membership Fee Start Date" @@ -1861,7 +1859,7 @@ msgstr "erstellt" msgid "updated" msgstr "aktualisiert" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/user_live/form.ex #, elixir-autogen, elixir-format msgid "Unknown error" @@ -1982,37 +1980,37 @@ msgstr "Bezahlstatus" msgid "Reset" msgstr "Zurücksetzen" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid " (Field: %{field})" msgstr " (Datenfeld: %{field})" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "CSV File" msgstr "CSV Datei" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Download CSV templates:" msgstr "CSV Vorlagen herunterladen:" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "English Template" msgstr "Englische Vorlage" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Error list truncated to %{count} entries" msgstr "Liste der Fehler auf %{count} Einträge reduziert" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Errors" msgstr "Fehler" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format msgid "Failed to prepare CSV import: %{reason}" msgstr "Das Vorbereiten des CSV Imports ist gescheitert: %{reason}" @@ -2027,27 +2025,27 @@ msgstr "Das Importieren von %{idx} ist gescheitert: %{reason}" msgid "Failed to read file: %{reason}" msgstr "Fehler beim Lesen der Datei: %{reason}" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Failed: %{count} row(s)" msgstr "Fehlgeschlagen: %{count} Zeile(n)" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "German Template" msgstr "Deutsche Vorlage" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format msgid "Import Members (CSV)" msgstr "Mitglieder importieren (CSV)" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Import Results" msgstr "Import-Ergebnisse" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format msgid "Import is already running. Please wait for it to complete." msgstr "Import läuft bereits. Bitte warte, bis er abgeschlossen ist." @@ -2062,7 +2060,7 @@ msgstr "Import-Status fehlt. Chunk %{idx} kann nicht verarbeitet werden." msgid "Invalid chunk index: %{idx}" msgstr "Ungültiger Chunk-Index: %{idx}" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Line %{line}: %{message}" msgstr "Zeile %{line}: %{message}" @@ -2072,47 +2070,47 @@ msgstr "Zeile %{line}: %{message}" msgid "No file was uploaded" msgstr "Es wurde keine Datei hochgeladen" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format msgid "Only administrators can import members from CSV files." msgstr "Nur Administrator*innen können Mitglieder aus CSV-Dateien importieren." -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format msgid "Please select a CSV file to import." msgstr "Bitte wähle eine CSV-Datei zum Importieren." -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format msgid "Please wait for the file upload to complete before starting the import." msgstr "Bitte warte, bis der Datei-Upload abgeschlossen ist, bevor du den Import startest." -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Processing chunk %{current} of %{total}..." msgstr "Verarbeite Chunk %{current} von %{total}..." -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Start Import" msgstr "Import starten" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Starting import..." msgstr "Import wird gestartet..." -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Successfully inserted: %{count} member(s)" msgstr "Erfolgreich eingefügt: %{count} Mitglied(er)" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Summary" msgstr "Zusammenfassung" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format, fuzzy msgid "Warnings" msgstr "Warnungen" @@ -2204,13 +2202,11 @@ msgstr "Gruppe erfolgreich gespeichert." #: lib/mv_web/live/components/member_filter_component.ex #: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/member_live/index.html.heex -#: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "Groups" msgstr "Gruppen" #: lib/mv_web/live/group_live/index.ex -#: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "No groups" msgstr "Keine Gruppen" @@ -2329,7 +2325,7 @@ msgstr "%{name} entfernen" msgid "Some members could not be added: %{errors}" msgstr "Einige Mitglieder konnten nicht hinzugefügt werden: %{errors}" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format, fuzzy msgid "CSV files only, maximum %{size} MB" msgstr "Nur CSV Dateien, maximal %{size} MB" @@ -2359,22 +2355,43 @@ msgstr "Datenfeld: %{name} – erwartet %{type}, erhalten: %{value}" msgid "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing." msgstr "Unbekannte Spalte '%{header}' wird ignoriert. Falls dies ein Datenfeld ist, erstelle es in Mila vor dem Import." +#: lib/mv_web/live/import_export_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Export Members (CSV)" +msgstr "Mitglieder importieren (CSV)" + +#: lib/mv_web/live/import_export_live.ex +#, elixir-autogen, elixir-format +msgid "Export functionality will be available in a future release." +msgstr "Export-Funktionalität ist im nächsten release verfügbar." + #: lib/mv/membership/import/import_runner.ex #, elixir-autogen, elixir-format, fuzzy msgid "Failed to read uploaded file: unexpected format" msgstr "Fehler beim Lesen der hochgeladenen Datei" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex +#, elixir-autogen, elixir-format +msgid "Import members from CSV files or export member data." +msgstr "Importiere Mitglieder aus CSV-Dateien oder exportiere Mitgliederdaten." + +#: lib/mv_web/components/layouts/sidebar.ex +#: lib/mv_web/live/import_export_live.ex +#, elixir-autogen, elixir-format +msgid "Import/Export" +msgstr "Import/Export" + +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format, fuzzy msgid "You do not have permission to access this page." msgstr "Du hast keine Berechtigung, auf diese Seite zuzugreifen." -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format, fuzzy msgid "Manage Member Data" msgstr "Mitgliederdaten verwalten" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format, fuzzy msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, so they must be listed in the list of member data (like e-mail or first name). Unknown data field columns will be ignored with a warning." msgstr "Verwende die Namen der Datenfelder als Spaltennamen in der CSV Datei. Datenfelder müssen in Mila bereits angelegt sein, damit sie importiert werden können. sie müssen in der Liste der Mitgliederdaten als Datenfeld enthalten sein (z.B. E-Mail). Spalten mit unbekannten Spaltenüberschriften werden mit einer Warnung ignoriert." @@ -2436,7 +2453,7 @@ msgstr "SSO / OIDC Nutzer*in" msgid "This user is linked via SSO (Single Sign-On). A password set or changed here only affects login with email and password in this application. It does not change the password in your identity provider (e.g. Authentik). To change the SSO password, use the identity provider or your organization's IT." msgstr "Diese*r Nutzer*in ist über SSO (Single Sign-On) verbunden. Ein hier festgelegtes oder geändertes Passwort wirkt sich nur auf die Anmeldung mit E-Mail-Adresse und Passwort in dieser Anwendung aus. Es ändert nicht das Passwort in Eurem Identitätsanbieter (z. B. Authentik). Um das SSO-Passwort zu ändern, wende dich an den Identitätsanbieter oder die IT-Abteilung Ihrer Organisation." -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Import aborted" msgstr "Import abgebrochen" @@ -2457,7 +2474,6 @@ msgid "unpaid" msgstr "Unbezahlt" #: lib/mv_web/live/member_live/index.html.heex -#: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "Member of group %{name}" msgstr "Mitglied der Gruppe %{name}" @@ -2598,23 +2614,11 @@ msgstr "Anzahl Mitglieder:" msgid "PDF" msgstr "PDF" -#: lib/mv_web/components/layouts/sidebar.ex -#: lib/mv_web/live/import_live.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Import" -msgstr "Import" - -#~ #: lib/mv_web/live/import_export_live.ex +#~ #: lib/mv_web/live/global_settings_live.ex #~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Export Members (CSV)" -#~ msgstr "Mitglieder exportieren (CSV)" +#~ msgid "Custom Fields in CSV Import" +#~ msgstr "Benutzerdefinierte Felder" -#~ #: lib/mv_web/live/import_export_live.ex #~ #, elixir-autogen, elixir-format -#~ msgid "Export functionality will be available in a future release." -#~ msgstr "Export-Funktionalität ist im nächsten release verfügbar." - -#~ #: lib/mv_web/live/import_export_live.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Import members from CSV files or export member data." -#~ msgstr "Importiere Mitglieder aus CSV-Dateien oder exportiere Mitgliederdaten." +#~ msgid "Failed to prepare CSV import: %{error}" +#~ msgstr "Das Vorbereiten des CSV Imports ist gescheitert: %{error}" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index df282f3..af24afd 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -145,7 +145,6 @@ msgid "House Number" msgstr "" #: lib/mv_web/live/member_live/form.ex -#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/show.ex #: lib/mv_web/translations/member_fields.ex #, elixir-autogen, elixir-format @@ -1544,7 +1543,6 @@ msgstr "" msgid "Delete Membership Fee Type" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/translations/member_fields.ex #, elixir-autogen, elixir-format msgid "Membership Fee Start Date" @@ -1862,7 +1860,7 @@ msgstr "" msgid "updated" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/user_live/form.ex #, elixir-autogen, elixir-format msgid "Unknown error" @@ -1983,37 +1981,37 @@ msgstr "" msgid "Reset" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid " (Field: %{field})" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "CSV File" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Download CSV templates:" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "English Template" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Error list truncated to %{count} entries" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Errors" msgstr "" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format msgid "Failed to prepare CSV import: %{reason}" msgstr "" @@ -2028,27 +2026,27 @@ msgstr "" msgid "Failed to read file: %{reason}" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Failed: %{count} row(s)" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "German Template" msgstr "" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format msgid "Import Members (CSV)" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Import Results" msgstr "" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format msgid "Import is already running. Please wait for it to complete." msgstr "" @@ -2063,7 +2061,7 @@ msgstr "" msgid "Invalid chunk index: %{idx}" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Line %{line}: %{message}" msgstr "" @@ -2073,47 +2071,47 @@ msgstr "" msgid "No file was uploaded" msgstr "" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format msgid "Only administrators can import members from CSV files." msgstr "" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format msgid "Please select a CSV file to import." msgstr "" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format msgid "Please wait for the file upload to complete before starting the import." msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Processing chunk %{current} of %{total}..." msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Start Import" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Starting import..." msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Successfully inserted: %{count} member(s)" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Summary" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Warnings" msgstr "" @@ -2205,13 +2203,11 @@ msgstr "" #: lib/mv_web/live/components/member_filter_component.ex #: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/member_live/index.html.heex -#: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "Groups" msgstr "" #: lib/mv_web/live/group_live/index.ex -#: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "No groups" msgstr "" @@ -2330,7 +2326,7 @@ msgstr "" msgid "Some members could not be added: %{errors}" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format, fuzzy msgid "CSV files only, maximum %{size} MB" msgstr "" @@ -2360,22 +2356,43 @@ msgstr "" msgid "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing." msgstr "" +#: lib/mv_web/live/import_export_live.ex +#, elixir-autogen, elixir-format +msgid "Export Members (CSV)" +msgstr "" + +#: lib/mv_web/live/import_export_live.ex +#, elixir-autogen, elixir-format +msgid "Export functionality will be available in a future release." +msgstr "" + #: lib/mv/membership/import/import_runner.ex #, elixir-autogen, elixir-format msgid "Failed to read uploaded file: unexpected format" msgstr "" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex +#, elixir-autogen, elixir-format +msgid "Import members from CSV files or export member data." +msgstr "" + +#: lib/mv_web/components/layouts/sidebar.ex +#: lib/mv_web/live/import_export_live.ex +#, elixir-autogen, elixir-format +msgid "Import/Export" +msgstr "" + +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format msgid "You do not have permission to access this page." msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Manage Member Data" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, so they must be listed in the list of member data (like e-mail or first name). Unknown data field columns will be ignored with a warning." msgstr "" @@ -2437,7 +2454,7 @@ msgstr "" msgid "This user is linked via SSO (Single Sign-On). A password set or changed here only affects login with email and password in this application. It does not change the password in your identity provider (e.g. Authentik). To change the SSO password, use the identity provider or your organization's IT." msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Import aborted" msgstr "" @@ -2458,7 +2475,6 @@ msgid "unpaid" msgstr "" #: lib/mv_web/live/member_live/index.html.heex -#: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "Member of group %{name}" msgstr "" @@ -2598,9 +2614,3 @@ msgstr "" #, elixir-autogen, elixir-format msgid "PDF" msgstr "" - -#: lib/mv_web/components/layouts/sidebar.ex -#: lib/mv_web/live/import_live.ex -#, elixir-autogen, elixir-format -msgid "Import" -msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 56f897d..88da6ff 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -145,7 +145,6 @@ msgid "House Number" msgstr "" #: lib/mv_web/live/member_live/form.ex -#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/show.ex #: lib/mv_web/translations/member_fields.ex #, elixir-autogen, elixir-format @@ -1544,7 +1543,6 @@ msgstr "" msgid "Delete Membership Fee Type" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/translations/member_fields.ex #, elixir-autogen, elixir-format, fuzzy msgid "Membership Fee Start Date" @@ -1862,7 +1860,7 @@ msgstr "" msgid "updated" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #: lib/mv_web/live/user_live/form.ex #, elixir-autogen, elixir-format msgid "Unknown error" @@ -1983,37 +1981,37 @@ msgstr "" msgid "Reset" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid " (Field: %{field})" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "CSV File" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Download CSV templates:" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "English Template" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Error list truncated to %{count} entries" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Errors" msgstr "" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format msgid "Failed to prepare CSV import: %{reason}" msgstr "" @@ -2028,27 +2026,27 @@ msgstr "" msgid "Failed to read file: %{reason}" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Failed: %{count} row(s)" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "German Template" msgstr "" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format msgid "Import Members (CSV)" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Import Results" msgstr "" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format msgid "Import is already running. Please wait for it to complete." msgstr "" @@ -2063,7 +2061,7 @@ msgstr "" msgid "Invalid chunk index: %{idx}" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Line %{line}: %{message}" msgstr "" @@ -2073,47 +2071,47 @@ msgstr "" msgid "No file was uploaded" msgstr "" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format msgid "Only administrators can import members from CSV files." msgstr "" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format, fuzzy msgid "Please select a CSV file to import." msgstr "" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format msgid "Please wait for the file upload to complete before starting the import." msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Processing chunk %{current} of %{total}..." msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Start Import" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Starting import..." msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Successfully inserted: %{count} member(s)" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Summary" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format, fuzzy msgid "Warnings" msgstr "" @@ -2205,13 +2203,11 @@ msgstr "" #: lib/mv_web/live/components/member_filter_component.ex #: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/member_live/index.html.heex -#: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "Groups" msgstr "" #: lib/mv_web/live/group_live/index.ex -#: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "No groups" msgstr "" @@ -2330,7 +2326,7 @@ msgstr "" msgid "Some members could not be added: %{errors}" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format, fuzzy msgid "CSV files only, maximum %{size} MB" msgstr "" @@ -2360,22 +2356,43 @@ msgstr "" msgid "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing." msgstr "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing." +#: lib/mv_web/live/import_export_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "Export Members (CSV)" +msgstr "" + +#: lib/mv_web/live/import_export_live.ex +#, elixir-autogen, elixir-format +msgid "Export functionality will be available in a future release." +msgstr "" + #: lib/mv/membership/import/import_runner.ex #, elixir-autogen, elixir-format, fuzzy msgid "Failed to read uploaded file: unexpected format" msgstr "" -#: lib/mv_web/live/import_live.ex +#: lib/mv_web/live/import_export_live.ex +#, elixir-autogen, elixir-format +msgid "Import members from CSV files or export member data." +msgstr "" + +#: lib/mv_web/components/layouts/sidebar.ex +#: lib/mv_web/live/import_export_live.ex +#, elixir-autogen, elixir-format +msgid "Import/Export" +msgstr "" + +#: lib/mv_web/live/import_export_live.ex #, elixir-autogen, elixir-format, fuzzy msgid "You do not have permission to access this page." msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format, fuzzy msgid "Manage Member Data" msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format, fuzzy msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, so they must be listed in the list of member data (like e-mail or first name). Unknown data field columns will be ignored with a warning." msgstr "" @@ -2437,7 +2454,7 @@ msgstr "" msgid "This user is linked via SSO (Single Sign-On). A password set or changed here only affects login with email and password in this application. It does not change the password in your identity provider (e.g. Authentik). To change the SSO password, use the identity provider or your organization's IT." msgstr "" -#: lib/mv_web/live/import_live/components.ex +#: lib/mv_web/live/import_export_live/components.ex #, elixir-autogen, elixir-format msgid "Import aborted" msgstr "" @@ -2458,7 +2475,6 @@ msgid "unpaid" msgstr "" #: lib/mv_web/live/member_live/index.html.heex -#: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "Member of group %{name}" msgstr "" @@ -2599,23 +2615,7 @@ msgstr "Member count:" msgid "PDF" msgstr "" -#: lib/mv_web/components/layouts/sidebar.ex -#: lib/mv_web/live/import_live.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Import" -msgstr "" - -#~ #: lib/mv_web/live/import_export_live.ex +#~ #: lib/mv_web/live/global_settings_live.ex #~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Export Members (CSV)" -#~ msgstr "" - -#~ #: lib/mv_web/live/import_export_live.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Export functionality will be available in a future release." -#~ msgstr "" - -#~ #: lib/mv_web/live/import_export_live.ex -#~ #, elixir-autogen, elixir-format -#~ msgid "Import members from CSV files or export member data." +#~ msgid "Custom Fields in CSV Import" #~ msgstr "" diff --git a/test/mv_web/live/global_settings_live_config_test.exs b/test/mv_web/live/global_settings_live_config_test.exs index 9ac75fd..73f831f 100644 --- a/test/mv_web/live/global_settings_live_config_test.exs +++ b/test/mv_web/live/global_settings_live_config_test.exs @@ -42,7 +42,7 @@ defmodule MvWeb.GlobalSettingsLiveConfigTest do # Arrange: Set custom row limit to 500 Application.put_env(:mv, :csv_import, max_rows: 500) - {:ok, view, _html} = live(conn, ~p"/admin/import") + {:ok, view, _html} = live(conn, ~p"/admin/import-export") # Generate CSV with 501 rows (exceeding custom limit of 500) header = "first_name;last_name;email;street;postal_code;city\n" diff --git a/test/mv_web/live/import_live_test.exs b/test/mv_web/live/import_export_live_test.exs similarity index 87% rename from test/mv_web/live/import_live_test.exs rename to test/mv_web/live/import_export_live_test.exs index 48fbb11..d0d20e1 100644 --- a/test/mv_web/live/import_live_test.exs +++ b/test/mv_web/live/import_export_live_test.exs @@ -1,6 +1,6 @@ -defmodule MvWeb.ImportLiveTest do +defmodule MvWeb.ImportExportLiveTest do @moduledoc """ - Tests for Import LiveView: authorization (business rule), CSV import integration, + Tests for Import/Export LiveView: authorization (business rule), CSV import integration, and minimal UI smoke tests. CSV parsing/validation logic is covered by Mv.Membership.Import.MemberCSVTest; here we verify access control and end-to-end outcomes. """ @@ -31,7 +31,7 @@ defmodule MvWeb.ImportLiveTest do # ---------- Business logic: Authorization ---------- describe "Authorization" do - test "non-admin user cannot access import page and sees permission error", %{ + test "non-admin user cannot access import/export page and sees permission error", %{ conn: conn } do member_user = Mv.Fixtures.user_with_role_fixture("own_data") @@ -42,7 +42,7 @@ defmodule MvWeb.ImportLiveTest do |> put_locale_en() assert {:error, {:redirect, %{to: redirect_path, flash: %{"error" => msg}}}} = - live(conn, ~p"/admin/import") + live(conn, ~p"/admin/import-export") assert redirect_path =~ "/users/" assert msg =~ "don't have permission" @@ -55,7 +55,7 @@ defmodule MvWeb.ImportLiveTest do Path.join([__DIR__, "..", "..", "fixtures", "valid_member_import.csv"]) |> File.read!() - {:ok, view, _html} = live(conn, ~p"/admin/import") + {:ok, view, _html} = live(conn, ~p"/admin/import-export") upload_csv_file(view, csv_content) submit_import(view) wait_for_import_completion() @@ -109,7 +109,7 @@ defmodule MvWeb.ImportLiveTest do end test "invalid CSV shows user-friendly prepare error", %{conn: conn} do - {:ok, view, _html} = live(conn, ~p"/admin/import") + {:ok, view, _html} = live(conn, ~p"/admin/import-export") upload_csv_file(view, "invalid_header\nincomplete_row", "invalid.csv") submit_import(view) html = render(view) @@ -120,7 +120,7 @@ defmodule MvWeb.ImportLiveTest do conn: conn, invalid_csv: csv_content } do - {:ok, view, _html} = live(conn, ~p"/admin/import") + {:ok, view, _html} = live(conn, ~p"/admin/import-export") upload_csv_file(view, csv_content, "invalid_import.csv") submit_import(view) wait_for_import_completion() @@ -135,7 +135,7 @@ defmodule MvWeb.ImportLiveTest do end test "error list is capped and truncation message is shown", %{conn: conn} do - {:ok, view, _html} = live(conn, ~p"/admin/import") + {:ok, view, _html} = live(conn, ~p"/admin/import-export") header = "first_name;last_name;email;street;postal_code;city\n" invalid_rows = @@ -153,7 +153,7 @@ defmodule MvWeb.ImportLiveTest do end test "row limit is enforced (1001 rows rejected)", %{conn: conn} do - {:ok, view, _html} = live(conn, ~p"/admin/import") + {:ok, view, _html} = live(conn, ~p"/admin/import-export") header = "first_name;last_name;email;street;postal_code;city\n" rows = @@ -168,7 +168,7 @@ defmodule MvWeb.ImportLiveTest do end test "BOM and semicolon delimiter are accepted", %{conn: conn} do - {:ok, view, _html} = live(conn, ~p"/admin/import") + {:ok, view, _html} = live(conn, ~p"/admin/import-export") csv_content = Path.join([__DIR__, "..", "..", "fixtures", "csv_with_bom_semicolon.csv"]) @@ -187,7 +187,7 @@ defmodule MvWeb.ImportLiveTest do test "physical line numbers in errors (empty line does not shift numbering)", %{ conn: conn } do - {:ok, view, _html} = live(conn, ~p"/admin/import") + {:ok, view, _html} = live(conn, ~p"/admin/import-export") csv_content = Path.join([__DIR__, "..", "..", "fixtures", "csv_with_empty_lines.csv"]) @@ -207,7 +207,7 @@ defmodule MvWeb.ImportLiveTest do conn: conn, unknown_custom_field_csv: csv_content } do - {:ok, view, _html} = live(conn, ~p"/admin/import") + {:ok, view, _html} = live(conn, ~p"/admin/import-export") upload_csv_file(view, csv_content, "unknown_custom.csv") submit_import(view) wait_for_import_completion() @@ -220,7 +220,7 @@ defmodule MvWeb.ImportLiveTest do end # ---------- UI (smoke / framework): tagged for exclusion from fast CI ---------- - describe "Import page UI" do + describe "Import/Export page UI" do @describetag :ui setup %{conn: conn} do admin_user = Mv.Fixtures.user_with_role_fixture("admin") @@ -233,17 +233,19 @@ defmodule MvWeb.ImportLiveTest do {:ok, conn: conn} end - test "page loads and shows import form", %{conn: conn} do - {:ok, view, _html} = live(conn, ~p"/admin/import") + test "page loads and shows import form and export placeholder", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/admin/import-export") assert has_element?(view, "[data-testid='csv-upload-form']") assert has_element?(view, "[data-testid='start-import-button']") assert has_element?(view, "[data-testid='custom-fields-link']") html = render(view) assert html =~ "Import Members (CSV)" + assert html =~ "Export Members (CSV)" + assert html =~ "Export functionality will be available" end test "template links and file input are present", %{conn: conn} do - {:ok, view, _html} = live(conn, ~p"/admin/import") + {:ok, view, _html} = live(conn, ~p"/admin/import-export") assert has_element?(view, "a[href*='/templates/member_import_en.csv']") assert has_element?(view, "a[href*='/templates/member_import_de.csv']") assert has_element?(view, "label[for='csv_file']") @@ -256,7 +258,7 @@ defmodule MvWeb.ImportLiveTest do Path.join([__DIR__, "..", "..", "fixtures", "valid_member_import.csv"]) |> File.read!() - {:ok, view, _html} = live(conn, ~p"/admin/import") + {:ok, view, _html} = live(conn, ~p"/admin/import-export") upload_csv_file(view, csv_content) submit_import(view) wait_for_import_completion() @@ -271,7 +273,7 @@ defmodule MvWeb.ImportLiveTest do @tag :skip test "empty CSV shows error", %{conn: conn} do conn = put_locale_en(conn) - {:ok, view, _html} = live(conn, ~p"/admin/import") + {:ok, view, _html} = live(conn, ~p"/admin/import-export") upload_csv_file(view, " ", "empty.csv") submit_import(view) html = render(view) diff --git a/test/mv_web/member_live/index_groups_accessibility_test.exs b/test/mv_web/member_live/index_groups_accessibility_test.exs index d14cd9f..ab9b728 100644 --- a/test/mv_web/member_live/index_groups_accessibility_test.exs +++ b/test/mv_web/member_live/index_groups_accessibility_test.exs @@ -3,7 +3,7 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do Tests for accessibility of groups feature in the member overview. Tests cover: - - Badges have aria-label for group membership (no role="status"; reserved for live regions) + - Badges have role="status" and aria-label - Filter dropdown has aria-label - Sort header has aria-label for screen reader - Keyboard navigation works (Tab through filter, sort header) @@ -44,7 +44,7 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do end @tag :ui - test "group badges have aria-label for screen readers", %{ + test "group badges have role and aria-label", %{ conn: conn, member1: member1, group1: group1 @@ -52,8 +52,8 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do conn = conn_with_oidc_user(conn) {:ok, view, html} = live(conn, "/members") - # Verify badges have aria-label containing the group name (no role=status on badges) - assert has_element?(view, "span[aria-label*='#{group1.name}']") + # Verify badges have role="status" and aria-label containing the group name + assert has_element?(view, "span[role='status'][aria-label*='#{group1.name}']") assert html =~ group1.name # Verify member1's row contains the badge diff --git a/test/mv_web/member_live/show_groups_display_test.exs b/test/mv_web/member_live/show_groups_display_test.exs deleted file mode 100644 index f8434b3..0000000 --- a/test/mv_web/member_live/show_groups_display_test.exs +++ /dev/null @@ -1,262 +0,0 @@ -defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do - @moduledoc """ - Tests for displaying groups in the member detail view (Issue #374). - - Tests cover: - - Groups in Personal Data (with and without groups) - - Group links with correct names and links to group detail pages - - Edge cases (one group, many groups) - - Security: groups visible only when user may view member - - Accessibility: group links have aria-label for screen readers - - ## Note on async - async: false to avoid PostgreSQL deadlocks when creating members and groups - in the same test run (same as IndexGroupsDisplayTest). - """ - use MvWeb.ConnCase, async: false - import Phoenix.LiveViewTest - require Ash.Query - use Gettext, backend: MvWeb.Gettext - - alias Mv.Membership.{Group, MemberGroup} - - describe "groups section" do - setup do - actor = Mv.Helpers.SystemActor.get_system_actor() - - {:ok, member} = - create_member(actor, %{ - first_name: "Alice", - last_name: "Anderson", - email: "alice@example.com" - }) - - %{member: member, actor: actor} - end - - test "displays Groups section when member has at least one group", %{ - conn: conn, - member: member, - actor: actor - } do - {:ok, group} = create_group(actor, "Board Members") - {:ok, _mg} = add_member_to_group(member, group, actor) - - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, ~p"/members/#{member}") - - assert html =~ gettext("Groups") - assert html =~ group.name - end - - test "displays all group links with correct names when member is in multiple groups", %{ - conn: conn, - member: member, - actor: actor - } do - {:ok, group1} = create_group(actor, "Board Members") - {:ok, group2} = create_group(actor, "Active Members") - {:ok, _mg1} = add_member_to_group(member, group1, actor) - {:ok, _mg2} = add_member_to_group(member, group2, actor) - - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, ~p"/members/#{member}") - - assert html =~ gettext("Groups") - assert html =~ group1.name - assert html =~ group2.name - end - - test "displays Groups section when member has no groups (empty state)", %{ - conn: conn, - member: member - } do - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, ~p"/members/#{member}") - - # Groups are in Personal Data; label "Groups" and empty state "No groups" must be present - assert html =~ gettext("Groups") - assert html =~ gettext("No groups") - end - - test "renders all group names when member has multiple groups", %{ - conn: conn, - member: member, - actor: actor - } do - {:ok, group1} = create_group(actor, "Alpha") - {:ok, group2} = create_group(actor, "Beta") - {:ok, _mg1} = add_member_to_group(member, group1, actor) - {:ok, _mg2} = add_member_to_group(member, group2, actor) - - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, ~p"/members/#{member}") - - assert html =~ "Alpha" - assert html =~ "Beta" - end - end - - describe "groups section links" do - setup do - actor = Mv.Helpers.SystemActor.get_system_actor() - - {:ok, member} = - create_member(actor, %{first_name: "Bob", last_name: "Brown", email: "bob@example.com"}) - - {:ok, group} = create_group(actor, "Board Members") - {:ok, _mg} = add_member_to_group(member, group, actor) - %{member: member, group: group} - end - - test "each group link goes to group detail page with correct slug", %{ - conn: conn, - member: member, - group: group - } do - conn = conn_with_oidc_user(conn) - {:ok, view, _html} = live(conn, ~p"/members/#{member}") - - # Link to group detail: /groups/:slug (slug is URL-friendly, e.g. "board-members") - assert has_element?(view, "a[href*='/groups/#{group.slug}']", group.name) - end - - test "clicking group link navigates to group detail page", %{ - conn: conn, - member: member, - group: group - } do - conn = conn_with_oidc_user(conn) - {:ok, view, _html} = live(conn, ~p"/members/#{member}") - - view - |> element("a[href*='/groups/#{group.slug}']") - |> render_click() - - assert_redirect(view, ~p"/groups/#{group.slug}") - end - end - - describe "groups section edge cases" do - setup do - actor = Mv.Helpers.SystemActor.get_system_actor() - - {:ok, member} = - create_member(actor, %{ - first_name: "Charlie", - last_name: "Clark", - email: "charlie@example.com" - }) - - %{member: member, actor: actor} - end - - test "member in exactly one group shows single link", %{ - conn: conn, - member: member, - actor: actor - } do - {:ok, group} = create_group(actor, "Solo Group") - {:ok, _mg} = add_member_to_group(member, group, actor) - - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, ~p"/members/#{member}") - - assert html =~ gettext("Groups") - assert html =~ group.name - end - - test "member in many groups shows all group links", %{ - conn: conn, - member: member, - actor: actor - } do - group_names = Enum.map(1..5, fn i -> "Group #{i}" end) - - groups = - Enum.map(group_names, fn name -> - {:ok, g} = create_group(actor, name) - g - end) - - for g <- groups do - {:ok, _mg} = add_member_to_group(member, g, actor) - end - - conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, ~p"/members/#{member}") - - assert html =~ gettext("Groups") - - for name <- group_names do - assert html =~ name - end - end - end - - describe "groups section with read_only user" do - @tag role: :read_only - test "user with read permission sees Groups section", %{conn: conn} do - actor = Mv.Helpers.SystemActor.get_system_actor() - - {:ok, member} = - create_member(actor, %{ - first_name: "Diana", - last_name: "Davis", - email: "diana@example.com" - }) - - {:ok, group} = create_group(actor, "Readers") - {:ok, _mg} = add_member_to_group(member, group, actor) - - {:ok, _view, html} = live(conn, ~p"/members/#{member}") - - assert html =~ gettext("Groups") - assert html =~ group.name - end - end - - describe "groups section accessibility" do - setup do - actor = Mv.Helpers.SystemActor.get_system_actor() - - {:ok, member} = - create_member(actor, %{first_name: "Eve", last_name: "Evans", email: "eve@example.com"}) - - {:ok, group} = create_group(actor, "A11y Group") - {:ok, _mg} = add_member_to_group(member, group, actor) - %{member: member, group: group} - end - - test "group links have aria-label for screen readers", %{ - conn: conn, - member: member, - group: group - } do - conn = conn_with_oidc_user(conn) - {:ok, view, html} = live(conn, ~p"/members/#{member}") - - assert html =~ group.name - - # Group link has aria-label indicating group membership for screen readers - assert has_element?(view, "a[aria-label*='#{group.name}']") - end - end - - # Helpers to reduce setup duplication (create member/group, assign member to group). - defp create_member(actor, attrs) do - Mv.Membership.create_member(attrs, actor: actor) - end - - defp create_group(actor, name) do - Group - |> Ash.Changeset.for_create(:create, %{name: name}) - |> Ash.create(actor: actor) - end - - defp add_member_to_group(member, group, actor) do - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) - |> Ash.create(actor: actor) - end -end