changes to accommodate a delete user modal

This commit is contained in:
Adam Piontek 2021-03-05 15:32:01 -05:00
parent 9651887f34
commit 0039146cd4
13 changed files with 302 additions and 209 deletions

View file

@ -17,7 +17,7 @@ defmodule Bones73k.Accounts.User do
@derive {Inspect, except: [:password]} @derive {Inspect, except: [:password]}
@primary_key {:id, :binary_id, autogenerate: true} @primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id # @foreign_key_type :binary_id
schema "users" do schema "users" do
field :email, :string field :email, :string
field :password, :string, virtual: true field :password, :string, virtual: true

View file

@ -8,7 +8,7 @@ defmodule Bones73k.Properties.Property do
field :description, :string field :description, :string
field :name, :string field :name, :string
field :price, :decimal field :price, :decimal
belongs_to :user, Bones73k.Accounts.User field :user_id, :binary_id
timestamps() timestamps()
end end

View file

@ -11,7 +11,7 @@ defmodule Bones73kWeb.ModalComponent do
phx-target="#<%= @id %>" phx-target="#<%= @id %>"
phx-page-loading> phx-page-loading>
<div class="modal-dialog"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@ -19,9 +19,7 @@ defmodule Bones73kWeb.ModalComponent do
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body">
<%= live_component @socket, @component, @opts %> <%= live_component @socket, @component, @opts %>
</div>
</div> </div>
</div> </div>

View file

@ -21,10 +21,16 @@ defmodule Bones73kWeb.PropertyLive.FormComponent do
{:noreply, assign(socket, :changeset, changeset)} {:noreply, assign(socket, :changeset, changeset)}
end end
@impl true
def handle_event("save", %{"property" => property_params}, socket) do def handle_event("save", %{"property" => property_params}, socket) do
save_property(socket, socket.assigns.action, property_params) save_property(socket, socket.assigns.action, property_params)
end end
@impl true
def handle_event("cancel", _, socket) do
{:noreply, push_event(socket, "modal-please-hide", %{})}
end
defp save_property(socket, :edit, property_params) do defp save_property(socket, :edit, property_params) do
case Properties.update_property(socket.assigns.property, property_params) do case Properties.update_property(socket.assigns.property, property_params) do
{:ok, _property} -> {:ok, _property} ->

View file

@ -5,6 +5,7 @@
phx_submit: "save" phx_submit: "save"
], fn f -> %> ], fn f -> %>
<div class="modal-body">
<div class="mb-3" phx-feedback-for="<%= input_id(f, :name)%>"> <div class="mb-3" phx-feedback-for="<%= input_id(f, :name)%>">
<%= label f, :name, class: "form-label" %> <%= label f, :name, class: "form-label" %>
@ -40,6 +41,17 @@
</div> </div>
</div> </div>
<%= submit "Save", phx_disable_with: "Saving...", class: "btn btn-primary" %> </div>
<div class="modal-footer">
<%= link "Cancel", to: "#", class: "btn btn-secondary me-2", phx_click: "cancel", phx_target: @myself %>
<%= submit "Save",
class: "btn btn-primary ",
disabled: !@changeset.valid?,
aria_disabled: !@changeset.valid? && "true" || false,
phx_disable_with: "Saving..."
%>
</div>
<% end %> <% end %>

View file

@ -0,0 +1,44 @@
defmodule Bones73kWeb.UserManagement.DeleteComponent do
use Bones73kWeb, :live_component
alias Bones73k.Accounts
@impl true
def update(assigns, socket) do
socket
|> assign(assigns)
|> live_okreply()
end
@impl true
def handle_event("cancel", _, socket) do
{:noreply, push_event(socket, "modal-please-hide", %{})}
end
@impl true
def handle_event("confirm", %{"id" => id, "email" => email}, socket) do
id
|> Accounts.get_user()
|> Accounts.delete_user()
|> case do
{:ok, _} ->
flash = {:info, "User deleted successfully: \"#{email}\""}
send(self(), {:put_flash_message, flash})
socket
|> push_event("modal-please-hide", %{})
|> live_noreply()
{:error, _} ->
flash =
{:error,
"Some error trying to delete user \"#{email}\". Possibly already deleted? Reloading list..."}
send(self(), {:put_flash_message, flash})
socket
|> push_event("modal-please-hide", %{})
|> live_noreply()
end
end
end

