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 %>