diff --git a/lib/phoenix_kit/users/auth.ex b/lib/phoenix_kit/users/auth.ex
index e11df51b8..83eb7b2d6 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,60 @@ 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)
+
+ value = Map.get(custom_fields, key)
+
+ CustomFields.add_field_definition(%{
+ "key" => key,
+ "label" => label,
+ "type" => infer_field_type(value),
+ "enabled" => true,
+ "user_accessible" => false,
+ "position" => pos
+ })
+ end)
+ 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/^[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
+
+ defp infer_field_type(_), do: "text"
+
@doc """
Updates both schema and custom fields in a single call.
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/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
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 %>
+ <% "uuid" -> %>
+
<% "select" -> %>