View file

@ -0,0 +1,16 @@
<div class="modal-body">
Are you sure you want to delete "<%= @delete_user.email %>"?
</div>
<div class="modal-footer">
<%= link "Cancel", to: "#", class: "btn btn-secondary me-2", phx_click: "cancel", phx_target: @myself %>
<%= link "Confirm Delete", to: "#",
class: "btn btn-danger",
phx_click: "confirm",
phx_target: @myself,
phx_value_id: @delete_user.id,
phx_value_email: @delete_user.email %>
</div>

View file

@ -10,7 +10,6 @@ defmodule Bones73kWeb.UserManagement.FormComponent do
socket socket
|> assign(assigns) |> assign(assigns)
|> init_changeset(assigns) |> init_changeset(assigns)
|> assign(:role_id, 1)
|> live_okreply() |> live_okreply()
end end
@ -40,7 +39,7 @@ defmodule Bones73kWeb.UserManagement.FormComponent do
&Routes.user_confirmation_url(socket, :confirm, &1) &Routes.user_confirmation_url(socket, :confirm, &1)
) )
flash = {:success, "User created successfully: #{user.email}"} flash = {:info, "User created successfully: #{user.email}"}
send(self(), {:put_flash_message, flash}) send(self(), {:put_flash_message, flash})
socket socket
@ -81,6 +80,11 @@ defmodule Bones73kWeb.UserManagement.FormComponent do
save_user(socket, user_params) save_user(socket, user_params)
end end
@impl true
def handle_event("cancel", _, socket) do
{:noreply, push_event(socket, "modal-please-hide", %{})}
end
def role_description(role) when is_atom(role) do def role_description(role) when is_atom(role) do
Keyword.get(User.roles(), role) Keyword.get(User.roles(), role)
end end

View file

@ -4,6 +4,8 @@
phx_submit: "save" phx_submit: "save"
], fn f -> %> ], fn f -> %>
<div class="modal-body">
<div class="mb-3" phx-feedback-for="<%= input_id(f, :email)%>"> <div class="mb-3" phx-feedback-for="<%= input_id(f, :email)%>">
<%= label f, :email, class: "form-label" %> <%= label f, :email, class: "form-label" %>
<div class="input-group has-validation"> <div class="input-group has-validation">
@ -23,7 +25,6 @@
</div> </div>
</div> </div>
<%= if Roles.can?(@current_user, %User{}, :edit_role) do %> <%= if Roles.can?(@current_user, %User{}, :edit_role) do %>
<%= label f, :role, class: "form-label" %> <%= label f, :role, class: "form-label" %>
<div class="input-group mb-3"> <div class="input-group mb-3">
@ -39,8 +40,7 @@
<%= hidden_input f, :role, value: input_value(f, :role) %> <%= hidden_input f, :role, value: input_value(f, :role) %>
<% end %> <% end %>
<div phx-feedback-for="<%= input_id(f, :password) %>">
<div class="mb-3" phx-feedback-for="<%= input_id(f, :password) %>">
<%= label f, :password, class: "form-label" %> <%= label f, :password, class: "form-label" %>
<div class="input-group has-validation"> <div class="input-group has-validation">
<span class="input-group-text"> <span class="input-group-text">
@ -56,12 +56,17 @@
</div> </div>
</div> </div>
</div>
<div class="modal-footer">
<%= link "Cancel", to: "#", class: "btn btn-secondary", phx_click: "cancel", phx_target: @myself %>
<%= submit "Save", <%= submit "Save",
class: "btn btn-primary", class: "btn btn-primary ",
disabled: !@changeset.valid?, disabled: !@changeset.valid?,
aria_disabled: !@changeset.valid? && "true" || false, aria_disabled: !@changeset.valid? && "true" || false,
phx_disable_with: "Saving..." phx_disable_with: "Saving..."
%> %>
</div>
<% end %> <% end %>

View file

