From 0eb3b912b3ff4e4cd2cfe5443a2801ae02918839 Mon Sep 17 00:00:00 2001 From: "Elias W. BA" Date: Wed, 11 Sep 2024 11:41:40 +0000 Subject: [PATCH] User profile page: basic info form (#2470) * Add basic info form to the profile page and update forms to use new inputs * Disable save button when no changes detected in the form * Test update basic user information * Tooltip on select box for contact pref --------- Co-authored-by: Stuart Corbishley --- CHANGELOG.md | 2 + lib/lightning/accounts.ex | 10 +- lib/lightning/accounts/user.ex | 14 ++ lib/lightning_web/components/new_inputs.ex | 21 ++ .../live/profile_live/edit.html.heex | 2 +- .../live/profile_live/form_component.ex | 42 +++- .../profile_live/form_component.html.heex | 210 ++++++++++-------- test/lightning_web/live/profile_live_test.exs | 70 +++++- 8 files changed, 266 insertions(+), 105 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 420a7434ae..ea6d645393 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to ### Changed +- Enhance user profile page to add a section for updating basic information + [#2470](https://github.com/OpenFn/lightning/pull/2470) - Upgraded Heroicons to v2.1.5, from v2.0.18 [#2483](https://github.com/OpenFn/lightning/pull/2483) - Standardize `link-uuid` style for uuid chips diff --git a/lib/lightning/accounts.ex b/lib/lightning/accounts.ex index bd10258cfd..278bdf5d8d 100644 --- a/lib/lightning/accounts.ex +++ b/lib/lightning/accounts.ex @@ -389,6 +389,14 @@ defmodule Lightning.Accounts do |> Repo.update() end + def change_user_info(%User{} = user, attrs \\ %{}) do + User.info_changeset(user, attrs) + end + + def update_user_info(%User{} = user, attrs) do + change_user_info(user, attrs) |> Repo.update() + end + ## Settings @doc """ @@ -510,7 +518,7 @@ defmodule Lightning.Accounts do ) end - def validate_change_user_email(user, params) do + def validate_change_user_email(user, params \\ %{}) do data = %{email: nil, current_password: nil} types = %{email: :string, current_password: :string} diff --git a/lib/lightning/accounts/user.ex b/lib/lightning/accounts/user.ex index 1395ed8c4a..0878afad34 100644 --- a/lib/lightning/accounts/user.ex +++ b/lib/lightning/accounts/user.ex @@ -221,6 +221,20 @@ defmodule Lightning.Accounts.User do |> validate_role() end + @doc """ + A user changeset for basic information: + + - first_name + - last_name + - contact_preference + """ + def info_changeset(user, attrs) do + user + |> cast(attrs, [:first_name, :last_name, :contact_preference]) + |> validate_name() + |> trim_name() + end + @doc """ A user changeset for changing the email. diff --git a/lib/lightning_web/components/new_inputs.ex b/lib/lightning_web/components/new_inputs.ex index 311632b6c2..bdab84cad9 100644 --- a/lib/lightning_web/components/new_inputs.ex +++ b/lib/lightning_web/components/new_inputs.ex @@ -77,6 +77,24 @@ defmodule LightningWeb.Components.NewInputs do """ end + attr :id, :string, required: true + attr :tooltip, :string, required: true + attr :class, :string, default: "" + attr :icon, :string, default: "hero-information-circle-solid" + attr :icon_class, :string, default: "w-4 h-4 text-primary-600 opacity-50" + + defp tooltip_for_label(assigns) do + classes = ~w"relative cursor-pointer" + + assigns = assign(assigns, class: classes ++ List.wrap(assigns.class)) + + ~H""" + + <.icon name={@icon} class={@icon_class} /> + + """ + end + @doc """ Renders an input with label and error messages. @@ -154,6 +172,8 @@ defmodule LightningWeb.Components.NewInputs do attr :display_errors, :boolean, default: true + attr :tooltip, :any, default: nil + slot :inner_block def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do @@ -215,6 +235,7 @@ defmodule LightningWeb.Components.NewInputs do :if={Map.get(@rest, :required, false)} class="text-red-500" > * + <.tooltip_for_label :if={@tooltip} id={@id} tooltip={@tooltip} />
diff --git a/lib/lightning_web/live/profile_live/edit.html.heex b/lib/lightning_web/live/profile_live/edit.html.heex index 7c6b0549af..0939ec18d1 100644 --- a/lib/lightning_web/live/profile_live/edit.html.heex +++ b/lib/lightning_web/live/profile_live/edit.html.heex @@ -11,7 +11,7 @@ <%= @current_user.first_name %> <%= @current_user.last_name %>

- Change email, change password, and request deletion. + Change name, email, password, and request deletion.

diff --git a/lib/lightning_web/live/profile_live/form_component.ex b/lib/lightning_web/live/profile_live/form_component.ex index 85a13c6142..a43a132f5a 100644 --- a/lib/lightning_web/live/profile_live/form_component.ex +++ b/lib/lightning_web/live/profile_live/form_component.ex @@ -11,10 +11,11 @@ defmodule LightningWeb.ProfileLive.FormComponent do {:ok, socket |> assign( - password_changeset: Accounts.change_user_password(user), - email_changeset: user |> Accounts.validate_change_user_email(%{}), user: user, - action: action + action: action, + email_changeset: Accounts.validate_change_user_email(user), + password_changeset: Accounts.change_user_password(user), + user_info_changeset: Accounts.change_user_info(user) )} end @@ -71,6 +72,20 @@ defmodule LightningWeb.ProfileLive.FormComponent do end end + @impl true + def handle_event("update_basic_info", %{"user" => user_params}, socket) do + case Accounts.update_user_info(socket.assigns.user, user_params) do + {:ok, _} -> + {:noreply, + socket + |> put_flash(:info, "User information updated successfully") + |> push_navigate(to: ~p"/profile")} + + {:error, changeset} -> + {:noreply, assign(socket, :user_info_changeset, changeset)} + end + end + @impl true def handle_event("validate_password", %{"user" => user_params}, socket) do changeset = @@ -90,4 +105,25 @@ defmodule LightningWeb.ProfileLive.FormComponent do {:noreply, assign(socket, :email_changeset, changeset)} end + + @impl true + def handle_event("validate_basic_info", %{"user" => user_params}, socket) do + changeset = + socket.assigns.user + |> Accounts.change_user_info(user_params) + |> Map.put(:action, :validate) + + {:noreply, assign(socket, :user_info_changeset, changeset)} + end + + def enum_options(module, field) do + value_label_map = %{ + critical: "Critical", + any: "Anytime" + } + + module + |> Ecto.Enum.values(field) + |> Enum.map(&{Map.get(value_label_map, &1, to_string(&1)), &1}) + end end diff --git a/lib/lightning_web/live/profile_live/form_component.html.heex b/lib/lightning_web/live/profile_live/form_component.html.heex index 41a3263b68..47cd3fa9cd 100644 --- a/lib/lightning_web/live/profile_live/form_component.html.heex +++ b/lib/lightning_web/live/profile_live/form_component.html.heex @@ -8,6 +8,60 @@ return_to={~p"/profile"} /> <% end %> + + <.form + :let={f} + as={:user} + for={@user_info_changeset} + id="basic-info-form" + phx-target={@myself} + phx-change="validate_basic_info" + phx-submit="update_basic_info" + class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2 mb-4 px-4 py-6 sm:p-8" + > +

Change basic information
+
+
+ <.input + type="text" + field={f[:first_name]} + label="First name" + required="true" + /> +
+
+ <.input + type="text" + field={f[:last_name]} + label="Last name" + required="true" + /> +
+
+ <.input + type="select" + field={f[:contact_preference]} + label="Contact preference" + required="true" + tooltip="How often would you like to hear from us about your OpenFn account? Choose 'Anytime' to stay in up to date with all information, or 'Critical' if you prefer to be notified only about issues affecting your projects and account status." + options={enum_options(Lightning.Accounts.User, :contact_preference)} + /> +
+
+ + + <.form :let={f} as={:user} @@ -15,45 +69,39 @@ phx-change="validate_email" phx-submit="change_email" phx-target={@myself} - id="email_form" - class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2 mb-4" + id="email-form" + class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2 mb-4 px-4 py-6 sm:p-8" > -
-
-
Change email
-
- <%= Phoenix.HTML.Form.label(f, :email, "New email address", - class: "block text-sm font-medium text-secondary-700" - ) %> - <%= Phoenix.HTML.Form.text_input(f, :email, - class: "block w-full rounded-md" - ) %> - <.old_error field={f[:email]} /> -
-
- <%= Phoenix.HTML.Form.label( - f, - :current_password, - "Enter password to confirm", - class: "block text-sm font-medium text-secondary-700" - ) %> - <%= Phoenix.HTML.Form.password_input(f, :current_password, - value: Phoenix.HTML.Form.input_value(f, :current_password), - class: "block w-full rounded-md" - ) %> - <.old_error field={f[:current_password]} /> -
-
- - Update email - -
- <%= Phoenix.HTML.Form.hidden_input(f, :id) %> +
Change email
+
+
+ <.input + type="text" + field={f[:email]} + label="New email address" + required="true" + />
+
+ <.input + type="text" + field={f[:current_password]} + label="Enter password to confirm" + required="true" + /> +
+
+ + <.form @@ -62,61 +110,47 @@ phx-change="validate_password" phx-submit="save_password" phx-target={@myself} - id="password_form" - class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2 mb-4" + id="password-form" + class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2 mb-4 px-4 py-6 sm:p-8" > -
-
-
Change password
-
- <%= Phoenix.HTML.Form.label(f, :password, "New password", - class: "block text-sm font-medium text-secondary-700" - ) %> - <%= Phoenix.HTML.Form.password_input(f, :password, - value: Phoenix.HTML.Form.input_value(f, :password), - class: "block w-full rounded-md", - phx_debounce: "blur" - ) %> - <.old_error field={f[:password]} /> -
- -
- <%= Phoenix.HTML.Form.label( - f, - :password_confirmation, - "Confirm new password", - class: "block text-sm font-medium text-secondary-700" - ) %> - <%= Phoenix.HTML.Form.password_input(f, :password_confirmation, - value: Phoenix.HTML.Form.input_value(f, :password_confirmation), - class: "block w-full rounded-md", - phx_debounce: "blur" - ) %> - <.old_error field={f[:password_confirmation]} /> -
-
- <%= Phoenix.HTML.Form.label(f, :current_password, - class: "block text-sm font-medium text-secondary-700" - ) %> - <%= Phoenix.HTML.Form.password_input(f, :current_password, - value: Phoenix.HTML.Form.input_value(f, :current_password), - class: "block w-full rounded-md", - phx_debounce: "blur" - ) %> - <.old_error field={f[:current_password]} /> -
-
- - - Update password - - -
+
Change password
+
+
+ <.input + type="text" + field={f[:password]} + label="New password" + required="true" + /> +
+
+ <.input + type="text" + field={f[:password_confirmation]} + label="Confirm new password" + required="true" + />
+
+ <.input + type="text" + field={f[:current_password]} + label="Current password" + required="true" + /> +
+
+ + <.live_component module={LightningWeb.ProfileLive.MfaComponent} diff --git a/test/lightning_web/live/profile_live_test.exs b/test/lightning_web/live/profile_live_test.exs index 46fe372401..d574f97d2d 100644 --- a/test/lightning_web/live/profile_live_test.exs +++ b/test/lightning_web/live/profile_live_test.exs @@ -53,37 +53,83 @@ defmodule LightningWeb.ProfileLiveTest do assert html =~ "Change password" end + test "update basic information", %{conn: conn, user: user} do + {:ok, profile_live, _html} = + live(conn, ~p"/profile") + + assert profile_live + |> has_element?("h2", "#{user.first_name} #{user.last_name}") + + assert profile_live + |> form("#basic-info-form", user: %{first_name: ""}) + |> render_change() =~ "This field can't be blank" + + assert profile_live + |> form("#basic-info-form", user: %{last_name: ""}) + |> render_change() =~ "This field can't be blank" + + assert profile_live + |> form("#basic-info-form", + user: %{ + first_name: "Kylian", + last_name: "", + contact_preference: "critical" + } + ) + |> render_submit() =~ "This field can't be blank" + + {:ok, profile_live, html} = + profile_live + |> form("#basic-info-form", + user: %{ + first_name: "Kylian", + last_name: "Mbappe", + contact_preference: "critical" + } + ) + |> render_submit() + |> follow_redirect(conn, ~p"/profile") + + assert html =~ "User information updated successfully" + + refute profile_live + |> has_element?("h2", "#{user.first_name} #{user.last_name}") + + assert profile_live + |> has_element?("h2", "Kylian Mbappe") + end + test "save password", %{conn: conn} do {:ok, profile_live, _html} = live(conn, Routes.profile_edit_path(conn, :edit)) assert profile_live - |> form("#password_form", user: @invalid_empty_password_attrs) + |> form("#password-form", user: @invalid_empty_password_attrs) |> render_change() =~ "can't be blank" assert profile_live - |> form("#password_form", user: @invalid_dont_match_password_attrs) + |> form("#password-form", user: @invalid_dont_match_password_attrs) |> render_change() =~ "Your passwords do not match" assert profile_live - |> form("#password_form", user: @invalid_too_short_password_attrs) + |> form("#password-form", user: @invalid_too_short_password_attrs) |> render_change() =~ "Password minimum length is 8 characters" assert profile_live - |> form("#password_form", user: @invalid_empty_password_attrs) + |> form("#password-form", user: @invalid_empty_password_attrs) |> render_submit() =~ "can't be blank" assert profile_live - |> form("#password_form", user: @invalid_dont_match_password_attrs) + |> form("#password-form", user: @invalid_dont_match_password_attrs) |> render_submit() =~ "Your passwords do not match" assert profile_live - |> form("#password_form", user: @invalid_too_short_password_attrs) + |> form("#password-form", user: @invalid_too_short_password_attrs) |> render_submit() =~ "Password minimum length is 8 characters" {:ok, conn} = profile_live - |> form("#password_form", user: @update_password_attrs) + |> form("#password-form", user: @update_password_attrs) |> render_submit() |> follow_redirect(conn) @@ -102,7 +148,7 @@ defmodule LightningWeb.ProfileLiveTest do live(conn, Routes.profile_edit_path(conn, :edit)) assert profile_live - |> form("#email_form", user: %{current_password: "invalid"}) + |> form("#email-form", user: %{current_password: "invalid"}) |> render_change() =~ "Your passwords do not match." end @@ -111,7 +157,7 @@ defmodule LightningWeb.ProfileLiveTest do live(conn, Routes.profile_edit_path(conn, :edit)) assert profile_live - |> form("#email_form", user: %{email: user.email}) + |> form("#email-form", user: %{email: user.email}) |> render_change() =~ "Please change your email" end @@ -120,15 +166,15 @@ defmodule LightningWeb.ProfileLiveTest do live(conn, Routes.profile_edit_path(conn, :edit)) assert profile_live - |> form("#email_form", user: @invalid_email_update_attrs) + |> form("#email-form", user: @invalid_email_update_attrs) |> render_change() =~ "can't be blank" assert profile_live - |> form("#email_form", user: %{email: "oops email"}) + |> form("#email-form", user: %{email: "oops email"}) |> render_change() =~ "Email address not valid." assert profile_live - |> form("#email_form", user: @update_email_attrs) + |> form("#email-form", user: @update_email_attrs) |> render_submit() =~ "Sending confirmation email..." end