Merge branch 'main' into bug/289_rauthy_error_message
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
carla 2026-02-19 10:02:30 +01:00
commit 2315f2588f
16 changed files with 1136 additions and 88 deletions

View file

@ -26,7 +26,6 @@ defmodule MvWeb.Components.SortHeaderComponent do
class="btn btn-ghost select-none"
phx-click="sort"
phx-value-field={@field}
phx-target={@myself}
data-testid={@field}
>
{@label}
@ -43,12 +42,6 @@ defmodule MvWeb.Components.SortHeaderComponent do
"""
end
@impl true
def handle_event("sort", %{"field" => field_str}, socket) do
send(self(), {:sort, field_str})
{:noreply, socket}
end
# -------------------------------------------------
# Hilfsfunktionen für ARIA Attribute & Icon SVG
# -------------------------------------------------

View file

@ -5,7 +5,7 @@ defmodule MvWeb.CustomFieldLive.FormComponent do
## Features
- Create new custom field definitions
- Edit existing custom fields
- Select value type from supported types
- Select value type from supported types (only on create; immutable after creation)
- Set required flag
- Real-time validation
@ -44,15 +44,50 @@ defmodule MvWeb.CustomFieldLive.FormComponent do
>
<.input field={@form[:name]} type="text" label={gettext("Name")} />
<.input
field={@form[:value_type]}
type="select"
label={gettext("Value type")}
options={
Ash.Resource.Info.attribute(Mv.Membership.CustomField, :value_type).constraints[:one_of]
|> Enum.map(fn type -> {MvWeb.Translations.FieldTypes.label(type), type} end)
}
/>
<%= if @custom_field do %>
<%!-- Show value_type as read-only input when editing (matches Member Field pattern) --%>
<div
class="tooltip tooltip-right"
data-tip={gettext("Value type cannot be changed after creation")}
aria-label={gettext("Value type cannot be changed after creation")}
>
<fieldset class="mb-2 fieldset">
<label>
<span class="mb-1 label flex items-center gap-2">
{gettext("Value type")}
<.icon
name="hero-information-circle"
class="w-4 h-4 text-base-content/60 cursor-help"
aria-hidden="true"
/>
</span>
<input
type="text"
name={@form[:value_type].name}
id={@form[:value_type].id}
value={MvWeb.Translations.FieldTypes.label(@custom_field.value_type)}
disabled
readonly
class="w-full input"
/>
</label>
</fieldset>
</div>
<% else %>
<%!-- Show value_type as select when creating --%>
<.input
field={@form[:value_type]}
type="select"
label={gettext("Value type")}
options={
Ash.Resource.Info.attribute(Mv.Membership.CustomField, :value_type).constraints[
:one_of
]
|> Enum.map(fn type -> {MvWeb.Translations.FieldTypes.label(type), type} end)
}
/>
<% end %>
<.input field={@form[:description]} type="text" label={gettext("Description")} />
<.input field={@form[:required]} type="checkbox" label={gettext("Required")} />
<.input
@ -85,8 +120,16 @@ defmodule MvWeb.CustomFieldLive.FormComponent do
@impl true
def handle_event("validate", %{"custom_field" => custom_field_params}, socket) do
# Remove value_type from params when editing (it's immutable after creation)
cleaned_params =
if socket.assigns[:custom_field] do
Map.delete(custom_field_params, "value_type")
else
custom_field_params
end
{:noreply,
assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, custom_field_params))}
assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, cleaned_params))}
end
@impl true
@ -94,7 +137,15 @@ defmodule MvWeb.CustomFieldLive.FormComponent do
# Actor must be passed from parent (IndexComponent); component socket has no current_user
actor = socket.assigns[:actor]
case MvWeb.LiveHelpers.submit_form(socket.assigns.form, custom_field_params, actor) do
# Remove value_type from params when editing (it's immutable after creation)
cleaned_params =
if socket.assigns[:custom_field] do
Map.delete(custom_field_params, "value_type")
else
custom_field_params
end
case MvWeb.LiveHelpers.submit_form(socket.assigns.form, cleaned_params, actor) do
{:ok, custom_field} ->
action =
case socket.assigns.form.source.type do

View file

@ -68,18 +68,15 @@ defmodule MvWeb.MemberLive.Index do
# This is appropriate for initialization errors that should be visible to the user.
actor = current_actor(socket)
custom_fields_visible =
Mv.Membership.CustomField
|> Ash.Query.filter(expr(show_in_overview == true))
|> Ash.Query.sort(name: :asc)
|> Ash.read!(actor: actor)
# Load ALL custom fields for the dropdown (to show all available fields)
all_custom_fields =
Mv.Membership.CustomField
|> Ash.Query.sort(name: :asc)
|> Ash.read!(actor: actor)
custom_fields_visible =
all_custom_fields
|> Enum.filter(& &1.show_in_overview)
# Load boolean custom fields (filtered and sorted from all_custom_fields)
boolean_custom_fields =
all_custom_fields
@ -163,6 +160,7 @@ defmodule MvWeb.MemberLive.Index do
- `"delete"` - Removes a member from the database
- `"select_member"` - Toggles individual member selection
- `"select_all"` - Toggles selection of all visible members
- `"sort"` - Sort event from SortHeaderComponent. Updates sort field/order and syncs URL
"""
@impl true
def handle_event("delete", %{"id" => id}, socket) do
@ -305,6 +303,46 @@ defmodule MvWeb.MemberLive.Index do
end
end
@impl true
def handle_event("sort", %{"field" => field_str}, socket) do
# Handle both atom and string field names (for custom fields)
field =
try do
String.to_existing_atom(field_str)
rescue
ArgumentError -> field_str
end
{new_field, new_order} = determine_new_sort(field, socket)
old_field = socket.assigns.sort_field
socket =
socket
|> assign(:sort_field, new_field)
|> assign(:sort_order, new_order)
|> update_sort_components(old_field, new_field, new_order)
|> load_members()
|> update_selection_assigns()
# URL sync - push_patch happens synchronously in the event handler
query_params =
build_query_params(
socket.assigns.query,
export_sort_field(socket.assigns.sort_field),
export_sort_order(socket.assigns.sort_order),
socket.assigns.cycle_status_filter,
socket.assigns[:group_filters],
socket.assigns.show_current_cycle,
socket.assigns.boolean_custom_field_filters
)
|> maybe_add_field_selection(
socket.assigns[:user_field_selection],
socket.assigns[:fields_in_url?] || false
)
{:noreply, push_patch(socket, to: ~p"/members?#{query_params}", replace: true)}
end
# Helper to format errors for display
defp format_error(%Ash.Error.Invalid{errors: errors}) do
error_messages =
@ -329,50 +367,10 @@ defmodule MvWeb.MemberLive.Index do
Handles messages from child components.
## Supported messages:
- `{:sort, field}` - Sort event from SortHeaderComponent. Updates sort field/order and syncs URL
- `{:search_changed, query}` - Search event from SearchBarComponent. Filters members and syncs URL
- `{:field_toggled, field, visible}` - Field toggle event from FieldVisibilityDropdownComponent
- `{:fields_selected, selection}` - Select all/deselect all event from FieldVisibilityDropdownComponent
"""
@impl true
def handle_info({:sort, field_str}, socket) do
# Handle both atom and string field names (for custom fields)
field =
try do
String.to_existing_atom(field_str)
rescue
ArgumentError -> field_str
end
{new_field, new_order} = determine_new_sort(field, socket)
old_field = socket.assigns.sort_field
socket =
socket
|> assign(:sort_field, new_field)
|> assign(:sort_order, new_order)
|> update_sort_components(old_field, new_field, new_order)
|> load_members()
|> update_selection_assigns()
# URL sync
query_params =
build_query_params(
socket.assigns.query,
export_sort_field(socket.assigns.sort_field),
export_sort_order(socket.assigns.sort_order),
socket.assigns.cycle_status_filter,
socket.assigns[:group_filters],
socket.assigns.show_current_cycle,
socket.assigns.boolean_custom_field_filters
)
|> maybe_add_field_selection(
socket.assigns[:user_field_selection],
socket.assigns[:fields_in_url?] || false
)
{:noreply, push_patch(socket, to: ~p"/members?#{query_params}", replace: true)}
end
@impl true
def handle_info({:search_changed, q}, socket) do