WIP: Add sidebar #260

Draft
rafael wants to merge 2 commits from sidebar into main
5 changed files with 44 additions and 45 deletions
Showing only changes of commit bb6ea0085b - Show all commits

View file

@ -54,6 +54,9 @@ defmodule Mv.Accounts.User do
auth_method :client_secret_jwt
code_verifier true
# Request email and profile scopes from OIDC provider (required for Authentik, Keycloak, etc.)
authorization_params scope: "openid email profile"
# id_token_signed_response_alg "EdDSA" #-> https://git.local-it.org/local-it/mitgliederverwaltung/issues/87
end
@ -69,7 +72,7 @@ defmodule Mv.Accounts.User do
# Default actions for framework/tooling integration:
# - :read -> Standard read used across the app and by admin tooling.
# - :destroy-> Standard delete used by admin tooling and maintenance tasks.
#
#
# NOTE: :create is INTENTIONALLY excluded from defaults!
# Using a default :create would bypass email-synchronization logic.
# Always use one of these explicit create actions instead:
@ -185,7 +188,9 @@ defmodule Mv.Accounts.User do
oidc_user_info = Ash.Changeset.get_argument(changeset, :oidc_user_info)
# Get the new email from OIDC user_info
new_email = Map.get(oidc_user_info, "preferred_username")
# Support both "email" (standard OIDC) and "preferred_username" (Rauthy)
new_email =
Map.get(oidc_user_info, "email") || Map.get(oidc_user_info, "preferred_username")
changeset
|> Ash.Changeset.change_attribute(:oidc_id, oidc_id)
@ -239,8 +244,11 @@ defmodule Mv.Accounts.User do
change fn changeset, _ctx ->
user_info = Ash.Changeset.get_argument(changeset, :user_info)
# Support both "email" (standard OIDC like Authentik, Keycloak) and "preferred_username" (Rauthy)
email = user_info["email"] || user_info["preferred_username"]
changeset
|> Ash.Changeset.change_attribute(:email, user_info["preferred_username"])
|> Ash.Changeset.change_attribute(:email, email)
|> Ash.Changeset.change_attribute(:oidc_id, user_info["sub"] || user_info["id"])
end

View file

@ -474,6 +474,7 @@ defmodule MvWeb.CoreComponents do
slot :col, required: true do
attr :label, :string
attr :col_click, :any, doc: "optional column-specific click handler that overrides row_click"
end
slot :action, doc: "the slot for showing user actions in the last table column"
@ -509,8 +510,11 @@ defmodule MvWeb.CoreComponents do
<tr :for={row <- @rows} id={@row_id && @row_id.(row)}>
<td
:for={col <- @col}
phx-click={@row_click && @row_click.(row)}
class={["max-w-xs truncate", @row_click && "hover:cursor-pointer"]}
phx-click={
(col[:col_click] && col[:col_click].(@row_item.(row))) ||
(@row_click && @row_click.(row))
}
class={["max-w-xs truncate", (col[:col_click] || @row_click) && "hover:cursor-pointer"]}
>
{render_slot(col, @row_item.(row))}
</td>

View file

@ -1082,6 +1082,16 @@ defmodule MvWeb.MemberLive.Index do
|> Enum.map(&format_member_email/1)
end
@doc """
Returns a JS command to toggle member selection when clicking the checkbox column.
Used as `col_click` handler to ensure clicking anywhere in the checkbox column
toggles the checkbox instead of navigating to the member details.
"""
def checkbox_column_click(member) do
JS.push("select_member", value: %{id: member.id})
end
# Formats a member's email in the format "First Last <email>"
# Used for copy_emails feature and mailto links to create email-client-friendly format.
def format_member_email(member) do

View file

@ -65,6 +65,7 @@
<!-- <:col :let={member} label="Id">{member.id}</:col> -->
<:col
:let={member}
col_click={&MvWeb.MemberLive.Index.checkbox_column_click/1}
label={
~H"""
<.input
@ -81,11 +82,7 @@
<.input
type="checkbox"
name={member.id}
phx-click="select_member"
phx-value-id={member.id}
checked={MapSet.member?(@selected_members, member.id)}
phx-capture-click
phx-stop-propagation
aria-label={gettext("Select member")}
role="checkbox"
/>

View file

@ -285,14 +285,9 @@ defmodule MvWeb.MemberLive.IndexTest 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()
# Select two members by sending the select_member event directly
render_click(view, "select_member", %{"id" => member1.id})
render_click(view, "select_member", %{"id" => member2.id})
# Trigger copy_emails event
view |> element("#copy-emails-btn") |> render_click()
@ -336,10 +331,8 @@ defmodule MvWeb.MemberLive.IndexTest 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()
# Select member with umlauts by sending the select_member event directly
render_click(view, "select_member", %{"id" => member3.id})
# Trigger copy_emails event - should not crash
view |> element("#copy-emails-btn") |> render_click()
@ -355,10 +348,8 @@ defmodule MvWeb.MemberLive.IndexTest 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()
# Select a member by sending the select_member event directly
render_click(view, "select_member", %{"id" => member1.id})
# Delete the member from the database
Ash.destroy!(member1)
@ -379,14 +370,9 @@ defmodule MvWeb.MemberLive.IndexTest 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()
# Select two members by sending the select_member event directly
render_click(view, "select_member", %{"id" => member1.id})
render_click(view, "select_member", %{"id" => member2.id})
# Get the socket state to verify the formatted email string
state = :sys.get_state(view.pid)
@ -415,10 +401,8 @@ defmodule MvWeb.MemberLive.IndexTest do
{:ok, view, _html} = live(conn, "/members")
# Select the test member
view
|> element("[phx-click='select_member'][phx-value-id='#{test_member.id}']")
|> render_click()
# Select the test member by sending the select_member event directly
render_click(view, "select_member", %{"id" => test_member.id})
# The format should be "Test Format <test.format@example.com>"
# We verify this by checking the flash shows 1 email was copied
@ -441,10 +425,8 @@ defmodule MvWeb.MemberLive.IndexTest 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()
# Select a member by sending the select_member event directly
render_click(view, "select_member", %{"id" => member1.id})
# Button should now be visible
assert has_element?(view, "#copy-emails-btn")
@ -457,10 +439,8 @@ defmodule MvWeb.MemberLive.IndexTest 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()
# Select a member by sending the select_member event directly
render_click(view, "select_member", %{"id" => member1.id})
# Click copy button
view |> element("#copy-emails-btn") |> render_click()