232 lines
6.5 KiB
Elixir
232 lines
6.5 KiB
Elixir
defmodule MvWeb.MemberLive.Index.FieldSelection do
|
|
@moduledoc """
|
|
Handles user-specific field selection persistence and URL parameter parsing.
|
|
|
|
This module manages:
|
|
- Reading/writing field selection from cookies (persistent storage)
|
|
- Reading/writing field selection from session (temporary storage)
|
|
- Parsing field selection from URL parameters
|
|
- Merging multiple sources with priority: URL > Session > Cookie
|
|
|
|
## Data Format
|
|
|
|
Field selection is stored as a map:
|
|
```elixir
|
|
%{
|
|
"first_name" => true,
|
|
"email" => true,
|
|
"street" => false,
|
|
"custom_field_abc-123" => true
|
|
}
|
|
```
|
|
|
|
## Cookie/Session Format
|
|
|
|
Stored as JSON string: `{"first_name":true,"email":true}`
|
|
|
|
## URL Format
|
|
|
|
Comma-separated list: `?fields=first_name,email,custom_field_abc-123`
|
|
"""
|
|
|
|
@cookie_name "member_field_selection"
|
|
@cookie_max_age 365 * 24 * 60 * 60
|
|
@session_key "member_field_selection"
|
|
|
|
@doc """
|
|
Reads field selection from session.
|
|
|
|
Returns a map of field names (strings) to boolean visibility values.
|
|
Returns empty map if no selection is stored.
|
|
"""
|
|
@spec get_from_session(map()) :: %{String.t() => boolean()}
|
|
def get_from_session(session) when is_map(session) do
|
|
case Map.get(session, @session_key) do
|
|
nil -> %{}
|
|
json_string when is_binary(json_string) -> parse_json(json_string)
|
|
_ -> %{}
|
|
end
|
|
end
|
|
|
|
def get_from_session(_), do: %{}
|
|
|
|
@doc """
|
|
Saves field selection to session.
|
|
|
|
Converts the map to JSON string and stores it in the session.
|
|
"""
|
|
@spec save_to_session(map(), %{String.t() => boolean()}) :: map()
|
|
def save_to_session(session, selection) when is_map(selection) do
|
|
json_string = Jason.encode!(selection)
|
|
Map.put(session, @session_key, json_string)
|
|
end
|
|
|
|
def save_to_session(session, _), do: session
|
|
|
|
@doc """
|
|
Reads field selection from cookie.
|
|
|
|
Returns a map of field names (strings) to boolean visibility values.
|
|
Returns empty map if no cookie is present.
|
|
|
|
Note: This function requires the connection to have cookies parsed.
|
|
In LiveView, cookies are typically accessed via get_connect_info.
|
|
"""
|
|
@spec get_from_cookie(Plug.Conn.t()) :: %{String.t() => boolean()}
|
|
def get_from_cookie(conn) do
|
|
case Plug.Conn.get_req_header(conn, "cookie") do
|
|
nil ->
|
|
%{}
|
|
|
|
cookie_header ->
|
|
# Parse cookies manually from header
|
|
cookies = parse_cookie_header(cookie_header)
|
|
|
|
case Map.get(cookies, @cookie_name) do
|
|
nil -> %{}
|
|
json_string when is_binary(json_string) -> parse_json(json_string)
|
|
_ -> %{}
|
|
end
|
|
end
|
|
end
|
|
|
|
# Parses cookie header string into a map
|
|
defp parse_cookie_header(cookie_header) when is_binary(cookie_header) do
|
|
cookie_header
|
|
|> String.split(";")
|
|
|> Enum.map(&String.trim/1)
|
|
|> Enum.map(&String.split(&1, "=", parts: 2))
|
|
|> Enum.reduce(%{}, fn
|
|
[key, value], acc -> Map.put(acc, key, URI.decode(value))
|
|
[key], acc -> Map.put(acc, key, "")
|
|
_, acc -> acc
|
|
end)
|
|
end
|
|
|
|
defp parse_cookie_header(_), do: %{}
|
|
|
|
@doc """
|
|
Saves field selection to cookie.
|
|
|
|
Sets a persistent cookie with the field selection as JSON.
|
|
"""
|
|
@spec save_to_cookie(Plug.Conn.t(), %{String.t() => boolean()}) :: Plug.Conn.t()
|
|
def save_to_cookie(conn, selection) when is_map(selection) do
|
|
json_string = Jason.encode!(selection)
|
|
secure = Application.get_env(:mv, :use_secure_cookies, false)
|
|
|
|
Plug.Conn.put_resp_cookie(conn, @cookie_name, json_string,
|
|
max_age: @cookie_max_age,
|
|
same_site: "Lax",
|
|
http_only: true,
|
|
secure: secure
|
|
)
|
|
end
|
|
|
|
def save_to_cookie(conn, _), do: conn
|
|
|
|
@doc """
|
|
Parses field selection from URL parameters.
|
|
|
|
Expects a comma-separated list of field names in the `fields` parameter.
|
|
All fields in the list are set to `true` (visible).
|
|
|
|
## Examples
|
|
|
|
iex> parse_from_url(%{"fields" => "first_name,email"})
|
|
%{"first_name" => true, "email" => true}
|
|
|
|
iex> parse_from_url(%{"fields" => "custom_field_abc-123"})
|
|
%{"custom_field_abc-123" => true}
|
|
|
|
iex> parse_from_url(%{})
|
|
%{}
|
|
"""
|
|
@spec parse_from_url(map()) :: %{String.t() => boolean()}
|
|
def parse_from_url(params) when is_map(params) do
|
|
case Map.get(params, "fields") do
|
|
nil -> %{}
|
|
"" -> %{}
|
|
fields_string when is_binary(fields_string) -> parse_fields_string(fields_string)
|
|
_ -> %{}
|
|
end
|
|
end
|
|
|
|
def parse_from_url(_), do: %{}
|
|
|
|
@doc """
|
|
Merges multiple field selection sources with priority.
|
|
|
|
Priority order (highest to lowest):
|
|
1. URL parameters
|
|
2. Session
|
|
3. Cookie
|
|
|
|
Later sources override earlier ones for the same field.
|
|
|
|
## Examples
|
|
|
|
iex> merge_sources(%{"first_name" => true}, %{"email" => true}, %{"street" => true})
|
|
%{"first_name" => true, "email" => true, "street" => true}
|
|
|
|
iex> merge_sources(%{"first_name" => false}, %{"first_name" => true}, %{})
|
|
%{"first_name" => false} # URL has priority
|
|
"""
|
|
@spec merge_sources(
|
|
%{String.t() => boolean()},
|
|
%{String.t() => boolean()},
|
|
%{String.t() => boolean()}
|
|
) :: %{String.t() => boolean()}
|
|
def merge_sources(url_selection, session_selection, cookie_selection) do
|
|
%{}
|
|
|> Map.merge(cookie_selection)
|
|
|> Map.merge(session_selection)
|
|
|> Map.merge(url_selection)
|
|
end
|
|
|
|
@doc """
|
|
Converts field selection map to URL parameter string.
|
|
|
|
Returns a comma-separated string of visible fields (where value is `true`).
|
|
|
|
## Examples
|
|
|
|
iex> to_url_param(%{"first_name" => true, "email" => true, "street" => false})
|
|
"first_name,email"
|
|
"""
|
|
@spec to_url_param(%{String.t() => boolean()}) :: String.t()
|
|
def to_url_param(selection) when is_map(selection) do
|
|
selection
|
|
|> Enum.filter(fn {_field, visible} -> visible end)
|
|
|> Enum.map(fn {field, _visible} -> field end)
|
|
|> Enum.join(",")
|
|
end
|
|
|
|
def to_url_param(_), do: ""
|
|
|
|
# Parses a JSON string into a map, handling errors gracefully
|
|
defp parse_json(json_string) when is_binary(json_string) do
|
|
case Jason.decode(json_string) do
|
|
{:ok, decoded} when is_map(decoded) ->
|
|
# Ensure all values are booleans
|
|
Enum.reduce(decoded, %{}, fn
|
|
{key, value} when is_boolean(value) -> {key, value}
|
|
{key, _value} -> {key, true}
|
|
end)
|
|
|
|
_ ->
|
|
%{}
|
|
end
|
|
end
|
|
|
|
defp parse_json(_), do: %{}
|
|
|
|
# Parses a comma-separated string of field names
|
|
defp parse_fields_string(fields_string) do
|
|
fields_string
|
|
|> String.split(",")
|
|
|> Enum.map(&String.trim/1)
|
|
|> Enum.filter(&(&1 != ""))
|
|
|> Enum.reduce(%{}, fn field, acc -> Map.put(acc, field, true) end)
|
|
end
|
|
end
|