From ff9f98f8e7b5941fc4ea06fa91d875298cc097b2 Mon Sep 17 00:00:00 2001 From: carla Date: Wed, 25 Feb 2026 09:45:10 +0100 Subject: [PATCH] style: consitent flash messages --- DESIGN_DUIDELINES.md | 6 +- docs/feature-roadmap.md | 5 ++ lib/mv_web/components/core_components.ex | 2 +- lib/mv_web/components/layouts.ex | 6 +- lib/mv_web/controllers/auth_controller.ex | 4 +- .../live/auth/link_oidc_account_live.ex | 4 +- lib/mv_web/live/datafields_live.ex | 6 +- lib/mv_web/live/global_settings_live.ex | 25 +++--- lib/mv_web/live/group_live/form.ex | 2 +- lib/mv_web/live/group_live/index.ex | 26 +++--- lib/mv_web/live/group_live/show.ex | 4 +- lib/mv_web/live/member_live/form.ex | 2 +- lib/mv_web/live/member_live/index.ex | 2 +- .../show/membership_fees_component.ex | 18 ++-- .../live/membership_fee_settings_live.ex | 4 +- .../live/membership_fee_type_live/form.ex | 2 +- .../live/membership_fee_type_live/index.ex | 2 +- lib/mv_web/live/role_live/form.ex | 86 +++++++++---------- lib/mv_web/live/role_live/index.ex | 2 +- lib/mv_web/live/role_live/show.ex | 8 +- lib/mv_web/live/user_live/form.ex | 2 +- lib/mv_web/live/user_live/index.ex | 2 +- 22 files changed, 117 insertions(+), 103 deletions(-) diff --git a/DESIGN_DUIDELINES.md b/DESIGN_DUIDELINES.md index b0372ef..18864b5 100644 --- a/DESIGN_DUIDELINES.md +++ b/DESIGN_DUIDELINES.md @@ -286,11 +286,11 @@ Notes: - warning: 6–8s - error: 8–12s (or manual dismiss for critical errors) - **MUST:** Keep a dismiss button for accessibility and user control. +- **Status:** Not yet implemented. See [feature-roadmap](docs/feature-roadmap.md) → Flash: Auto-dismiss and consistency. -### 9.3 Variants + special “email copied” +### 9.3 Variants (unified) - Supported semantic variants: `info`, `success`, `warning`, `error`. -- **Special case:** clipboard “Email copied” uses a **soft/light blue** tone distinct from normal info. -- **MUST:** Model this as `tone="soft"` (or similar prop) on the flash component, not hard-coded colors in views. +- **MUST:** Use the same variants for all flash types, : e.g. `success` for copy success, no separate tone or styling. This keeps flash UX consistent across the app. ### 9.4 Accessibility - Flash must work with screen readers (live region behavior belongs in the flash component implementation). diff --git a/docs/feature-roadmap.md b/docs/feature-roadmap.md index b699560..66b46eb 100644 --- a/docs/feature-roadmap.md +++ b/docs/feature-roadmap.md @@ -191,6 +191,11 @@ - ❌ Mobile navigation - ❌ Context-sensitive help - ❌ Onboarding tooltips +- ❌ **Flash: Auto-dismiss and consistency** (Design Guidelines §9) + - Auto-dismiss: info/success 4–6s, warning 6–8s, error 8–12s; dismiss button kept for accessibility. + - Implement via JS hook (e.g. `FlashAutoDismiss`) + `data-dismiss-ms` (or `data-kind`) on flash component; on timeout push `lv:clear-flash` and hide element. + - LiveView: add shared `handle_event("lv:clear-flash", %{"key" => key}, socket)` (e.g. in `MvWeb` live_view quote) calling `clear_flash(socket, key)`. + - All flashes (including “Email copied”) use the same variants (info, success, warning, error); no special tone. See `DESIGN_DUIDELINES.md` §9. --- diff --git a/lib/mv_web/components/core_components.ex b/lib/mv_web/components/core_components.ex index 4f9d7af..1e8e7f3 100644 --- a/lib/mv_web/components/core_components.ex +++ b/lib/mv_web/components/core_components.ex @@ -60,7 +60,7 @@ defmodule MvWeb.CoreComponents do id={@id} phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")} role="alert" - class="z-50 toast toast-bottom toast-end" + class="pointer-events-auto" {@rest} >
+
<.flash kind={:success} flash={@flash} /> <.flash kind={:warning} flash={@flash} /> <.flash kind={:info} flash={@flash} /> diff --git a/lib/mv_web/controllers/auth_controller.ex b/lib/mv_web/controllers/auth_controller.ex index 28f3846..20a76f5 100644 --- a/lib/mv_web/controllers/auth_controller.ex +++ b/lib/mv_web/controllers/auth_controller.ex @@ -31,7 +31,7 @@ defmodule MvWeb.AuthController do |> store_in_session(user) # If your resource has a different name, update the assign name here (i.e :current_admin) |> assign(:current_user, user) - |> put_flash(:info, message) + |> put_flash(:success, message) |> redirect(to: return_to) end @@ -322,7 +322,7 @@ defmodule MvWeb.AuthController do conn |> clear_session(:mv) - |> put_flash(:info, gettext("You are now signed out")) + |> put_flash(:success, gettext("You are now signed out")) |> redirect(to: return_to) end end diff --git a/lib/mv_web/live/auth/link_oidc_account_live.ex b/lib/mv_web/live/auth/link_oidc_account_live.ex index b6c24b1..01bd57b 100644 --- a/lib/mv_web/live/auth/link_oidc_account_live.ex +++ b/lib/mv_web/live/auth/link_oidc_account_live.ex @@ -81,7 +81,7 @@ defmodule MvWeb.LinkOidcAccountLive do socket |> put_flash( - :info, + :success, dgettext("auth", "Account activated! Redirecting to complete sign-in...") ) |> Phoenix.LiveView.redirect(to: ~p"/auth/user/oidc") @@ -217,7 +217,7 @@ defmodule MvWeb.LinkOidcAccountLive do {:noreply, socket |> put_flash( - :info, + :success, dgettext( "auth", "Your OIDC account has been successfully linked! Redirecting to complete sign-in..." diff --git a/lib/mv_web/live/datafields_live.ex b/lib/mv_web/live/datafields_live.ex index f7436ab..f922d22 100644 --- a/lib/mv_web/live/datafields_live.ex +++ b/lib/mv_web/live/datafields_live.ex @@ -64,12 +64,12 @@ defmodule MvWeb.DatafieldsLive do {:noreply, socket |> assign(:active_editing_section, nil) - |> put_flash(:info, gettext("Data field %{action} successfully", action: action))} + |> put_flash(:success, gettext("Data field %{action} successfully", action: action))} end @impl true def handle_info({:custom_field_deleted, _custom_field}, socket) do - {:noreply, put_flash(socket, :info, gettext("Data field deleted successfully"))} + {:noreply, put_flash(socket, :success, gettext("Data field deleted successfully"))} end @impl true @@ -115,7 +115,7 @@ defmodule MvWeb.DatafieldsLive do socket |> assign(:settings, updated_settings) |> assign(:active_editing_section, nil) - |> put_flash(:info, gettext("Member field %{action} successfully", action: action))} + |> put_flash(:success, gettext("Member field %{action} successfully", action: action))} end @impl true diff --git a/lib/mv_web/live/global_settings_live.ex b/lib/mv_web/live/global_settings_live.ex index f3a61bc..485601a 100644 --- a/lib/mv_web/live/global_settings_live.ex +++ b/lib/mv_web/live/global_settings_live.ex @@ -357,20 +357,21 @@ defmodule MvWeb.GlobalSettingsLive do errors_with_names = enrich_sync_errors(errors) result = %{synced: synced, errors: errors_with_names} + {flash_kind, flash_message} = + if(errors_with_names == [], + do: {:success, gettext("Synced %{count} member(s) to Vereinfacht.", count: synced)}, + else: + {:warning, + gettext("Synced %{count} member(s). %{error_count} failed.", + count: synced, + error_count: length(errors_with_names) + )} + ) + socket = socket |> assign(:last_vereinfacht_sync_result, result) - |> put_flash( - :info, - if(errors_with_names == [], - do: gettext("Synced %{count} member(s) to Vereinfacht.", count: synced), - else: - gettext("Synced %{count} member(s). %{error_count} failed.", - count: synced, - error_count: length(errors_with_names) - ) - ) - ) + |> put_flash(flash_kind, flash_message) {:noreply, socket} @@ -409,7 +410,7 @@ defmodule MvWeb.GlobalSettingsLive do |> assign(:oidc_client_secret_set, present?(fresh_settings.oidc_client_secret)) |> assign(:oidc_configured, Mv.Config.oidc_configured?()) |> assign(:vereinfacht_test_result, test_result) - |> put_flash(:info, gettext("Settings updated successfully")) + |> put_flash(:success, gettext("Settings updated successfully")) |> assign_form() {:noreply, socket} diff --git a/lib/mv_web/live/group_live/form.ex b/lib/mv_web/live/group_live/form.ex index 5f781a7..d9999d3 100644 --- a/lib/mv_web/live/group_live/form.ex +++ b/lib/mv_web/live/group_live/form.ex @@ -128,7 +128,7 @@ defmodule MvWeb.GroupLive.Form do socket = socket - |> put_flash(:info, gettext("Group saved successfully.")) + |> put_flash(:success, gettext("Group saved successfully.")) |> push_navigate(to: redirect_path) {:noreply, socket} diff --git a/lib/mv_web/live/group_live/index.ex b/lib/mv_web/live/group_live/index.ex index b6c8277..70358e0 100644 --- a/lib/mv_web/live/group_live/index.ex +++ b/lib/mv_web/live/group_live/index.ex @@ -76,24 +76,24 @@ defmodule MvWeb.GroupLive.Index do <:col :let={group} label={gettext("Members")} class="text-right"> {group.member_count || 0} - <:action :let={group}> - <.button - variant="ghost" - size="sm" - navigate={~p"/groups/#{group.slug}"} - > - {gettext("View")} - - <%= if can?(@current_user, :update, Mv.Membership.Group) do %> + <:action :let={group}> <.button variant="ghost" size="sm" - navigate={~p"/groups/#{group.slug}/edit"} + navigate={~p"/groups/#{group.slug}"} > - {gettext("Edit group")} + {gettext("View")} - <% end %> - + <%= if can?(@current_user, :update, Mv.Membership.Group) do %> + <.button + variant="ghost" + size="sm" + navigate={~p"/groups/#{group.slug}/edit"} + > + {gettext("Edit group")} + + <% end %> + <% end %>
diff --git a/lib/mv_web/live/group_live/show.ex b/lib/mv_web/live/group_live/show.ex index 46766ef..7e2d57f 100644 --- a/lib/mv_web/live/group_live/show.ex +++ b/lib/mv_web/live/group_live/show.ex @@ -150,7 +150,7 @@ defmodule MvWeb.GroupLive.Show do
- <%= for member <- @selected_members do %> + <%= for member <- @selected_members do %> {MvWeb.Helpers.MemberHelpers.display_name(member)} <.tooltip content={gettext("Remove")} position="top"> @@ -909,7 +909,7 @@ defmodule MvWeb.GroupLive.Show do :ok -> {:noreply, socket - |> put_flash(:info, gettext("Group deleted successfully.")) + |> put_flash(:success, gettext("Group deleted successfully.")) |> redirect(to: ~p"/groups")} {:error, error} -> diff --git a/lib/mv_web/live/member_live/form.ex b/lib/mv_web/live/member_live/form.ex index 625ab2a..66260f4 100644 --- a/lib/mv_web/live/member_live/form.ex +++ b/lib/mv_web/live/member_live/form.ex @@ -390,7 +390,7 @@ defmodule MvWeb.MemberLive.Form do socket = socket - |> put_flash(:info, flash_message) + |> put_flash(:success, flash_message) |> maybe_put_vereinfacht_sync_flash(member.id) |> push_navigate(to: return_path(socket.assigns.return_to, member)) diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 3283b5c..a7f6316 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -175,7 +175,7 @@ defmodule MvWeb.MemberLive.Index do {:noreply, socket |> assign(:members, updated_members) - |> put_flash(:info, gettext("Member deleted successfully"))} + |> put_flash(:success, gettext("Member deleted successfully"))} {:error, %Ash.Error.Forbidden{}} -> {:noreply, diff --git a/lib/mv_web/live/member_live/show/membership_fees_component.ex b/lib/mv_web/live/member_live/show/membership_fees_component.ex index 2e75b57..23f0dda 100644 --- a/lib/mv_web/live/member_live/show/membership_fees_component.ex +++ b/lib/mv_web/live/member_live/show/membership_fees_component.ex @@ -562,7 +562,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do get_available_fee_types(updated_member, actor) ) |> assign(:interval_warning, nil) - |> put_flash(:info, gettext("Membership fee type removed"))} + |> put_flash(:success, gettext("Membership fee type removed"))} {:error, error} -> {:noreply, put_flash(socket, :error, format_error(error))} @@ -621,7 +621,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do get_available_fee_types(updated_member, actor) ) |> assign(:interval_warning, nil) - |> put_flash(:info, gettext("Membership fee type updated. Cycles regenerated."))} + |> put_flash(:success, gettext("Membership fee type updated. Cycles regenerated."))} {:error, error} -> {:noreply, put_flash(socket, :error, format_error(error))} @@ -649,7 +649,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do {:noreply, socket |> assign(:cycles, updated_cycles) - |> put_flash(:info, gettext("Cycle status updated"))} + |> put_flash(:success, gettext("Cycle status updated"))} {:error, %Ash.Error.Invalid{} = error} -> error_msg = @@ -705,7 +705,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do |> assign(:member, updated_member) |> assign(:cycles, cycles) |> assign(:regenerating, false) - |> put_flash(:info, gettext("Cycles regenerated successfully"))} + |> put_flash(:success, gettext("Cycles regenerated successfully"))} {:error, error} -> {:noreply, @@ -755,7 +755,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do socket |> assign(:cycles, updated_cycles) |> assign(:editing_cycle, nil) - |> put_flash(:info, gettext("Cycle amount updated"))} + |> put_flash(:success, gettext("Cycle amount updated"))} {:error, error} -> {:noreply, @@ -794,7 +794,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do socket |> assign(:cycles, updated_cycles) |> assign(:deleting_cycle, nil) - |> put_flash(:info, gettext("Cycle deleted"))} + |> put_flash(:success, gettext("Cycle deleted"))} {:ok, _destroyed} -> # Handle case where return_destroyed? is true @@ -804,7 +804,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do socket |> assign(:cycles, updated_cycles) |> assign(:deleting_cycle, nil) - |> put_flash(:info, gettext("Cycle deleted"))} + |> put_flash(:success, gettext("Cycle deleted"))} {:error, error} -> {:noreply, @@ -950,7 +950,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do |> assign(:creating_cycle, false) |> assign(:create_cycle_date, nil) |> assign(:create_cycle_error, nil) - |> put_flash(:info, gettext("Cycle created successfully"))} + |> put_flash(:success, gettext("Cycle created successfully"))} {:error, error} -> {:noreply, @@ -1013,7 +1013,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do |> assign(:member, updated_member) |> assign(:cycles, updated_cycles) |> reset_modal.() - |> put_flash(:info, gettext("All cycles deleted"))} + |> put_flash(:success, gettext("All cycles deleted"))} {:ok, _} -> {:noreply, diff --git a/lib/mv_web/live/membership_fee_settings_live.ex b/lib/mv_web/live/membership_fee_settings_live.ex index aabb210..84ce662 100644 --- a/lib/mv_web/live/membership_fee_settings_live.ex +++ b/lib/mv_web/live/membership_fee_settings_live.ex @@ -82,7 +82,7 @@ defmodule MvWeb.MembershipFeeSettingsLive do {:noreply, socket |> assign(:settings, updated_settings) - |> put_flash(:info, gettext("Settings saved successfully.")) + |> put_flash(:success, gettext("Settings saved successfully.")) |> assign_form()} {:error, form} -> @@ -105,7 +105,7 @@ defmodule MvWeb.MembershipFeeSettingsLive do socket |> assign(:membership_fee_types, updated_types) |> assign(:member_counts, updated_counts) - |> put_flash(:info, gettext("Membership fee type deleted"))} + |> put_flash(:success, gettext("Membership fee type deleted"))} {:error, %Ash.Error.Forbidden{}} -> {:noreply, diff --git a/lib/mv_web/live/membership_fee_type_live/form.ex b/lib/mv_web/live/membership_fee_type_live/form.ex index 72add11..ca61e19 100644 --- a/lib/mv_web/live/membership_fee_type_live/form.ex +++ b/lib/mv_web/live/membership_fee_type_live/form.ex @@ -317,7 +317,7 @@ defmodule MvWeb.MembershipFeeTypeLive.Form do socket = socket - |> put_flash(:info, gettext("Membership fee type saved successfully")) + |> put_flash(:success, gettext("Membership fee type saved successfully")) |> push_navigate(to: return_path(socket.assigns.return_to, membership_fee_type)) {:noreply, socket} diff --git a/lib/mv_web/live/membership_fee_type_live/index.ex b/lib/mv_web/live/membership_fee_type_live/index.ex index 0a17920..ee3b791 100644 --- a/lib/mv_web/live/membership_fee_type_live/index.ex +++ b/lib/mv_web/live/membership_fee_type_live/index.ex @@ -149,7 +149,7 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do socket |> assign(:membership_fee_types, updated_types) |> assign(:member_counts, updated_counts) - |> put_flash(:info, gettext("Membership fee type deleted"))} + |> put_flash(:success, gettext("Membership fee type deleted"))} {:error, %Ash.Error.Forbidden{}} -> {:noreply, diff --git a/lib/mv_web/live/role_live/form.ex b/lib/mv_web/live/role_live/form.ex index ccd03cf..684e695 100644 --- a/lib/mv_web/live/role_live/form.ex +++ b/lib/mv_web/live/role_live/form.ex @@ -39,50 +39,50 @@ defmodule MvWeb.RoleLive.Form do
<.input field={@form[:name]} type="text" label={gettext("Name")} required /> - <.input - field={@form[:description]} - type="textarea" - label={gettext("Description")} - rows="3" - /> + <.input + field={@form[:description]} + type="textarea" + label={gettext("Description")} + rows="3" + /> -
- - + + <%= for permission_set <- all_permission_sets() do %> + + <% end %> + + <%= if @form.errors[:permission_set_name] do %> + <%= for error <- List.wrap(@form.errors[:permission_set_name]) do %> + <% {msg, _opts} = if is_tuple(error), do: error, else: {error, []} %> +

