From 323dd3499e2c77cbfb4c1248f53496f238af2e8d Mon Sep 17 00:00:00 2001 From: Alexander Don Date: Fri, 20 Mar 2026 11:16:16 +0200 Subject: [PATCH 1/4] Fix avatar upload handling and custom_fields preservation in UserSettings Move avatar upload detection into validate_profile handler where it actually fires, and remove the dead validate handler that was never reached. Fix update_profile to merge form custom_fields on top of all existing fields instead of only preserving avatar_file_uuid. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../live/components/user_settings.ex | 52 ++++++++----------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/lib/phoenix_kit_web/live/components/user_settings.ex b/lib/phoenix_kit_web/live/components/user_settings.ex index 40dba7685..496aae8e6 100644 --- a/lib/phoenix_kit_web/live/components/user_settings.ex +++ b/lib/phoenix_kit_web/live/components/user_settings.ex @@ -244,6 +244,23 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do def handle_event("validate_profile", params, socket) do %{"user" => user_params} = params + socket = + if params["_target"] == ["avatar"] do + entries = socket.assigns.uploads.avatar.entries + + if entries != [] do + send_update_after( + __MODULE__, + %{id: socket.assigns.id, action: :check_avatar_uploads_complete}, + 500 + ) + end + + socket + else + socket + end + socket = case {params["browser_timezone_name"], params["browser_timezone_offset"]} do {name, offset} when is_binary(name) and is_binary(offset) -> @@ -292,25 +309,17 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do # Custom fields are nested under profile_form[user][custom_fields] custom_fields_data = get_in(params, ["profile_form", "user", "custom_fields"]) + existing_custom_fields = user.custom_fields || %{} + merged_params = case custom_fields_data do custom_fields when is_map(custom_fields) -> - existing_avatar = get_in(user.custom_fields, ["avatar_file_uuid"]) - - updated_custom_fields = - if existing_avatar do - Map.put(custom_fields, "avatar_file_uuid", existing_avatar) - else - custom_fields - end - + updated_custom_fields = Map.merge(existing_custom_fields, custom_fields) Map.put(user_params, "custom_fields", updated_custom_fields) _ -> - existing_avatar = get_in(user.custom_fields, ["avatar_file_uuid"]) - - if existing_avatar do - Map.put(user_params, "custom_fields", %{"avatar_file_uuid" => existing_avatar}) + if map_size(existing_custom_fields) > 0 do + Map.put(user_params, "custom_fields", existing_custom_fields) else user_params end @@ -435,23 +444,6 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do end end - def handle_event("validate", %{"_target" => ["avatar"]}, socket) do - entries = socket.assigns.uploads.avatar.entries - Logger.info("avatar validate event: entries=#{length(entries)}") - - if entries != [] do - Logger.info("avatar validate: scheduling check_uploads_complete") - - send_update_after( - __MODULE__, - %{id: socket.assigns.id, action: :check_avatar_uploads_complete}, - 500 - ) - end - - {:noreply, socket} - end - def handle_event("cancel_upload", %{"ref" => ref}, socket) do {:noreply, cancel_upload(socket, :avatar, ref)} end From 27cb4a6d9479363c3d6aeff3e8e0952552eb05c8 Mon Sep 17 00:00:00 2001 From: Alexander Don Date: Fri, 20 Mar 2026 11:32:15 +0200 Subject: [PATCH 2/4] Add auto-registration of custom field definitions on save When update_user_custom_fields/2 writes keys that have no corresponding field definition, automatically create definitions so the data is visible in admin and user settings UIs. New fields default to enabled, admin-only (user_accessible: false), type "text", with label derived from the key. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/phoenix_kit/users/auth.ex | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/lib/phoenix_kit/users/auth.ex b/lib/phoenix_kit/users/auth.ex index e11df51b8..e3fa05cba 100644 --- a/lib/phoenix_kit/users/auth.ex +++ b/lib/phoenix_kit/users/auth.ex @@ -1423,6 +1423,8 @@ defmodule PhoenixKit.Users.Auth do {:error, %Ecto.Changeset{}} """ def update_user_custom_fields(%User{} = user, custom_fields) when is_map(custom_fields) do + ensure_field_definitions_exist(custom_fields) + case user |> User.custom_fields_changeset(%{custom_fields: custom_fields}) |> Repo.update() do @@ -1436,6 +1438,37 @@ defmodule PhoenixKit.Users.Auth do end end + defp ensure_field_definitions_exist(custom_fields) when is_map(custom_fields) do + definitions = CustomFields.list_field_definitions() + existing_keys = definitions |> Enum.map(& &1["key"]) |> MapSet.new() + new_keys = custom_fields |> Map.keys() |> Enum.reject(&MapSet.member?(existing_keys, &1)) + + unless new_keys == [] do + next_position = + case definitions do + [] -> 1 + defs -> (Enum.max_by(defs, &(&1["position"] || 0))["position"] || 0) + 1 + end + + Enum.each(Enum.with_index(new_keys, next_position), fn {key, pos} -> + label = + key + |> String.replace("_", " ") + |> String.split() + |> Enum.map_join(" ", &String.capitalize/1) + + CustomFields.add_field_definition(%{ + "key" => key, + "label" => label, + "type" => "text", + "enabled" => true, + "user_accessible" => false, + "position" => pos + }) + end) + end + end + @doc """ Updates both schema and custom fields in a single call. From 51db8beba424491332141207c30deea7e1c8d609 Mon Sep 17 00:00:00 2001 From: Alexander Don Date: Fri, 20 Mar 2026 11:43:16 +0200 Subject: [PATCH 3/4] Add type inference for auto-registered custom field definitions Instead of defaulting all auto-registered fields to "text", inspect the actual value to infer the correct type: boolean for booleans, number for numbers, url/email for matching strings, text for everything else. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/phoenix_kit/users/auth.ex | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/phoenix_kit/users/auth.ex b/lib/phoenix_kit/users/auth.ex index e3fa05cba..4c72c2332 100644 --- a/lib/phoenix_kit/users/auth.ex +++ b/lib/phoenix_kit/users/auth.ex @@ -1457,10 +1457,12 @@ defmodule PhoenixKit.Users.Auth do |> String.split() |> Enum.map_join(" ", &String.capitalize/1) + value = Map.get(custom_fields, key) + CustomFields.add_field_definition(%{ "key" => key, "label" => label, - "type" => "text", + "type" => infer_field_type(value), "enabled" => true, "user_accessible" => false, "position" => pos @@ -1469,6 +1471,19 @@ defmodule PhoenixKit.Users.Auth do end end + defp infer_field_type(value) when is_boolean(value), do: "boolean" + defp infer_field_type(value) when is_number(value), do: "number" + + defp infer_field_type(value) when is_binary(value) do + cond do + String.match?(value, ~r/^https?:\/\//) -> "url" + String.match?(value, ~r/^[^\s]+@[^\s]+\.[^\s]+$/) -> "email" + true -> "text" + end + end + + defp infer_field_type(_), do: "text" + @doc """ Updates both schema and custom fields in a single call. From eb2dd8caa9d968a86c17dbaa9cf74a943b135284 Mon Sep 17 00:00:00 2001 From: Alexander Don Date: Fri, 20 Mar 2026 11:50:30 +0200 Subject: [PATCH 4/4] Add uuid type to custom fields system Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/phoenix_kit/users/auth.ex | 14 +++++++++++--- lib/phoenix_kit/users/custom_fields.ex | 13 ++++++++++++- lib/phoenix_kit_web/live/settings/users.html.heex | 2 +- lib/phoenix_kit_web/users/user_form.html.heex | 8 ++++++++ 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/lib/phoenix_kit/users/auth.ex b/lib/phoenix_kit/users/auth.ex index 4c72c2332..83eb7b2d6 100644 --- a/lib/phoenix_kit/users/auth.ex +++ b/lib/phoenix_kit/users/auth.ex @@ -1476,9 +1476,17 @@ defmodule PhoenixKit.Users.Auth do defp infer_field_type(value) when is_binary(value) do cond do - String.match?(value, ~r/^https?:\/\//) -> "url" - String.match?(value, ~r/^[^\s]+@[^\s]+\.[^\s]+$/) -> "email" - true -> "text" + String.match?(value, ~r/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i) -> + "uuid" + + String.match?(value, ~r/^https?:\/\//) -> + "url" + + String.match?(value, ~r/^[^\s]+@[^\s]+\.[^\s]+$/) -> + "email" + + true -> + "text" end end diff --git a/lib/phoenix_kit/users/custom_fields.ex b/lib/phoenix_kit/users/custom_fields.ex index 3ce5dfa10..15892794e 100644 --- a/lib/phoenix_kit/users/custom_fields.ex +++ b/lib/phoenix_kit/users/custom_fields.ex @@ -45,7 +45,7 @@ defmodule PhoenixKit.Users.CustomFields do @setting_key "custom_user_fields_definitions" - @supported_types ~w(text textarea number boolean date email url select radio checkbox) + @supported_types ~w(text textarea number boolean date email url uuid select radio checkbox) # Field Definition Management @@ -528,6 +528,17 @@ defmodule PhoenixKit.Users.CustomFields do defp validate_type(%{"type" => "boolean"}, _value), do: {:error, "Must be true or false"} + defp validate_type(%{"type" => "uuid"} = _field_def, value) do + if String.match?( + to_string(value), + ~r/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + ) do + :ok + else + {:error, "Invalid UUID format"} + end + end + defp validate_type(%{"type" => "date"} = _field_def, value) do case Date.from_iso8601(to_string(value)) do {:ok, _date} -> :ok diff --git a/lib/phoenix_kit_web/live/settings/users.html.heex b/lib/phoenix_kit_web/live/settings/users.html.heex index 8c859ed90..1af58db14 100644 --- a/lib/phoenix_kit_web/live/settings/users.html.heex +++ b/lib/phoenix_kit_web/live/settings/users.html.heex @@ -488,7 +488,7 @@ required phx-change="field_type_changed" > - <%= for type <- ~w(text textarea number boolean date email url select radio checkbox) do %> + <%= for type <- ~w(text textarea number boolean date email url uuid select radio checkbox) do %>