diff --git a/lib/modules/sync/web/connections_live.ex b/lib/modules/sync/web/connections_live.ex
index 1111486cd..4c2ab3538 100644
--- a/lib/modules/sync/web/connections_live.ex
+++ b/lib/modules/sync/web/connections_live.ex
@@ -1116,6 +1116,8 @@ defmodule PhoenixKit.Modules.Sync.Web.ConnectionsLive do
end
# Get all FK dependencies for a table (recursive)
+ @dialyzer {:nowarn_function, get_table_dependencies: 2}
+ @dialyzer {:nowarn_function, get_table_dependencies: 3}
defp get_table_dependencies(table_name, tables) do
get_table_dependencies(table_name, tables, MapSet.new())
|> MapSet.to_list()
@@ -1159,6 +1161,7 @@ defmodule PhoenixKit.Modules.Sync.Web.ConnectionsLive do
topo_sort(graph)
end
+ @dialyzer {:nowarn_function, topo_sort: 4}
defp topo_sort(graph) do
topo_sort(graph, Map.keys(graph), [], MapSet.new())
end
@@ -1174,6 +1177,7 @@ defmodule PhoenixKit.Modules.Sync.Web.ConnectionsLive do
end
end
+ @dialyzer {:nowarn_function, visit_node: 5}
defp visit_node(graph, node, sorted, visited, path) do
if MapSet.member?(visited, node) do
{sorted, visited}
diff --git a/lib/phoenix_kit_web/live/components/user_settings.ex b/lib/phoenix_kit_web/live/components/user_settings.ex
index ababf9e70..40dba7685 100644
--- a/lib/phoenix_kit_web/live/components/user_settings.ex
+++ b/lib/phoenix_kit_web/live/components/user_settings.ex
@@ -119,6 +119,8 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do
CustomFields.list_user_accessible_field_definitions()
end)
|> assign_new(:last_uploaded_avatar_uuid, fn -> nil end)
+ |> assign_new(:show_email_form, fn -> false end)
+ |> assign_new(:show_password_form, fn -> false end)
|> maybe_allow_upload()
{:ok, socket}
@@ -253,8 +255,11 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do
socket
end
+ # Custom fields are nested under profile_form[user][custom_fields]
+ custom_fields_data = get_in(params, ["profile_form", "user", "custom_fields"])
+
merged_params =
- case params["custom_fields"] do
+ case custom_fields_data do
custom_fields when is_map(custom_fields) ->
Map.put(user_params, "custom_fields", custom_fields)
@@ -284,8 +289,11 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do
%{"user" => user_params} = params
user = socket.assigns.user
+ # Custom fields are nested under profile_form[user][custom_fields]
+ custom_fields_data = get_in(params, ["profile_form", "user", "custom_fields"])
+
merged_params =
- case params["custom_fields"] do
+ case custom_fields_data do
custom_fields when is_map(custom_fields) ->
existing_avatar = get_in(user.custom_fields, ["avatar_file_uuid"])
@@ -448,6 +456,14 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do
{:noreply, cancel_upload(socket, :avatar, ref)}
end
+ def handle_event("toggle_email_form", _params, socket) do
+ {:noreply, assign(socket, :show_email_form, not socket.assigns.show_email_form)}
+ end
+
+ def handle_event("toggle_password_form", _params, socket) do
+ {:noreply, assign(socket, :show_password_form, not socket.assigns.show_password_form)}
+ end
+
# Private helpers
defp check_timezone_mismatch(socket, selected_timezone) do
@@ -638,530 +654,351 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do
@impl Phoenix.LiveComponent
def render(assigns) do
~H"""
-
-
- <%!-- Left Column - Profile --%>
-
- <%!-- Profile Information Card --%>
- <%= if :profile in @sections do %>
-
-
-
- <.icon name="hero-user" class="w-5 h-5" /> Profile Information
-
-
- <%!-- Success Message --%>
- <%= if @profile_success_message do %>
-
- <.icon name="hero-check" class="stroke-current shrink-0 h-5 w-5" />
- {@profile_success_message}
-
- <% end %>
-
- <%!-- Avatar Upload Section --%>
-
-
- Profile Picture
-
-
-
- <%!-- Avatar Preview --%>
-
- <%= if get_in(@user.custom_fields, ["avatar_file_uuid"]) do %>
- <% avatar_url =
- PhoenixKit.Modules.Storage.URLSigner.signed_url(
- get_in(@user.custom_fields, ["avatar_file_uuid"]),
- "thumbnail"
- ) %>
-
- <% else %>
-
-
- {String.upcase(String.at(@user.email, 0))}
-
-
- <% end %>
-
-
- <%!-- Upload Controls --%>
-
- <.file_upload
- upload={@uploads.avatar}
- variant="button"
- label="Choose Profile Picture"
- />
-
-
- Upload a profile picture (max 10MB). Accepts JPG, PNG, GIF.
-
-
- <%!-- Success Message --%>
- <%= if @last_uploaded_avatar_uuid do %>
-
- <.icon name="hero-check" class="stroke-current shrink-0 h-5 w-5" />
- Avatar uploaded successfully!
-
- <% end %>
-
- <%!-- Avatar Error Message --%>
- <%= if @avatar_error_message do %>
-
- <.icon
- name="hero-exclamation-triangle"
- class="stroke-current shrink-0 h-5 w-5"
- />
- {@avatar_error_message}
-
- <% end %>
+
+
+
+ <.icon name="hero-user-circle" class="w-6 h-6" /> Account Settings
+
+
+ <%!-- Profile Section --%>
+ <%= if :profile in @sections do %>
+
+ <%!-- Profile Form with Avatar --%>
+ <.simple_form
+ for={@profile_form}
+ id={"#{@id}-profile-form"}
+ phx-submit="update_profile"
+ phx-change="validate_profile"
+ phx-target={@myself}
+ >
+
+ <%!-- Avatar Section --%>
+
+ <%= if get_in(@user.custom_fields, ["avatar_file_uuid"]) do %>
+ <% avatar_url =
+ PhoenixKit.Modules.Storage.URLSigner.signed_url(
+ get_in(@user.custom_fields, ["avatar_file_uuid"]),
+ "thumbnail"
+ ) %>
+
+ <% else %>
+
+
+ {String.upcase(String.at(@user.email, 0))}
+
-
-
- <%!-- Divider after avatar section --%>
-
+ <% end %>
+ <.file_upload
+ upload={@uploads.avatar}
+ variant="button"
+ label="Upload"
+ />
- <.simple_form
- for={@profile_form}
- id={"#{@id}-profile-form"}
- phx-submit="update_profile"
- phx-change="validate_profile"
- phx-target={@myself}
- >
-
- <.input
- field={@profile_form[:first_name]}
- type="text"
- label="First Name"
- />
+ <%!-- Name Fields --%>
+
+ <.input
+ field={@profile_form[:first_name]}
+ type="text"
+ label="First Name"
+ />
+ <.input
+ field={@profile_form[:last_name]}
+ type="text"
+ label="Last Name"
+ />
+
<.input
- field={@profile_form[:last_name]}
+ field={@profile_form[:username]}
type="text"
- label="Last Name"
+ label="Username"
/>
+
-
- <%!-- Custom Fields Section --%>
- <%= if length(@custom_field_definitions) > 0 do %>
-
Additional Information
-
- <%= for field_def <- @custom_field_definitions do %>
- <%= case field_def["type"] do %>
- <% "text" -> %>
- <.input
- name={"custom_fields[#{field_def["key"]}]"}
- type="text"
- label={field_def["label"]}
- value={
- get_in(@user.custom_fields, [field_def["key"]]) ||
- field_def["default"]
- }
- required={field_def["required"]}
- />
- <% "textarea" -> %>
- <.textarea
- name={"custom_fields[#{field_def["key"]}]"}
- label={field_def["label"]}
- value={
- get_in(@user.custom_fields, [field_def["key"]]) ||
- field_def["default"]
- }
- required={field_def["required"]}
- />
- <% "number" -> %>
- <.input
- name={"custom_fields[#{field_def["key"]}]"}
- type="number"
- label={field_def["label"]}
- value={
- get_in(@user.custom_fields, [field_def["key"]]) ||
- field_def["default"]
- }
- required={field_def["required"]}
- />
- <% "email" -> %>
- <.input
- name={"custom_fields[#{field_def["key"]}]"}
- type="email"
- label={field_def["label"]}
- value={
- get_in(@user.custom_fields, [field_def["key"]]) ||
- field_def["default"]
- }
- required={field_def["required"]}
- />
- <% "url" -> %>
- <.input
- name={"custom_fields[#{field_def["key"]}]"}
- type="url"
- label={field_def["label"]}
- value={
- get_in(@user.custom_fields, [field_def["key"]]) ||
- field_def["default"]
- }
- required={field_def["required"]}
- />
- <% "date" -> %>
- <.input
- name={"custom_fields[#{field_def["key"]}]"}
- type="date"
- label={field_def["label"]}
- value={
- get_in(@user.custom_fields, [field_def["key"]]) ||
- field_def["default"]
- }
- required={field_def["required"]}
- />
+ <%= for field <- @custom_field_definitions do %>
+ <% field_name = "profile_form[user][custom_fields][#{field["key"]}]" %>
+ <% field_id = "profile_form_user_custom_fields_#{field["key"]}" %>
+ <% field_value =
+ get_in(@user.custom_fields, [field["key"]]) || field["default"] || "" %>
+
+
+ {field["label"]}
+
+ <%= case field["type"] do %>
<% "select" -> %>
- <.select
- name={"custom_fields[#{field_def["key"]}]"}
- label={field_def["label"]}
- options={Enum.map(field_def["options"], &{&1, &1})}
- value={
- get_in(@user.custom_fields, [field_def["key"]]) ||
- field_def["default"]
- }
- required={field_def["required"]}
- />
+
+ <%= for {option, index} <- Enum.with_index(field["options"] || []) do %>
+ <% {label, _value} =
+ if is_binary(option),
+ do: {option, option},
+ else: {option["label"], option["value"]} %>
+ <% index_str = to_string(index) %>
+ <% field_value_str = to_string(field_value) %>
+ <% is_selected = field_value_str == index_str %>
+
+ {label}
+
+ <% end %>
+
<% _ -> %>
- <%!-- Fallback for unknown field types --%>
- <.input
- name={"custom_fields[#{field_def["key"]}]"}
+
<% end %>
- <% end %>
- <% end %>
-
-
- <.select
- field={@profile_form[:user_timezone]}
- label="Personal Timezone"
- options={@timezone_options}
- />
-
- <%!-- Timezone Mismatch Warning --%>
- <%= if assigns[:timezone_mismatch_warning] do %>
-
- <.icon
- name="hero-exclamation-triangle"
- class="stroke-current shrink-0 h-5 w-5"
- />
-
-
Timezone Mismatch Detected
-
- {@timezone_mismatch_warning}
-
-
-
- <% end %>
-
- <%!-- Browser Timezone Info --%>
- <%= if assigns[:browser_timezone_name] do %>
-
- Browser detected: {@browser_timezone_name} ({@browser_timezone_offset})
-
- <% end %>
-
- <%!-- Debug button for timezone detection --%>
-
-
- Detect Browser Timezone (Debug)
-
-
- Click if timezone detection isn't working automatically
-
-
- <:actions>
- <.button
- phx-disable-with="Updating..."
- class="btn-primary"
- >
- <.icon name="hero-user" class="w-4 h-4 mr-2" /> Update Profile
-
-
-
+ <% end %>
+
-
- <% end %>
-
- <%!-- Email Settings Card --%>
- <%= if :email in @sections do %>
-
-
-
- <.icon name="hero-envelope" class="w-5 h-5" /> Email Address
-
-
Change your account email address
-
- <%!-- Email Success Message --%>
- <%= if @email_success_message do %>
-
- <.icon name="hero-check" class="stroke-current shrink-0 h-5 w-5" />
- {@email_success_message}
-
- <% end %>
-
- <%!-- Email Error Message --%>
- <%= if @email_error_message do %>
-
- <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-5 w-5" />
- {@email_error_message}
-
- <% end %>
- <.simple_form
- for={@email_form}
- id={"#{@id}-email-form"}
- phx-submit="update_email"
- phx-change="validate_email"
- phx-target={@myself}
- >
- <.input
- field={@email_form[:email]}
- type="email"
- label="Email"
- required
- />
- <.input
- field={@email_form[:current_password]}
- name="current_password"
- id={"#{@id}-current-password-for-email"}
- type="password"
- label="Current password"
- value={@email_form_current_password}
- required
- />
- <:actions>
- <.button
- phx-disable-with="Changing..."
- class="btn-primary"
- >
- <.icon name="hero-envelope" class="w-4 h-4 mr-2" /> Change Email
-
-
-
-
+ <:actions>
+
+ <.button phx-disable-with="Updating..." class="btn-primary">
+ Update Profile
+
+
+
+
+
+
+ <% end %>
+
+ <%!-- Email Section --%>
+ <%= if :email in @sections do %>
+
+
+
+ <.icon name="hero-envelope" class="w-5 h-5 text-primary" /> Email Address
+
+
+ <.icon
+ name={if @show_email_form, do: "hero-x-mark", else: "hero-pencil"}
+ class="w-4 h-4"
+ />
+ {if @show_email_form, do: "Cancel", else: "Change Email"}
+
- <% end %>
-
- <%!-- Password Settings Card --%>
- <%= if :password in @sections do %>
-
-
-
- <.icon name="hero-lock-closed" class="w-5 h-5" /> Password
-
-
Update your account password
-
- <%!-- Password Success Message --%>
- <%= if @password_success_message do %>
-
- <.icon name="hero-check" class="stroke-current shrink-0 h-5 w-5" />
- {@password_success_message}
-
- <% end %>
- <%!-- Password Error Message --%>
- <%= if @password_error_message do %>
-
- <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-5 w-5" />
- {@password_error_message}
-
- <% end %>
+
{@current_email}
- <.simple_form
- for={@password_form}
- id={"#{@id}-password-form"}
- action={Routes.path("/users/log-in?_action=password_updated")}
- method="post"
- phx-change="validate_password"
- phx-submit="update_password"
- phx-trigger-action={@trigger_submit}
- phx-target={@myself}
- >
-
- <.input
- field={@password_form[:password]}
- type="password"
- label="New password"
- required
- />
- <.input
- field={@password_form[:password_confirmation]}
- type="password"
- label="Confirm new password"
- />
- <.input
- field={@password_form[:current_password]}
- name="current_password"
- type="password"
- label="Current password"
- id={"#{@id}-current-password-for-password"}
- value={@current_password}
- required
- />
- <:actions>
- <.button
- phx-disable-with="Changing..."
- class="btn-primary"
- >
- <.icon name="hero-lock-closed" class="w-4 h-4 mr-2" /> Change Password
-
-
-
+ <%= if @email_success_message do %>
+
+ <.icon name="hero-check" class="stroke-current shrink-0 h-4 w-4" />
+ {@email_success_message}
-
- <% end %>
-
-
- <%!-- Right Column --%>
-
- <%!-- Connected Accounts Card --%>
- <%= if :oauth in @sections and @oauth_available do %>
-
-
-
- <.icon name="hero-link" class="w-5 h-5" /> Connected Accounts
-
-
- Manage OAuth providers for quick sign-in
-
-
- <%!-- OAuth Success Message --%>
- <%= if @oauth_success_message do %>
-
- <.icon name="hero-check" class="stroke-current shrink-0 h-5 w-5" />
-
{@oauth_success_message}
+ <% end %>
+ <%= if @email_error_message do %>
+
+ <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-4 w-4" />
+ {@email_error_message}
+
+ <% end %>
+
+ <%= if @show_email_form do %>
+ <.simple_form
+ for={@email_form}
+ id={"#{@id}-email-form"}
+ phx-submit="update_email"
+ phx-change="validate_email"
+ phx-target={@myself}
+ >
+ <.input
+ field={@email_form[:email]}
+ type="email"
+ label="New Email"
+ required
+ />
+ <.input
+ field={@email_form[:current_password]}
+ name="current_password"
+ type="password"
+ label="Current Password"
+ value={@email_form_current_password}
+ required
+ />
+ <:actions>
+
+ <.button phx-disable-with="Changing..." class="btn-primary">
+ Update Email
+
- <% end %>
+
+
+ <% end %>
+
+ <% end %>
+
+
+
+ <%!-- Password Section --%>
+ <%= if :password in @sections do %>
+
+
+
+ <.icon name="hero-lock-closed" class="w-5 h-5 text-primary" /> Password
+
+
+ <.icon
+ name={if @show_password_form, do: "hero-x-mark", else: "hero-pencil"}
+ class="w-4 h-4"
+ />
+ {if @show_password_form, do: "Cancel", else: "Change Password"}
+
+
- <%!-- OAuth Error Message --%>
- <%= if @oauth_error_message do %>
-
- <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-5 w-5" />
- {@oauth_error_message}
-
- <% end %>
+
••••••••
- <%!-- Connected Providers List --%>
- <%= if length(@oauth_providers) > 0 do %>
-
-
Connected Providers
- <%= for provider <- @oauth_providers do %>
-
-
- <%= case provider.provider do %>
- <% "google" -> %>
- <.icon name="hero-globe-alt" class="w-6 h-6" />
- <% "apple" -> %>
- <.icon name="hero-device-phone-mobile" class="w-6 h-6" />
- <% "github" -> %>
- <.icon name="hero-code-bracket" class="w-6 h-6" />
- <% _ -> %>
- <.icon name="hero-link" class="w-6 h-6" />
- <% end %>
-
-
- {format_provider_name(provider.provider)}
-
-
- {provider.provider_email || @current_email}
-
-
-
-
- <.icon name="hero-trash" class="w-4 h-4 mr-1" /> Disconnect
-
-
- <% end %>
-
- <% else %>
-
- <.icon name="hero-information-circle" class="stroke-current shrink-0 h-5 w-5" />
-
- You don't have any OAuth providers connected yet. Connect one for faster sign-in.
-
+ <%= if @password_success_message do %>
+
+ <.icon name="hero-check" class="stroke-current shrink-0 h-4 w-4" />
+ {@password_success_message}
+
+ <% end %>
+ <%= if @password_error_message do %>
+
+ <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-4 w-4" />
+ {@password_error_message}
+
+ <% end %>
+
+ <%= if @show_password_form do %>
+ <.simple_form
+ for={@password_form}
+ id={"#{@id}-password-form"}
+ action={Routes.path("/users/log-in?_action=password_updated")}
+ method="post"
+ phx-change="validate_password"
+ phx-submit="update_password"
+ phx-trigger-action={@trigger_submit}
+ phx-target={@myself}
+ >
+
+ <.input
+ field={@password_form[:password]}
+ type="password"
+ label="New Password"
+ required
+ />
+ <.input
+ field={@password_form[:password_confirmation]}
+ type="password"
+ label="Confirm New Password"
+ />
+ <.input
+ field={@password_form[:current_password]}
+ name="current_password"
+ type="password"
+ label="Current Password"
+ value={@current_password}
+ required
+ />
+ <:actions>
+
+ <.button phx-disable-with="Changing..." class="btn-primary">
+ Update Password
+
- <% end %>
-
- <%!-- Available Providers to Connect --%>
- <%= if length(@available_providers) > 0 do %>
-
-
- Connect Additional Providers
-
-
- <%= for provider <- @available_providers do %>
-
- <%= case provider do %>
- <% "google" -> %>
- <.icon name="hero-globe-alt" class="w-5 h-5" />
- Connect Google Account
- <% "apple" -> %>
- <.icon name="hero-device-phone-mobile" class="w-5 h-5" />
- Connect Apple Account
- <% "github" -> %>
- <.icon name="hero-code-bracket" class="w-5 h-5" />
- Connect GitHub Account
- <% _ -> %>
- <.icon name="hero-link" class="w-5 h-5" />
- Connect {format_provider_name(provider)}
- <% end %>
-
+
+
+ <% end %>
+
+ <% end %>
+
+ <%!-- OAuth Section --%>
+ <%= if :oauth in @sections and @oauth_available do %>
+
+
+ <.icon name="hero-link" class="w-5 h-5 text-primary" /> Connected Accounts
+
+
+ <%= if @oauth_success_message do %>
+
+ <.icon name="hero-check" class="stroke-current shrink-0 h-4 w-4" />
+ {@oauth_success_message}
+
+ <% end %>
+ <%= if @oauth_error_message do %>
+
+ <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-4 w-4" />
+ {@oauth_error_message}
+
+ <% end %>
+
+ <%!-- Connected Providers --%>
+ <%= if length(@oauth_providers) > 0 do %>
+
+ <%= for provider <- @oauth_providers do %>
+
+
+ <%= case provider.provider do %>
+ <% "google" -> %>
+ <.icon name="hero-globe-alt" class="w-5 h-5" />
+ <% "github" -> %>
+ <.icon name="hero-code-bracket" class="w-5 h-5" />
+ <% _ -> %>
+ <.icon name="hero-link" class="w-5 h-5" />
<% end %>
+ {format_provider_name(provider.provider)}
+
+ Disconnect
+
<% end %>
-
- <%!-- Password Warning for OAuth-only Users --%>
- <%= if length(@oauth_providers) > 0 and @user.hashed_password == nil do %>
-
- <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-5 w-5" />
-
-
No Password Set
-
- You signed up using OAuth. Consider setting a password above as a backup sign-in method.
-
-
-
+
+ <% end %>
+
+ <%!-- Available Providers --%>
+ <%= if length(@available_providers) > 0 do %>
+
+ <%= for provider <- @available_providers do %>
+
+ <.icon name="hero-plus" class="w-4 h-4 mr-1" />
+ Connect {format_provider_name(provider)}
+
<% end %>
-
- <% end %>
-
+ <% end %>
+
+ <% end %>
"""