Implements uneditable type for custom fields closes #198 #433
3 changed files with 43 additions and 49 deletions
|
|
@ -26,7 +26,6 @@ defmodule MvWeb.Components.SortHeaderComponent do
|
||||||
class="btn btn-ghost select-none"
|
class="btn btn-ghost select-none"
|
||||||
phx-click="sort"
|
phx-click="sort"
|
||||||
phx-value-field={@field}
|
phx-value-field={@field}
|
||||||
phx-target={@myself}
|
|
||||||
data-testid={@field}
|
data-testid={@field}
|
||||||
>
|
>
|
||||||
{@label}
|
{@label}
|
||||||
|
|
@ -43,12 +42,6 @@ defmodule MvWeb.Components.SortHeaderComponent do
|
||||||
"""
|
"""
|
||||||
end
|
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
|
# Hilfsfunktionen für ARIA Attribute & Icon SVG
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
- `"delete"` - Removes a member from the database
|
- `"delete"` - Removes a member from the database
|
||||||
- `"select_member"` - Toggles individual member selection
|
- `"select_member"` - Toggles individual member selection
|
||||||
- `"select_all"` - Toggles selection of all visible members
|
- `"select_all"` - Toggles selection of all visible members
|
||||||
|
- `"sort"` - Sort event from SortHeaderComponent. Updates sort field/order and syncs URL
|
||||||
"""
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("delete", %{"id" => id}, socket) do
|
def handle_event("delete", %{"id" => id}, socket) do
|
||||||
|
|
@ -305,6 +306,46 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
end
|
end
|
||||||
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
|
# Helper to format errors for display
|
||||||
defp format_error(%Ash.Error.Invalid{errors: errors}) do
|
defp format_error(%Ash.Error.Invalid{errors: errors}) do
|
||||||
error_messages =
|
error_messages =
|
||||||
|
|
@ -329,50 +370,10 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
Handles messages from child components.
|
Handles messages from child components.
|
||||||
|
|
||||||
## Supported messages:
|
## 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
|
- `{:search_changed, query}` - Search event from SearchBarComponent. Filters members and syncs URL
|
||||||
- `{:field_toggled, field, visible}` - Field toggle event from FieldVisibilityDropdownComponent
|
- `{:field_toggled, field, visible}` - Field toggle event from FieldVisibilityDropdownComponent
|
||||||
- `{:fields_selected, selection}` - Select all/deselect all 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
|
@impl true
|
||||||
def handle_info({:search_changed, q}, socket) do
|
def handle_info({:search_changed, q}, socket) do
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "component behavior" do
|
describe "component behavior" do
|
||||||
test "clicking sends sort message to parent", %{conn: conn} do
|
test "clicking triggers sort event on parent LiveView", %{conn: conn} do
|
||||||
conn = conn_with_oidc_user(conn)
|
conn = conn_with_oidc_user(conn)
|
||||||
{:ok, view, _html} = live(conn, "/members")
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
|
@ -232,7 +232,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
||||||
|> element("button[phx-value-field='first_name']")
|
|> element("button[phx-value-field='first_name']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# The component should send a message to the parent LiveView
|
# The component triggers a "sort" event on the parent LiveView
|
||||||
# This is tested indirectly through the URL change in integration tests
|
# This is tested indirectly through the URL change in integration tests
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue