WIP: Persist member overview sort settings #228

Draft
rafael wants to merge 2 commits from persist-sort-order into main
3 changed files with 304 additions and 207 deletions

View file

@ -43,14 +43,46 @@ Hooks.ComboBox = {
destroyed() { destroyed() {
this.el.removeEventListener("keydown", this.handleKeyDown) this.el.removeEventListener("keydown", this.handleKeyDown)
},
};
// MemberSortPersistence hook: Persists sorting order to a cookie
Hooks.MemberSortPersistence = {
mounted() {
this.handleEvent("persist_sort", ({ sort_field, sort_order }) => {
const setCookie = (name, value) => {
const secure = window.location.protocol === "https:" ? "Secure" : "";
document.cookie = `${name}=${encodeURIComponent(
value
)}; path=/; SameSite=Lax; ${secure}`;
};
setCookie("member_sort_field", sort_field);
setCookie("member_sort_order", sort_order);
});
},
};
// Helper to read and decode cookie value
const getCookie = (name) => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
return decodeURIComponent(parts.pop().split(";").shift());
} }
} return null;
};
let liveSocket = new LiveSocket("/live", Socket, { let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500, longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken}, params: () => {
hooks: Hooks return {
}) _csrf_token: csrfToken,
member_sort_field: getCookie("member_sort_field"),
member_sort_order: getCookie("member_sort_order"),
};
},
hooks: Hooks,
});
// Listen for custom events from LiveView // Listen for custom events from LiveView
window.addEventListener("phx:set-input-value", (e) => { window.addEventListener("phx:set-input-value", (e) => {

View file

@ -39,6 +39,7 @@ defmodule MvWeb.MemberLive.Index do
Sets up initial assigns for page title, search query, sort configuration, Sets up initial assigns for page title, search query, sort configuration,
and member selection. Actual data loading happens in `handle_params/3`. and member selection. Actual data loading happens in `handle_params/3`.
Reads sorting preferences from cookies if available.
""" """
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
@ -52,12 +53,15 @@ defmodule MvWeb.MemberLive.Index do
|> Ash.Query.sort(name: :asc) |> Ash.Query.sort(name: :asc)
|> Ash.read!() |> Ash.read!()
# Read sorting preferences from cookies (sent via connect_params)
{sort_field, sort_order} = get_sort_from_cookies(socket)
socket = socket =
socket socket
|> assign(:page_title, gettext("Members")) |> assign(:page_title, gettext("Members"))
|> assign(:query, "") |> assign(:query, "")
|> assign_new(:sort_field, fn -> :first_name end) |> assign_new(:sort_field, fn -> sort_field end)
|> assign_new(:sort_order, fn -> :asc end) |> assign_new(:sort_order, fn -> sort_order end)
|> assign(:selected_members, []) |> assign(:selected_members, [])
|> assign(:custom_fields_visible, custom_fields_visible) |> assign(:custom_fields_visible, custom_fields_visible)
@ -139,9 +143,16 @@ defmodule MvWeb.MemberLive.Index do
{new_field, new_order} = determine_new_sort(field, socket) {new_field, new_order} = determine_new_sort(field, socket)
socket =
socket socket
|> update_sort_components(socket.assigns.sort_field, new_field, new_order) |> update_sort_components(socket.assigns.sort_field, new_field, new_order)
|> push_sort_url(new_field, new_order) # Persist sorting to cookie via JavaScript hook
|> push_event("persist_sort", %{
sort_field: new_field,
sort_order: new_order
})
push_sort_url(socket, new_field, new_order)
end end
@impl true @impl true
@ -733,4 +744,56 @@ defmodule MvWeb.MemberLive.Index do
nil nil
end end
end end
# Retrieves sorting preferences from cookies.
#
# Reads the member_sort_field and member_sort_order from connect_params (sent by JavaScript).
# Falls back to default values (:first_name, :asc) if cookies are not present or invalid.
#
# Parameters:
# - `socket` - The LiveView socket containing connect_params
#
# Returns:
# - `{field, order}` tuple where field is an atom or string, and order is :asc or :desc
defp get_sort_from_cookies(socket) do
connect_params = get_connect_params(socket) || %{}
sort_field_str = connect_params["member_sort_field"]
sort_order_str = connect_params["member_sort_order"]
field = parse_and_validate_field(sort_field_str)
order = parse_and_validate_order(sort_order_str)
{field, order}
end
# Parses and validates a sort field with configurable default.
#
# Handles both regular member fields (converted to atoms) and custom fields
# (kept as strings). Validates against allowed sort fields.
#
# Parameters:
# - `field` - Field to parse (string or nil)
# - `default` - Default field to use if parsing fails (default: :first_name)
#
# Returns a valid sort field (atom or string for custom fields).
defp parse_and_validate_field(field, default \\ :first_name)
defp parse_and_validate_field(field_str, default) when is_binary(field_str) do
if valid_sort_field?(field_str), do: field_str, else: default
end
defp parse_and_validate_field(_, default), do: default
# Parses and validates a sort order with configurable default.
#
# Ensures the order is either :asc or :desc.
#
# Parameters:
# - `order` - Order to parse, as a string or nil
#
# Returns `:asc` or `:desc`.
defp parse_and_validate_order("asc"), do: :asc
defp parse_and_validate_order("desc"), do: :desc
defp parse_and_validate_order(_), do: :asc
end end

View file

@ -1,4 +1,5 @@
<Layouts.app flash={@flash} current_user={@current_user}> <Layouts.app flash={@flash} current_user={@current_user}>
<div id="member-sort-persistence" phx-hook="MemberSortPersistence">
<.header> <.header>
{gettext("Members")} {gettext("Members")}
<:actions> <:actions>
@ -205,4 +206,5 @@
</.link> </.link>
</:action> </:action>
</.table> </.table>
</div>
</Layouts.app> </Layouts.app>