+ <.icon name="hero-exclamation-circle" class="size-5" /> + {msg} +

+ <% end %> <% end %> - - <%= if @form.errors[:permission_set_name] do %> - <%= for error <- List.wrap(@form.errors[:permission_set_name]) do %> - <% {msg, _opts} = if is_tuple(error), do: error, else: {error, []} %> -

- <.icon name="hero-exclamation-circle" class="size-5" /> - {msg} -

- <% end %> - <% end %> -
+
@@ -177,7 +177,7 @@ defmodule MvWeb.RoleLive.Form do socket = socket - |> put_flash(:info, gettext("Role saved successfully.")) + |> put_flash(:success, gettext("Role saved successfully.")) |> push_navigate(to: redirect_path) {:noreply, socket} diff --git a/lib/mv_web/live/role_live/index.ex b/lib/mv_web/live/role_live/index.ex index 091b191..2169400 100644 --- a/lib/mv_web/live/role_live/index.ex +++ b/lib/mv_web/live/role_live/index.ex @@ -100,7 +100,7 @@ defmodule MvWeb.RoleLive.Index do socket |> assign(:roles, updated_roles) |> assign(:user_counts, updated_counts) - |> put_flash(:info, gettext("Role deleted successfully."))} + |> put_flash(:success, gettext("Role deleted successfully."))} {:error, error} -> error_message = format_error(error) diff --git a/lib/mv_web/live/role_live/show.ex b/lib/mv_web/live/role_live/show.ex index 4dbbb1f..8b5b1b2 100644 --- a/lib/mv_web/live/role_live/show.ex +++ b/lib/mv_web/live/role_live/show.ex @@ -124,7 +124,7 @@ defmodule MvWeb.RoleLive.Show do :ok -> {:noreply, socket - |> put_flash(:info, gettext("Role deleted successfully.")) + |> put_flash(:success, gettext("Role deleted successfully.")) |> push_navigate(to: ~p"/admin/roles")} {:error, error} -> @@ -165,7 +165,11 @@ defmodule MvWeb.RoleLive.Show do <:subtitle>{gettext("Role details and permissions.")} <:actions> - <.button navigate={~p"/admin/roles"} variant="neutral" aria-label={gettext("Back to roles list")}> + <.button + navigate={~p"/admin/roles"} + variant="neutral" + aria-label={gettext("Back to roles list")} + > <.icon name="hero-arrow-left" class="size-4" /> {gettext("Back")} diff --git a/lib/mv_web/live/user_live/form.ex b/lib/mv_web/live/user_live/form.ex index f9f17bb..8ff5966 100644 --- a/lib/mv_web/live/user_live/form.ex +++ b/lib/mv_web/live/user_live/form.ex @@ -556,7 +556,7 @@ defmodule MvWeb.UserLive.Form do socket = socket - |> put_flash(:info, gettext("User %{action} successfully", action: action)) + |> put_flash(:success, gettext("User %{action} successfully", action: action)) |> push_navigate(to: return_path(socket.assigns.return_to, updated_user)) {:noreply, socket} diff --git a/lib/mv_web/live/user_live/index.ex b/lib/mv_web/live/user_live/index.ex index 72cc55c..ba36605 100644 --- a/lib/mv_web/live/user_live/index.ex +++ b/lib/mv_web/live/user_live/index.ex @@ -61,7 +61,7 @@ defmodule MvWeb.UserLive.Index do {:noreply, socket |> assign(:users, updated_users) - |> put_flash(:info, gettext("User deleted successfully"))} + |> put_flash(:success, gettext("User deleted successfully"))} {:error, %Ash.Error.Forbidden{}} -> {:noreply,