@ -14,6 +14,7 @@ defmodule Bones73kWeb.UserManagementLive.Index do
def mount(_params, session, socket) do def mount(_params, session, socket) do
socket socket
|> assign_defaults(session) |> assign_defaults(session)
|> assign(:page, nil)
|> live_okreply() |> live_okreply()
end end
@ -27,7 +28,9 @@ defmodule Bones73kWeb.UserManagementLive.Index do
socket socket
|> assign(:query, query_map(params)) |> assign(:query, query_map(params))
|> assign_modal_return_to() |> assign_modal_return_to()
|> page_query() |> assign(:delete_user, nil)
|> assign(:page, nil)
|> request_page_query()
|> apply_action(socket.assigns.live_action, params) |> apply_action(socket.assigns.live_action, params)
|> live_noreply() |> live_noreply()
else else
@ -38,6 +41,11 @@ defmodule Bones73kWeb.UserManagementLive.Index do
end end
end end
defp request_page_query(%{assigns: %{query: query}} = socket) do
send(self(), {:do_page_query, query})
socket
end
defp apply_action(socket, :index, _params) do defp apply_action(socket, :index, _params) do
socket socket
|> assign(:page_title, "Listing Users") |> assign(:page_title, "Listing Users")
@ -78,56 +86,30 @@ defmodule Bones73kWeb.UserManagementLive.Index do
} }
end end
defp page_query(%{assigns: %{query: query}} = socket) do defp page_query(query) do
result_page =
from(u in User) from(u in User)
|> or_where([u], ilike(u.email, ^"%#{query.filter}%")) |> or_where([u], ilike(u.email, ^"%#{query.filter}%"))
# |> or_where([u], ilike(u.singer_name, ^"%#{query.filter}%"))
|> order_by([u], [ |> order_by([u], [
{^String.to_existing_atom(query.sort_order), ^String.to_existing_atom(query.sort_by)} {^String.to_existing_atom(query.sort_order), ^String.to_existing_atom(query.sort_by)}
]) ])
|> Repo.paginate(page: query.page_number, page_size: query.page_size) |> Repo.paginate(page: query.page_number, page_size: query.page_size)
socket
|> assign(:page, result_page)
|> assign(:table_loading, false)
end end
@impl true @impl true
def handle_event("delete", %{"id" => id, "email" => email}, socket) do def handle_event("delete-modal", %{"id" => id}, socket) do
id {:noreply, assign(socket, :delete_user, Accounts.get_user(id))}
|> Accounts.get_user()
|> Accounts.delete_user()
|> case do
{:ok, _} ->
socket
|> put_flash(:success, "User deleted successfully: \"#{email}\"")
|> assign(:table_loading, true)
|> page_query()
|> live_noreply()
{:error, _} ->
socket
|> put_flash(
:error,
"Something went wrong attempting to delete user \"#{email}\". Possibly already deleted? Reloading list..."
)
|> assign(:table_loading, true)
|> page_query()
|> live_noreply()
end
end end
@impl true @impl true
def handle_event("filter-change", params, socket) do def handle_event("filter-change", params, socket) do
send(self(), {:query_update, Map.put(params, "page_number", "1")}) send(self(), {:query_update, Map.put(params, "page_number", "1")})
{:noreply, assign(socket, :table_loading, true)} {:noreply, socket}
end end
@impl true @impl true
def handle_event("filter-clear", _params, socket) do def handle_event("filter-clear", _params, socket) do
send(self(), {:query_update, %{"filter" => "", "page_number" => "1"}}) send(self(), {:query_update, %{"filter" => "", "page_number" => "1"}})
{:noreply, assign(socket, :table_loading, true)} {:noreply, socket}
end end
@impl true @impl true
@ -143,32 +125,32 @@ defmodule Bones73kWeb.UserManagementLive.Index do
)) || )) ||
send(self(), {:query_update, Map.put(params, "sort_order", "asc")}) send(self(), {:query_update, Map.put(params, "sort_order", "asc")})
{:noreply, assign(socket, :table_loading, true)} {:noreply, socket}
end end
@impl true @impl true
def handle_event("sort-by-change", %{"sort" => params}, socket) do def handle_event("sort-by-change", %{"sort" => params}, socket) do
send(self(), {:query_update, params}) send(self(), {:query_update, params})
{:noreply, assign(socket, :table_loading, true)} {:noreply, socket}
end end
@impl true @impl true
def handle_event("sort-order-change", _params, socket) do def handle_event("sort-order-change", _params, socket) do
new_sort_order = (socket.assigns.query.sort_order == "asc" && "desc") || "asc" new_sort_order = (socket.assigns.query.sort_order == "asc" && "desc") || "asc"
send(self(), {:query_update, %{"sort_order" => new_sort_order}}) send(self(), {:query_update, %{"sort_order" => new_sort_order}})
{:noreply, assign(socket, :table_loading, true)} {:noreply, socket}
end end
@impl true @impl true
def handle_event("page-change", params, socket) do def handle_event("page-change", params, socket) do
send(self(), {:query_update, params}) send(self(), {:query_update, params})
{:noreply, assign(socket, :table_loading, true)} {:noreply, socket}
end end
@impl true @impl true
def handle_event("page-size-change", params, socket) do def handle_event("page-size-change", params, socket) do
send(self(), {:query_update, Map.put(params, "page_number", "1")}) send(self(), {:query_update, Map.put(params, "page_number", "1")})
{:noreply, assign(socket, :table_loading, true)} {:noreply, socket}
end end
@impl true @impl true
@ -180,6 +162,11 @@ defmodule Bones73kWeb.UserManagementLive.Index do
)} )}
end end
@impl true
def handle_info({:do_page_query, query}, socket) do
{:noreply, assign(socket, :page, page_query(query))}
end
@impl true @impl true
def handle_info({:close_modal, _}, %{assigns: %{modal_return_to: to}} = socket) do def handle_info({:close_modal, _}, %{assigns: %{modal_return_to: to}} = socket) do
socket |> copy_flash() |> push_patch(to: to) |> live_noreply() socket |> copy_flash() |> push_patch(to: to) |> live_noreply()

View file

@ -7,6 +7,14 @@
current_user: @current_user %> current_user: @current_user %>
<% end %> <% end %>
<%= if @delete_user do %>
<%= live_modal @socket, Bones73kWeb.UserManagement.DeleteComponent,
id: @delete_user.id,
title: "Delete User",
delete_user: @delete_user,
current_user: @current_user %>
<% end %>
<h2 class="mb-3"> <h2 class="mb-3">
<%= icon_div @socket, "bi-people", [class: "icon baseline"] %> <%= icon_div @socket, "bi-people", [class: "icon baseline"] %>
@ -14,16 +22,16 @@
</h2> </h2>
<%# filtering and new item creation %> <%# filtering and new item creation %>
<div class="d-flex flex-column flex-sm-row justify-content-between d-flex align-items-start"> <div class="d-flex flex-column flex-sm-row justify-content-between d-flex align-items-start mb-3">
<%= live_patch to: Routes.user_management_index_path(@socket, :new, Enum.into(@query, [])), <%= live_patch to: Routes.user_management_index_path(@socket, :new, Enum.into(@query, [])),
class: "btn btn-primary mb-2" do %> class: "btn btn-primary mb-3 mb-sm-0" do %>
<%= icon_div @socket, "bi-person-plus", [class: "icon baseline me-1"] %> <%= icon_div @socket, "bi-person-plus", [class: "icon baseline me-1"] %>
New User New User
<% end %> <% end %>
<%= form_for :filter, "#", [phx_change: "filter-change"], fn flt -> %> <%= form_for :filter, "#", [phx_change: "filter-change"], fn flt -> %>
<div class="input-group mb-2"> <div class="input-group">
<span class="input-group-text"> <span class="input-group-text">
<%= icon_div @socket, "bi-filter", [class: "icon"] %> <%= icon_div @socket, "bi-filter", [class: "icon"] %>
@ -47,10 +55,11 @@
</div> </div>
<%# main data table %> <%# main data table %>
<table class="table"> <div class="table-responsive">
<table class="table">
<thead> <thead>
<tr class="<%= @table_loading && "loading" || "" %>"> <tr>
<th scope="col" phx-click="sort-change" phx-value-sort_by="email" class="cursor-pointer"> <th scope="col" phx-click="sort-change" phx-value-sort_by="email" class="cursor-pointer">
Email Email
@ -84,6 +93,15 @@
</thead> </thead>
<tbody id="users"> <tbody id="users">
<%= if !@page do %>
<tr>
<td class="text-center" colspan="5">
<div class="spinner-border text-primary my-5" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</td>
</tr>
<% else %>
<%= for user <- @page.entries do %> <%= for user <- @page.entries do %>
<tr id="user-<%= user.id %>"> <tr id="user-<%= user.id %>">
<td class="align-middle"><%= user.email %></td> <td class="align-middle"><%= user.email %></td>
@ -98,38 +116,40 @@
<%= icon_div @socket, "bi-x", [class: "icon baseline fs-4 text-warning"], [role: "img", aria_hidden: false] %> <%= icon_div @socket, "bi-x", [class: "icon baseline fs-4 text-warning"], [role: "img", aria_hidden: false] %>
<% end %> <% end %>
</td> </td>
<td class="align-middle text-end"> <td class="align-middle text-end text-nowrap">
<%= live_patch to: Routes.user_management_index_path(@socket, :edit, user.id, Enum.into(@query, [])), class: "btn btn-outline-primary btn-sm" do %> <%= live_patch to: Routes.user_management_index_path(@socket, :edit, user.id, Enum.into(@query, [])), class: "btn btn-outline-primary btn-sm text-nowrap" do %>
<%= icon_div @socket, "bi-pencil", [class: "icon baseline", style: "margin-right:0.125rem;"] %> <%= icon_div @socket, "bi-pencil", [class: "icon baseline", style: "margin-right:0.125rem;"] %>
Edit Edit
<% end %> <% end %>
<%= if Roles.can?(@current_user, user, :delete) do %> <%= if Roles.can?(@current_user, user, :delete) do %>
<%= link to: "#",
phx_click: "delete", <button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id="<%= user.id %>">
phx_value_id: user.id,
phx_value_email: user.email,
data: [confirm: "Are you sure you want to delete this user? \"#{user.email}\""],
class: "btn btn-outline-danger btn-sm" do %>
<%= icon_div @socket, "bi-trash", [class: "icon baseline", style: "margin-right:0.125rem;"] %> <%= icon_div @socket, "bi-trash", [class: "icon baseline", style: "margin-right:0.125rem;"] %>
Delete Delete
<% end %> </button>
<% end %> <% end %>
</td> </td>
</tr> </tr>
<% end %> <% end %>
<% end %>
</tbody> </tbody>
</table> </table>
</div>
<%# pagination interface %> <%# pagination interface %>
<div class="d-flex justify-content-between d-flex align-items-start"> <%= if @page do %>
<div class="d-flex flex-column flex-sm-row justify-content-between d-flex align-items-start my-3">
<%# <div class="d-flex justify-content-between d-flex align-items-start"> %>
<%# items per page selector %> <%# items per page selector %>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center mb-3 mb-sm-0">
<%= form_for :page_size, "#", [phx_change: "page-size-change"], fn pgsz -> %> <%= form_for :page_size, "#", [phx_change: "page-size-change"], fn pgsz -> %>
<%= select pgsz, :page_size, <%= select pgsz, :page_size,
[10, 15, 20, 30, 50, 100] |> Enum.map(fn n -> {"#{n} per page", n} end), [10, 15, 20, 30, 50, 100] |> Enum.map(fn n -> {"#{n} per page", n} end),
@ -193,3 +213,4 @@
</nav> </nav>
</div> </div>
<% end %>

View file

@ -7,7 +7,7 @@ defmodule Bones73k.Repo.Migrations.CreateProperties do
add(:name, :string) add(:name, :string)
add(:price, :decimal) add(:price, :decimal)
add(:description, :text) add(:description, :text)
add(:user_id, references(:users, type: :binary_id, on_delete: :nothing)) add(:user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false)
timestamps() timestamps()
end end

View file

@ -37,7 +37,7 @@ defmodule Bones73k.AccountsTest do
describe "get_user!/1" do describe "get_user!/1" do
test "raises if id is invalid" do test "raises if id is invalid" do
assert_raise Ecto.NoResultsError, fn -> assert_raise Ecto.NoResultsError, fn ->
Accounts.get_user!(-1) Ecto.UUID.generate() |> Accounts.get_user!()
end end
end end