From 702e36c88fb21e3e863e44a600c158f69c4941e0 Mon Sep 17 00:00:00 2001 From: Daniel Flanagan Date: Sun, 21 Jan 2024 23:48:10 -0600 Subject: [PATCH] CRUD --- lib/homeman/accounts.ex | 296 ++++++++++++++++++ lib/homeman/accounts/task.ex | 22 ++ lib/homeman/accounts/todo.ex | 22 ++ lib/homeman/accounts/user.ex | 21 ++ .../live/task_live/form_component.ex | 93 ++++++ lib/homeman_web/live/task_live/index.ex | 47 +++ .../live/task_live/index.html.heex | 44 +++ lib/homeman_web/live/task_live/show.ex | 21 ++ lib/homeman_web/live/task_live/show.html.heex | 29 ++ .../live/todo_live/form_component.ex | 92 ++++++ lib/homeman_web/live/todo_live/index.ex | 47 +++ .../live/todo_live/index.html.heex | 43 +++ lib/homeman_web/live/todo_live/show.ex | 21 ++ lib/homeman_web/live/todo_live/show.html.heex | 28 ++ .../live/user_live/form_component.ex | 92 ++++++ lib/homeman_web/live/user_live/index.ex | 47 +++ .../live/user_live/index.html.heex | 43 +++ lib/homeman_web/live/user_live/show.ex | 21 ++ lib/homeman_web/live/user_live/show.html.heex | 28 ++ lib/homeman_web/router.ex | 23 ++ .../20240122053553_create_users.exs | 14 + .../20240122054006_create_todos.exs | 17 + .../20240122054053_create_tasks.exs | 15 + priv/repo/seeds.exs | 6 + test/homeman/accounts_test.exs | 181 +++++++++++ test/homeman_web/live/task_live_test.exs | 113 +++++++ test/homeman_web/live/todo_live_test.exs | 113 +++++++ test/homeman_web/live/user_live_test.exs | 113 +++++++ test/support/fixtures/accounts_fixtures.ex | 55 ++++ 29 files changed, 1707 insertions(+) create mode 100644 lib/homeman/accounts.ex create mode 100644 lib/homeman/accounts/task.ex create mode 100644 lib/homeman/accounts/todo.ex create mode 100644 lib/homeman/accounts/user.ex create mode 100644 lib/homeman_web/live/task_live/form_component.ex create mode 100644 lib/homeman_web/live/task_live/index.ex create mode 100644 lib/homeman_web/live/task_live/index.html.heex create mode 100644 lib/homeman_web/live/task_live/show.ex create mode 100644 lib/homeman_web/live/task_live/show.html.heex create mode 100644 lib/homeman_web/live/todo_live/form_component.ex create mode 100644 lib/homeman_web/live/todo_live/index.ex create mode 100644 lib/homeman_web/live/todo_live/index.html.heex create mode 100644 lib/homeman_web/live/todo_live/show.ex create mode 100644 lib/homeman_web/live/todo_live/show.html.heex create mode 100644 lib/homeman_web/live/user_live/form_component.ex create mode 100644 lib/homeman_web/live/user_live/index.ex create mode 100644 lib/homeman_web/live/user_live/index.html.heex create mode 100644 lib/homeman_web/live/user_live/show.ex create mode 100644 lib/homeman_web/live/user_live/show.html.heex create mode 100644 priv/repo/migrations/20240122053553_create_users.exs create mode 100644 priv/repo/migrations/20240122054006_create_todos.exs create mode 100644 priv/repo/migrations/20240122054053_create_tasks.exs create mode 100644 test/homeman/accounts_test.exs create mode 100644 test/homeman_web/live/task_live_test.exs create mode 100644 test/homeman_web/live/todo_live_test.exs create mode 100644 test/homeman_web/live/user_live_test.exs create mode 100644 test/support/fixtures/accounts_fixtures.ex diff --git a/lib/homeman/accounts.ex b/lib/homeman/accounts.ex new file mode 100644 index 0000000..e6ae6a2 --- /dev/null +++ b/lib/homeman/accounts.ex @@ -0,0 +1,296 @@ +defmodule Homeman.Accounts do + @moduledoc """ + The Accounts context. + """ + + import Ecto.Query, warn: false + alias Homeman.Repo + + alias Homeman.Accounts.User + + @doc """ + Returns the list of users. + + ## Examples + + iex> list_users() + [%User{}, ...] + + """ + def list_users do + Repo.all(User) + end + + @doc """ + Gets a single user. + + Raises `Ecto.NoResultsError` if the User does not exist. + + ## Examples + + iex> get_user!(123) + %User{} + + iex> get_user!(456) + ** (Ecto.NoResultsError) + + """ + def get_user!(id), do: Repo.get!(User, id) + + @doc """ + Creates a user. + + ## Examples + + iex> create_user(%{field: value}) + {:ok, %User{}} + + iex> create_user(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_user(attrs \\ %{}) do + %User{} + |> User.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a user. + + ## Examples + + iex> update_user(user, %{field: new_value}) + {:ok, %User{}} + + iex> update_user(user, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_user(%User{} = user, attrs) do + user + |> User.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a user. + + ## Examples + + iex> delete_user(user) + {:ok, %User{}} + + iex> delete_user(user) + {:error, %Ecto.Changeset{}} + + """ + def delete_user(%User{} = user) do + Repo.delete(user) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking user changes. + + ## Examples + + iex> change_user(user) + %Ecto.Changeset{data: %User{}} + + """ + def change_user(%User{} = user, attrs \\ %{}) do + User.changeset(user, attrs) + end + + alias Homeman.Accounts.Todo + + @doc """ + Returns the list of todos. + + ## Examples + + iex> list_todos() + [%Todo{}, ...] + + """ + def list_todos do + Repo.all(Todo) + end + + @doc """ + Gets a single todo. + + Raises `Ecto.NoResultsError` if the Todo does not exist. + + ## Examples + + iex> get_todo!(123) + %Todo{} + + iex> get_todo!(456) + ** (Ecto.NoResultsError) + + """ + def get_todo!(id), do: Repo.get!(Todo, id) + + @doc """ + Creates a todo. + + ## Examples + + iex> create_todo(%{field: value}) + {:ok, %Todo{}} + + iex> create_todo(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_todo(attrs \\ %{}) do + %Todo{} + |> Todo.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a todo. + + ## Examples + + iex> update_todo(todo, %{field: new_value}) + {:ok, %Todo{}} + + iex> update_todo(todo, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_todo(%Todo{} = todo, attrs) do + todo + |> Todo.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a todo. + + ## Examples + + iex> delete_todo(todo) + {:ok, %Todo{}} + + iex> delete_todo(todo) + {:error, %Ecto.Changeset{}} + + """ + def delete_todo(%Todo{} = todo) do + Repo.delete(todo) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking todo changes. + + ## Examples + + iex> change_todo(todo) + %Ecto.Changeset{data: %Todo{}} + + """ + def change_todo(%Todo{} = todo, attrs \\ %{}) do + Todo.changeset(todo, attrs) + end + + alias Homeman.Accounts.Task + + @doc """ + Returns the list of tasks. + + ## Examples + + iex> list_tasks() + [%Task{}, ...] + + """ + def list_tasks do + Repo.all(Task) + end + + @doc """ + Gets a single task. + + Raises `Ecto.NoResultsError` if the Task does not exist. + + ## Examples + + iex> get_task!(123) + %Task{} + + iex> get_task!(456) + ** (Ecto.NoResultsError) + + """ + def get_task!(id), do: Repo.get!(Task, id) + + @doc """ + Creates a task. + + ## Examples + + iex> create_task(%{field: value}) + {:ok, %Task{}} + + iex> create_task(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_task(attrs \\ %{}) do + %Task{} + |> Task.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a task. + + ## Examples + + iex> update_task(task, %{field: new_value}) + {:ok, %Task{}} + + iex> update_task(task, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_task(%Task{} = task, attrs) do + task + |> Task.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a task. + + ## Examples + + iex> delete_task(task) + {:ok, %Task{}} + + iex> delete_task(task) + {:error, %Ecto.Changeset{}} + + """ + def delete_task(%Task{} = task) do + Repo.delete(task) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking task changes. + + ## Examples + + iex> change_task(task) + %Ecto.Changeset{data: %Task{}} + + """ + def change_task(%Task{} = task, attrs \\ %{}) do + Task.changeset(task, attrs) + end +end diff --git a/lib/homeman/accounts/task.ex b/lib/homeman/accounts/task.ex new file mode 100644 index 0000000..043dc3b --- /dev/null +++ b/lib/homeman/accounts/task.ex @@ -0,0 +1,22 @@ +defmodule Homeman.Accounts.Task do + use Ecto.Schema + import Ecto.Changeset + + @primary_key {:id, :binary_id, autogenerate: true} + @foreign_key_type :binary_id + schema "tasks" do + field :description, :string + field :phase, :string + field :emoji, :string + field :completed_at, :naive_datetime + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(task, attrs) do + task + |> cast(attrs, [:description, :emoji, :completed_at, :phase]) + |> validate_required([:description, :emoji, :phase]) + end +end diff --git a/lib/homeman/accounts/todo.ex b/lib/homeman/accounts/todo.ex new file mode 100644 index 0000000..6e737d3 --- /dev/null +++ b/lib/homeman/accounts/todo.ex @@ -0,0 +1,22 @@ +defmodule Homeman.Accounts.Todo do + use Ecto.Schema + import Ecto.Changeset + + @primary_key {:id, :binary_id, autogenerate: true} + @foreign_key_type :binary_id + schema "todos" do + field :description, :string + field :emoji, :string + field :completed_at, :naive_datetime + field :assignee_user_id, :binary_id + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(todo, attrs) do + todo + |> cast(attrs, [:description, :emoji, :completed_at]) + |> validate_required([:description, :emoji, :completed_at]) + end +end diff --git a/lib/homeman/accounts/user.ex b/lib/homeman/accounts/user.ex new file mode 100644 index 0000000..6f038ef --- /dev/null +++ b/lib/homeman/accounts/user.ex @@ -0,0 +1,21 @@ +defmodule Homeman.Accounts.User do + use Ecto.Schema + import Ecto.Changeset + + @primary_key {:id, :binary_id, autogenerate: true} + @foreign_key_type :binary_id + schema "users" do + field :name, :string + field :color, :string + field :avatar_url, :string + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(user, attrs) do + user + |> cast(attrs, [:name, :avatar_url, :color]) + |> validate_required([:name, :avatar_url, :color]) + end +end diff --git a/lib/homeman_web/live/task_live/form_component.ex b/lib/homeman_web/live/task_live/form_component.ex new file mode 100644 index 0000000..bd407b6 --- /dev/null +++ b/lib/homeman_web/live/task_live/form_component.ex @@ -0,0 +1,93 @@ +defmodule HomemanWeb.TaskLive.FormComponent do + use HomemanWeb, :live_component + + alias Homeman.Accounts + + @impl true + def render(assigns) do + ~H""" +
+ <.header> + <%= @title %> + <:subtitle>Use this form to manage task records in your database. + + + <.simple_form + for={@form} + id="task-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + > + <.input field={@form[:description]} type="text" label="Description" /> + <.input field={@form[:emoji]} type="text" label="Emoji" /> + <.input field={@form[:completed_at]} type="datetime-local" label="Completed at" /> + <.input field={@form[:phase]} type="text" label="Phase" /> + <:actions> + <.button phx-disable-with="Saving...">Save Task + + +
+ """ + end + + @impl true + def update(%{task: task} = assigns, socket) do + changeset = Accounts.change_task(task) + + {:ok, + socket + |> assign(assigns) + |> assign_form(changeset)} + end + + @impl true + def handle_event("validate", %{"task" => task_params}, socket) do + changeset = + socket.assigns.task + |> Accounts.change_task(task_params) + |> Map.put(:action, :validate) + + {:noreply, assign_form(socket, changeset)} + end + + def handle_event("save", %{"task" => task_params}, socket) do + save_task(socket, socket.assigns.action, task_params) + end + + defp save_task(socket, :edit, task_params) do + case Accounts.update_task(socket.assigns.task, task_params) do + {:ok, task} -> + notify_parent({:saved, task}) + + {:noreply, + socket + |> put_flash(:info, "Task updated successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign_form(socket, changeset)} + end + end + + defp save_task(socket, :new, task_params) do + case Accounts.create_task(task_params) do + {:ok, task} -> + notify_parent({:saved, task}) + + {:noreply, + socket + |> put_flash(:info, "Task created successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign_form(socket, changeset)} + end + end + + defp assign_form(socket, %Ecto.Changeset{} = changeset) do + assign(socket, :form, to_form(changeset)) + end + + defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) +end diff --git a/lib/homeman_web/live/task_live/index.ex b/lib/homeman_web/live/task_live/index.ex new file mode 100644 index 0000000..1fb7908 --- /dev/null +++ b/lib/homeman_web/live/task_live/index.ex @@ -0,0 +1,47 @@ +defmodule HomemanWeb.TaskLive.Index do + use HomemanWeb, :live_view + + alias Homeman.Accounts + alias Homeman.Accounts.Task + + @impl true + def mount(_params, _session, socket) do + {:ok, stream(socket, :tasks, Accounts.list_tasks())} + end + + @impl true + def handle_params(params, _url, socket) do + {:noreply, apply_action(socket, socket.assigns.live_action, params)} + end + + defp apply_action(socket, :edit, %{"id" => id}) do + socket + |> assign(:page_title, "Edit Task") + |> assign(:task, Accounts.get_task!(id)) + end + + defp apply_action(socket, :new, _params) do + socket + |> assign(:page_title, "New Task") + |> assign(:task, %Task{}) + end + + defp apply_action(socket, :index, _params) do + socket + |> assign(:page_title, "Listing Tasks") + |> assign(:task, nil) + end + + @impl true + def handle_info({HomemanWeb.TaskLive.FormComponent, {:saved, task}}, socket) do + {:noreply, stream_insert(socket, :tasks, task)} + end + + @impl true + def handle_event("delete", %{"id" => id}, socket) do + task = Accounts.get_task!(id) + {:ok, _} = Accounts.delete_task(task) + + {:noreply, stream_delete(socket, :tasks, task)} + end +end diff --git a/lib/homeman_web/live/task_live/index.html.heex b/lib/homeman_web/live/task_live/index.html.heex new file mode 100644 index 0000000..3634cf0 --- /dev/null +++ b/lib/homeman_web/live/task_live/index.html.heex @@ -0,0 +1,44 @@ +<.header> + Listing Tasks + <:actions> + <.link patch={~p"/tasks/new"}> + <.button>New Task + + + + +<.table + id="tasks" + rows={@streams.tasks} + row_click={fn {_id, task} -> JS.navigate(~p"/tasks/#{task}") end} +> + <:col :let={{_id, task}} label="Description"><%= task.description %> + <:col :let={{_id, task}} label="Emoji"><%= task.emoji %> + <:col :let={{_id, task}} label="Completed at"><%= task.completed_at %> + <:col :let={{_id, task}} label="Phase"><%= task.phase %> + <:action :let={{_id, task}}> +
+ <.link navigate={~p"/tasks/#{task}"}>Show +
+ <.link patch={~p"/tasks/#{task}/edit"}>Edit + + <:action :let={{id, task}}> + <.link + phx-click={JS.push("delete", value: %{id: task.id}) |> hide("##{id}")} + data-confirm="Are you sure?" + > + Delete + + + + +<.modal :if={@live_action in [:new, :edit]} id="task-modal" show on_cancel={JS.patch(~p"/tasks")}> + <.live_component + module={HomemanWeb.TaskLive.FormComponent} + id={@task.id || :new} + title={@page_title} + action={@live_action} + task={@task} + patch={~p"/tasks"} + /> + diff --git a/lib/homeman_web/live/task_live/show.ex b/lib/homeman_web/live/task_live/show.ex new file mode 100644 index 0000000..7dda5f9 --- /dev/null +++ b/lib/homeman_web/live/task_live/show.ex @@ -0,0 +1,21 @@ +defmodule HomemanWeb.TaskLive.Show do + use HomemanWeb, :live_view + + alias Homeman.Accounts + + @impl true + def mount(_params, _session, socket) do + {:ok, socket} + end + + @impl true + def handle_params(%{"id" => id}, _, socket) do + {:noreply, + socket + |> assign(:page_title, page_title(socket.assigns.live_action)) + |> assign(:task, Accounts.get_task!(id))} + end + + defp page_title(:show), do: "Show Task" + defp page_title(:edit), do: "Edit Task" +end diff --git a/lib/homeman_web/live/task_live/show.html.heex b/lib/homeman_web/live/task_live/show.html.heex new file mode 100644 index 0000000..68cafb7 --- /dev/null +++ b/lib/homeman_web/live/task_live/show.html.heex @@ -0,0 +1,29 @@ +<.header> + Task <%= @task.id %> + <:subtitle>This is a task record from your database. + <:actions> + <.link patch={~p"/tasks/#{@task}/show/edit"} phx-click={JS.push_focus()}> + <.button>Edit task + + + + +<.list> + <:item title="Description"><%= @task.description %> + <:item title="Emoji"><%= @task.emoji %> + <:item title="Completed at"><%= @task.completed_at %> + <:item title="Phase"><%= @task.phase %> + + +<.back navigate={~p"/tasks"}>Back to tasks + +<.modal :if={@live_action == :edit} id="task-modal" show on_cancel={JS.patch(~p"/tasks/#{@task}")}> + <.live_component + module={HomemanWeb.TaskLive.FormComponent} + id={@task.id} + title={@page_title} + action={@live_action} + task={@task} + patch={~p"/tasks/#{@task}"} + /> + diff --git a/lib/homeman_web/live/todo_live/form_component.ex b/lib/homeman_web/live/todo_live/form_component.ex new file mode 100644 index 0000000..84560b7 --- /dev/null +++ b/lib/homeman_web/live/todo_live/form_component.ex @@ -0,0 +1,92 @@ +defmodule HomemanWeb.TodoLive.FormComponent do + use HomemanWeb, :live_component + + alias Homeman.Accounts + + @impl true + def render(assigns) do + ~H""" +
+ <.header> + <%= @title %> + <:subtitle>Use this form to manage todo records in your database. + + + <.simple_form + for={@form} + id="todo-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + > + <.input field={@form[:description]} type="text" label="Description" /> + <.input field={@form[:emoji]} type="text" label="Emoji" /> + <.input field={@form[:completed_at]} type="datetime-local" label="Completed at" /> + <:actions> + <.button phx-disable-with="Saving...">Save Todo + + +
+ """ + end + + @impl true + def update(%{todo: todo} = assigns, socket) do + changeset = Accounts.change_todo(todo) + + {:ok, + socket + |> assign(assigns) + |> assign_form(changeset)} + end + + @impl true + def handle_event("validate", %{"todo" => todo_params}, socket) do + changeset = + socket.assigns.todo + |> Accounts.change_todo(todo_params) + |> Map.put(:action, :validate) + + {:noreply, assign_form(socket, changeset)} + end + + def handle_event("save", %{"todo" => todo_params}, socket) do + save_todo(socket, socket.assigns.action, todo_params) + end + + defp save_todo(socket, :edit, todo_params) do + case Accounts.update_todo(socket.assigns.todo, todo_params) do + {:ok, todo} -> + notify_parent({:saved, todo}) + + {:noreply, + socket + |> put_flash(:info, "Todo updated successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign_form(socket, changeset)} + end + end + + defp save_todo(socket, :new, todo_params) do + case Accounts.create_todo(todo_params) do + {:ok, todo} -> + notify_parent({:saved, todo}) + + {:noreply, + socket + |> put_flash(:info, "Todo created successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign_form(socket, changeset)} + end + end + + defp assign_form(socket, %Ecto.Changeset{} = changeset) do + assign(socket, :form, to_form(changeset)) + end + + defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) +end diff --git a/lib/homeman_web/live/todo_live/index.ex b/lib/homeman_web/live/todo_live/index.ex new file mode 100644 index 0000000..78d664d --- /dev/null +++ b/lib/homeman_web/live/todo_live/index.ex @@ -0,0 +1,47 @@ +defmodule HomemanWeb.TodoLive.Index do + use HomemanWeb, :live_view + + alias Homeman.Accounts + alias Homeman.Accounts.Todo + + @impl true + def mount(_params, _session, socket) do + {:ok, stream(socket, :todos, Accounts.list_todos())} + end + + @impl true + def handle_params(params, _url, socket) do + {:noreply, apply_action(socket, socket.assigns.live_action, params)} + end + + defp apply_action(socket, :edit, %{"id" => id}) do + socket + |> assign(:page_title, "Edit Todo") + |> assign(:todo, Accounts.get_todo!(id)) + end + + defp apply_action(socket, :new, _params) do + socket + |> assign(:page_title, "New Todo") + |> assign(:todo, %Todo{}) + end + + defp apply_action(socket, :index, _params) do + socket + |> assign(:page_title, "Listing Todos") + |> assign(:todo, nil) + end + + @impl true + def handle_info({HomemanWeb.TodoLive.FormComponent, {:saved, todo}}, socket) do + {:noreply, stream_insert(socket, :todos, todo)} + end + + @impl true + def handle_event("delete", %{"id" => id}, socket) do + todo = Accounts.get_todo!(id) + {:ok, _} = Accounts.delete_todo(todo) + + {:noreply, stream_delete(socket, :todos, todo)} + end +end diff --git a/lib/homeman_web/live/todo_live/index.html.heex b/lib/homeman_web/live/todo_live/index.html.heex new file mode 100644 index 0000000..fcbefc2 --- /dev/null +++ b/lib/homeman_web/live/todo_live/index.html.heex @@ -0,0 +1,43 @@ +<.header> + Listing Todos + <:actions> + <.link patch={~p"/todos/new"}> + <.button>New Todo + + + + +<.table + id="todos" + rows={@streams.todos} + row_click={fn {_id, todo} -> JS.navigate(~p"/todos/#{todo}") end} +> + <:col :let={{_id, todo}} label="Description"><%= todo.description %> + <:col :let={{_id, todo}} label="Emoji"><%= todo.emoji %> + <:col :let={{_id, todo}} label="Completed at"><%= todo.completed_at %> + <:action :let={{_id, todo}}> +
+ <.link navigate={~p"/todos/#{todo}"}>Show +
+ <.link patch={~p"/todos/#{todo}/edit"}>Edit + + <:action :let={{id, todo}}> + <.link + phx-click={JS.push("delete", value: %{id: todo.id}) |> hide("##{id}")} + data-confirm="Are you sure?" + > + Delete + + + + +<.modal :if={@live_action in [:new, :edit]} id="todo-modal" show on_cancel={JS.patch(~p"/todos")}> + <.live_component + module={HomemanWeb.TodoLive.FormComponent} + id={@todo.id || :new} + title={@page_title} + action={@live_action} + todo={@todo} + patch={~p"/todos"} + /> + diff --git a/lib/homeman_web/live/todo_live/show.ex b/lib/homeman_web/live/todo_live/show.ex new file mode 100644 index 0000000..1543ea8 --- /dev/null +++ b/lib/homeman_web/live/todo_live/show.ex @@ -0,0 +1,21 @@ +defmodule HomemanWeb.TodoLive.Show do + use HomemanWeb, :live_view + + alias Homeman.Accounts + + @impl true + def mount(_params, _session, socket) do + {:ok, socket} + end + + @impl true + def handle_params(%{"id" => id}, _, socket) do + {:noreply, + socket + |> assign(:page_title, page_title(socket.assigns.live_action)) + |> assign(:todo, Accounts.get_todo!(id))} + end + + defp page_title(:show), do: "Show Todo" + defp page_title(:edit), do: "Edit Todo" +end diff --git a/lib/homeman_web/live/todo_live/show.html.heex b/lib/homeman_web/live/todo_live/show.html.heex new file mode 100644 index 0000000..0724fd3 --- /dev/null +++ b/lib/homeman_web/live/todo_live/show.html.heex @@ -0,0 +1,28 @@ +<.header> + Todo <%= @todo.id %> + <:subtitle>This is a todo record from your database. + <:actions> + <.link patch={~p"/todos/#{@todo}/show/edit"} phx-click={JS.push_focus()}> + <.button>Edit todo + + + + +<.list> + <:item title="Description"><%= @todo.description %> + <:item title="Emoji"><%= @todo.emoji %> + <:item title="Completed at"><%= @todo.completed_at %> + + +<.back navigate={~p"/todos"}>Back to todos + +<.modal :if={@live_action == :edit} id="todo-modal" show on_cancel={JS.patch(~p"/todos/#{@todo}")}> + <.live_component + module={HomemanWeb.TodoLive.FormComponent} + id={@todo.id} + title={@page_title} + action={@live_action} + todo={@todo} + patch={~p"/todos/#{@todo}"} + /> + diff --git a/lib/homeman_web/live/user_live/form_component.ex b/lib/homeman_web/live/user_live/form_component.ex new file mode 100644 index 0000000..1f57b5a --- /dev/null +++ b/lib/homeman_web/live/user_live/form_component.ex @@ -0,0 +1,92 @@ +defmodule HomemanWeb.UserLive.FormComponent do + use HomemanWeb, :live_component + + alias Homeman.Accounts + + @impl true + def render(assigns) do + ~H""" +
+ <.header> + <%= @title %> + <:subtitle>Use this form to manage user records in your database. + + + <.simple_form + for={@form} + id="user-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + > + <.input field={@form[:name]} type="text" label="Name" /> + <.input field={@form[:avatar_url]} type="text" label="Avatar url" /> + <.input field={@form[:color]} type="text" label="Color" /> + <:actions> + <.button phx-disable-with="Saving...">Save User + + +
+ """ + end + + @impl true + def update(%{user: user} = assigns, socket) do + changeset = Accounts.change_user(user) + + {:ok, + socket + |> assign(assigns) + |> assign_form(changeset)} + end + + @impl true + def handle_event("validate", %{"user" => user_params}, socket) do + changeset = + socket.assigns.user + |> Accounts.change_user(user_params) + |> Map.put(:action, :validate) + + {:noreply, assign_form(socket, changeset)} + end + + def handle_event("save", %{"user" => user_params}, socket) do + save_user(socket, socket.assigns.action, user_params) + end + + defp save_user(socket, :edit, user_params) do + case Accounts.update_user(socket.assigns.user, user_params) do + {:ok, user} -> + notify_parent({:saved, user}) + + {:noreply, + socket + |> put_flash(:info, "User updated successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign_form(socket, changeset)} + end + end + + defp save_user(socket, :new, user_params) do + case Accounts.create_user(user_params) do + {:ok, user} -> + notify_parent({:saved, user}) + + {:noreply, + socket + |> put_flash(:info, "User created successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign_form(socket, changeset)} + end + end + + defp assign_form(socket, %Ecto.Changeset{} = changeset) do + assign(socket, :form, to_form(changeset)) + end + + defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) +end diff --git a/lib/homeman_web/live/user_live/index.ex b/lib/homeman_web/live/user_live/index.ex new file mode 100644 index 0000000..2057dd0 --- /dev/null +++ b/lib/homeman_web/live/user_live/index.ex @@ -0,0 +1,47 @@ +defmodule HomemanWeb.UserLive.Index do + use HomemanWeb, :live_view + + alias Homeman.Accounts + alias Homeman.Accounts.User + + @impl true + def mount(_params, _session, socket) do + {:ok, stream(socket, :users, Accounts.list_users())} + end + + @impl true + def handle_params(params, _url, socket) do + {:noreply, apply_action(socket, socket.assigns.live_action, params)} + end + + defp apply_action(socket, :edit, %{"id" => id}) do + socket + |> assign(:page_title, "Edit User") + |> assign(:user, Accounts.get_user!(id)) + end + + defp apply_action(socket, :new, _params) do + socket + |> assign(:page_title, "New User") + |> assign(:user, %User{}) + end + + defp apply_action(socket, :index, _params) do + socket + |> assign(:page_title, "Listing Users") + |> assign(:user, nil) + end + + @impl true + def handle_info({HomemanWeb.UserLive.FormComponent, {:saved, user}}, socket) do + {:noreply, stream_insert(socket, :users, user)} + end + + @impl true + def handle_event("delete", %{"id" => id}, socket) do + user = Accounts.get_user!(id) + {:ok, _} = Accounts.delete_user(user) + + {:noreply, stream_delete(socket, :users, user)} + end +end diff --git a/lib/homeman_web/live/user_live/index.html.heex b/lib/homeman_web/live/user_live/index.html.heex new file mode 100644 index 0000000..e98a2d4 --- /dev/null +++ b/lib/homeman_web/live/user_live/index.html.heex @@ -0,0 +1,43 @@ +<.header> + Listing Users + <:actions> + <.link patch={~p"/users/new"}> + <.button>New User + + + + +<.table + id="users" + rows={@streams.users} + row_click={fn {_id, user} -> JS.navigate(~p"/users/#{user}") end} +> + <:col :let={{_id, user}} label="Name"><%= user.name %> + <:col :let={{_id, user}} label="Avatar url"><%= user.avatar_url %> + <:col :let={{_id, user}} label="Color"><%= user.color %> + <:action :let={{_id, user}}> +
+ <.link navigate={~p"/users/#{user}"}>Show +
+ <.link patch={~p"/users/#{user}/edit"}>Edit + + <:action :let={{id, user}}> + <.link + phx-click={JS.push("delete", value: %{id: user.id}) |> hide("##{id}")} + data-confirm="Are you sure?" + > + Delete + + + + +<.modal :if={@live_action in [:new, :edit]} id="user-modal" show on_cancel={JS.patch(~p"/users")}> + <.live_component + module={HomemanWeb.UserLive.FormComponent} + id={@user.id || :new} + title={@page_title} + action={@live_action} + user={@user} + patch={~p"/users"} + /> + diff --git a/lib/homeman_web/live/user_live/show.ex b/lib/homeman_web/live/user_live/show.ex new file mode 100644 index 0000000..f99759f --- /dev/null +++ b/lib/homeman_web/live/user_live/show.ex @@ -0,0 +1,21 @@ +defmodule HomemanWeb.UserLive.Show do + use HomemanWeb, :live_view + + alias Homeman.Accounts + + @impl true + def mount(_params, _session, socket) do + {:ok, socket} + end + + @impl true + def handle_params(%{"id" => id}, _, socket) do + {:noreply, + socket + |> assign(:page_title, page_title(socket.assigns.live_action)) + |> assign(:user, Accounts.get_user!(id))} + end + + defp page_title(:show), do: "Show User" + defp page_title(:edit), do: "Edit User" +end diff --git a/lib/homeman_web/live/user_live/show.html.heex b/lib/homeman_web/live/user_live/show.html.heex new file mode 100644 index 0000000..9e5d0a7 --- /dev/null +++ b/lib/homeman_web/live/user_live/show.html.heex @@ -0,0 +1,28 @@ +<.header> + User <%= @user.id %> + <:subtitle>This is a user record from your database. + <:actions> + <.link patch={~p"/users/#{@user}/show/edit"} phx-click={JS.push_focus()}> + <.button>Edit user + + + + +<.list> + <:item title="Name"><%= @user.name %> + <:item title="Avatar url"><%= @user.avatar_url %> + <:item title="Color"><%= @user.color %> + + +<.back navigate={~p"/users"}>Back to users + +<.modal :if={@live_action == :edit} id="user-modal" show on_cancel={JS.patch(~p"/users/#{@user}")}> + <.live_component + module={HomemanWeb.UserLive.FormComponent} + id={@user.id} + title={@page_title} + action={@live_action} + user={@user} + patch={~p"/users/#{@user}"} + /> + diff --git a/lib/homeman_web/router.ex b/lib/homeman_web/router.ex index 23d5651..07d7ce8 100644 --- a/lib/homeman_web/router.ex +++ b/lib/homeman_web/router.ex @@ -18,6 +18,29 @@ defmodule HomemanWeb.Router do pipe_through :browser get "/", PageController, :home + + live "/tasks", TaskLive.Index, :index + live "/tasks/new", TaskLive.Index, :new + live "/tasks/:id/edit", TaskLive.Index, :edit + + live "/tasks/:id", TaskLive.Show, :show + live "/tasks/:id/show/edit", TaskLive.Show, :edit + + + live "/todos", TodoLive.Index, :index + live "/todos/new", TodoLive.Index, :new + live "/todos/:id/edit", TodoLive.Index, :edit + + live "/todos/:id", TodoLive.Show, :show + live "/todos/:id/show/edit", TodoLive.Show, :edit + + + live "/users", UserLive.Index, :index + live "/users/new", UserLive.Index, :new + live "/users/:id/edit", UserLive.Index, :edit + + live "/users/:id", UserLive.Show, :show + live "/users/:id/show/edit", UserLive.Show, :edit end # Other scopes may use custom stacks. diff --git a/priv/repo/migrations/20240122053553_create_users.exs b/priv/repo/migrations/20240122053553_create_users.exs new file mode 100644 index 0000000..72a145a --- /dev/null +++ b/priv/repo/migrations/20240122053553_create_users.exs @@ -0,0 +1,14 @@ +defmodule Homeman.Repo.Migrations.CreateUsers do + use Ecto.Migration + + def change do + create table(:users, primary_key: false) do + add :id, :binary_id, primary_key: true + add :name, :string + add :avatar_url, :string, null: true + add :color, :string + + timestamps(type: :utc_datetime) + end + end +end diff --git a/priv/repo/migrations/20240122054006_create_todos.exs b/priv/repo/migrations/20240122054006_create_todos.exs new file mode 100644 index 0000000..0b7a11a --- /dev/null +++ b/priv/repo/migrations/20240122054006_create_todos.exs @@ -0,0 +1,17 @@ +defmodule Homeman.Repo.Migrations.CreateTodos do + use Ecto.Migration + + def change do + create table(:todos, primary_key: false) do + add :id, :binary_id, primary_key: true + add :description, :string + add :emoji, :string, null: true + add :completed_at, :naive_datetime, null: true + add :assignee_user_id, references(:users, on_delete: :nothing, type: :binary_id), null: true + + timestamps(type: :utc_datetime) + end + + create index(:todos, [:assignee_user_id]) + end +end diff --git a/priv/repo/migrations/20240122054053_create_tasks.exs b/priv/repo/migrations/20240122054053_create_tasks.exs new file mode 100644 index 0000000..fe4b28c --- /dev/null +++ b/priv/repo/migrations/20240122054053_create_tasks.exs @@ -0,0 +1,15 @@ +defmodule Homeman.Repo.Migrations.CreateTasks do + use Ecto.Migration + + def change do + create table(:tasks, primary_key: false) do + add :id, :binary_id, primary_key: true + add :description, :string + add :emoji, :string, null: true + add :completed_at, :naive_datetime, null: true + add :phase, :string + + timestamps(type: :utc_datetime) + end + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index babb004..6d6611e 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -9,3 +9,9 @@ # # We recommend using the bang functions (`insert!`, `update!` # and so on) as they will fail if something goes wrong. + +%{ + name: "Daddy", + color: "#00aaff", + avatar_url: "/uploads/space.png", +} diff --git a/test/homeman/accounts_test.exs b/test/homeman/accounts_test.exs new file mode 100644 index 0000000..a497bde --- /dev/null +++ b/test/homeman/accounts_test.exs @@ -0,0 +1,181 @@ +defmodule Homeman.AccountsTest do + use Homeman.DataCase + + alias Homeman.Accounts + + describe "users" do + alias Homeman.Accounts.User + + import Homeman.AccountsFixtures + + @invalid_attrs %{name: nil, color: nil, avatar_url: nil} + + test "list_users/0 returns all users" do + user = user_fixture() + assert Accounts.list_users() == [user] + end + + test "get_user!/1 returns the user with given id" do + user = user_fixture() + assert Accounts.get_user!(user.id) == user + end + + test "create_user/1 with valid data creates a user" do + valid_attrs = %{name: "some name", color: "some color", avatar_url: "some avatar_url"} + + assert {:ok, %User{} = user} = Accounts.create_user(valid_attrs) + assert user.name == "some name" + assert user.color == "some color" + assert user.avatar_url == "some avatar_url" + end + + test "create_user/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Accounts.create_user(@invalid_attrs) + end + + test "update_user/2 with valid data updates the user" do + user = user_fixture() + update_attrs = %{name: "some updated name", color: "some updated color", avatar_url: "some updated avatar_url"} + + assert {:ok, %User{} = user} = Accounts.update_user(user, update_attrs) + assert user.name == "some updated name" + assert user.color == "some updated color" + assert user.avatar_url == "some updated avatar_url" + end + + test "update_user/2 with invalid data returns error changeset" do + user = user_fixture() + assert {:error, %Ecto.Changeset{}} = Accounts.update_user(user, @invalid_attrs) + assert user == Accounts.get_user!(user.id) + end + + test "delete_user/1 deletes the user" do + user = user_fixture() + assert {:ok, %User{}} = Accounts.delete_user(user) + assert_raise Ecto.NoResultsError, fn -> Accounts.get_user!(user.id) end + end + + test "change_user/1 returns a user changeset" do + user = user_fixture() + assert %Ecto.Changeset{} = Accounts.change_user(user) + end + end + + describe "todos" do + alias Homeman.Accounts.Todo + + import Homeman.AccountsFixtures + + @invalid_attrs %{description: nil, emoji: nil, completed_at: nil} + + test "list_todos/0 returns all todos" do + todo = todo_fixture() + assert Accounts.list_todos() == [todo] + end + + test "get_todo!/1 returns the todo with given id" do + todo = todo_fixture() + assert Accounts.get_todo!(todo.id) == todo + end + + test "create_todo/1 with valid data creates a todo" do + valid_attrs = %{description: "some description", emoji: "some emoji", completed_at: ~N[2024-01-21 05:40:00]} + + assert {:ok, %Todo{} = todo} = Accounts.create_todo(valid_attrs) + assert todo.description == "some description" + assert todo.emoji == "some emoji" + assert todo.completed_at == ~N[2024-01-21 05:40:00] + end + + test "create_todo/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Accounts.create_todo(@invalid_attrs) + end + + test "update_todo/2 with valid data updates the todo" do + todo = todo_fixture() + update_attrs = %{description: "some updated description", emoji: "some updated emoji", completed_at: ~N[2024-01-22 05:40:00]} + + assert {:ok, %Todo{} = todo} = Accounts.update_todo(todo, update_attrs) + assert todo.description == "some updated description" + assert todo.emoji == "some updated emoji" + assert todo.completed_at == ~N[2024-01-22 05:40:00] + end + + test "update_todo/2 with invalid data returns error changeset" do + todo = todo_fixture() + assert {:error, %Ecto.Changeset{}} = Accounts.update_todo(todo, @invalid_attrs) + assert todo == Accounts.get_todo!(todo.id) + end + + test "delete_todo/1 deletes the todo" do + todo = todo_fixture() + assert {:ok, %Todo{}} = Accounts.delete_todo(todo) + assert_raise Ecto.NoResultsError, fn -> Accounts.get_todo!(todo.id) end + end + + test "change_todo/1 returns a todo changeset" do + todo = todo_fixture() + assert %Ecto.Changeset{} = Accounts.change_todo(todo) + end + end + + describe "tasks" do + alias Homeman.Accounts.Task + + import Homeman.AccountsFixtures + + @invalid_attrs %{description: nil, phase: nil, emoji: nil, completed_at: nil} + + test "list_tasks/0 returns all tasks" do + task = task_fixture() + assert Accounts.list_tasks() == [task] + end + + test "get_task!/1 returns the task with given id" do + task = task_fixture() + assert Accounts.get_task!(task.id) == task + end + + test "create_task/1 with valid data creates a task" do + valid_attrs = %{description: "some description", phase: "some phase", emoji: "some emoji", completed_at: ~N[2024-01-21 05:40:00]} + + assert {:ok, %Task{} = task} = Accounts.create_task(valid_attrs) + assert task.description == "some description" + assert task.phase == "some phase" + assert task.emoji == "some emoji" + assert task.completed_at == ~N[2024-01-21 05:40:00] + end + + test "create_task/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Accounts.create_task(@invalid_attrs) + end + + test "update_task/2 with valid data updates the task" do + task = task_fixture() + update_attrs = %{description: "some updated description", phase: "some updated phase", emoji: "some updated emoji", completed_at: ~N[2024-01-22 05:40:00]} + + assert {:ok, %Task{} = task} = Accounts.update_task(task, update_attrs) + assert task.description == "some updated description" + assert task.phase == "some updated phase" + assert task.emoji == "some updated emoji" + assert task.completed_at == ~N[2024-01-22 05:40:00] + end + + test "update_task/2 with invalid data returns error changeset" do + task = task_fixture() + assert {:error, %Ecto.Changeset{}} = Accounts.update_task(task, @invalid_attrs) + assert task == Accounts.get_task!(task.id) + end + + test "delete_task/1 deletes the task" do + task = task_fixture() + assert {:ok, %Task{}} = Accounts.delete_task(task) + assert_raise Ecto.NoResultsError, fn -> Accounts.get_task!(task.id) end + end + + test "change_task/1 returns a task changeset" do + task = task_fixture() + assert %Ecto.Changeset{} = Accounts.change_task(task) + end + end +end diff --git a/test/homeman_web/live/task_live_test.exs b/test/homeman_web/live/task_live_test.exs new file mode 100644 index 0000000..34ad5e7 --- /dev/null +++ b/test/homeman_web/live/task_live_test.exs @@ -0,0 +1,113 @@ +defmodule HomemanWeb.TaskLiveTest do + use HomemanWeb.ConnCase + + import Phoenix.LiveViewTest + import Homeman.AccountsFixtures + + @create_attrs %{description: "some description", phase: "some phase", emoji: "some emoji", completed_at: "2024-01-21T05:40:00"} + @update_attrs %{description: "some updated description", phase: "some updated phase", emoji: "some updated emoji", completed_at: "2024-01-22T05:40:00"} + @invalid_attrs %{description: nil, phase: nil, emoji: nil, completed_at: nil} + + defp create_task(_) do + task = task_fixture() + %{task: task} + end + + describe "Index" do + setup [:create_task] + + test "lists all tasks", %{conn: conn, task: task} do + {:ok, _index_live, html} = live(conn, ~p"/tasks") + + assert html =~ "Listing Tasks" + assert html =~ task.description + end + + test "saves new task", %{conn: conn} do + {:ok, index_live, _html} = live(conn, ~p"/tasks") + + assert index_live |> element("a", "New Task") |> render_click() =~ + "New Task" + + assert_patch(index_live, ~p"/tasks/new") + + assert index_live + |> form("#task-form", task: @invalid_attrs) + |> render_change() =~ "can't be blank" + + assert index_live + |> form("#task-form", task: @create_attrs) + |> render_submit() + + assert_patch(index_live, ~p"/tasks") + + html = render(index_live) + assert html =~ "Task created successfully" + assert html =~ "some description" + end + + test "updates task in listing", %{conn: conn, task: task} do + {:ok, index_live, _html} = live(conn, ~p"/tasks") + + assert index_live |> element("#tasks-#{task.id} a", "Edit") |> render_click() =~ + "Edit Task" + + assert_patch(index_live, ~p"/tasks/#{task}/edit") + + assert index_live + |> form("#task-form", task: @invalid_attrs) + |> render_change() =~ "can't be blank" + + assert index_live + |> form("#task-form", task: @update_attrs) + |> render_submit() + + assert_patch(index_live, ~p"/tasks") + + html = render(index_live) + assert html =~ "Task updated successfully" + assert html =~ "some updated description" + end + + test "deletes task in listing", %{conn: conn, task: task} do + {:ok, index_live, _html} = live(conn, ~p"/tasks") + + assert index_live |> element("#tasks-#{task.id} a", "Delete") |> render_click() + refute has_element?(index_live, "#tasks-#{task.id}") + end + end + + describe "Show" do + setup [:create_task] + + test "displays task", %{conn: conn, task: task} do + {:ok, _show_live, html} = live(conn, ~p"/tasks/#{task}") + + assert html =~ "Show Task" + assert html =~ task.description + end + + test "updates task within modal", %{conn: conn, task: task} do + {:ok, show_live, _html} = live(conn, ~p"/tasks/#{task}") + + assert show_live |> element("a", "Edit") |> render_click() =~ + "Edit Task" + + assert_patch(show_live, ~p"/tasks/#{task}/show/edit") + + assert show_live + |> form("#task-form", task: @invalid_attrs) + |> render_change() =~ "can't be blank" + + assert show_live + |> form("#task-form", task: @update_attrs) + |> render_submit() + + assert_patch(show_live, ~p"/tasks/#{task}") + + html = render(show_live) + assert html =~ "Task updated successfully" + assert html =~ "some updated description" + end + end +end diff --git a/test/homeman_web/live/todo_live_test.exs b/test/homeman_web/live/todo_live_test.exs new file mode 100644 index 0000000..0ce5150 --- /dev/null +++ b/test/homeman_web/live/todo_live_test.exs @@ -0,0 +1,113 @@ +defmodule HomemanWeb.TodoLiveTest do + use HomemanWeb.ConnCase + + import Phoenix.LiveViewTest + import Homeman.AccountsFixtures + + @create_attrs %{description: "some description", emoji: "some emoji", completed_at: "2024-01-21T05:40:00"} + @update_attrs %{description: "some updated description", emoji: "some updated emoji", completed_at: "2024-01-22T05:40:00"} + @invalid_attrs %{description: nil, emoji: nil, completed_at: nil} + + defp create_todo(_) do + todo = todo_fixture() + %{todo: todo} + end + + describe "Index" do + setup [:create_todo] + + test "lists all todos", %{conn: conn, todo: todo} do + {:ok, _index_live, html} = live(conn, ~p"/todos") + + assert html =~ "Listing Todos" + assert html =~ todo.description + end + + test "saves new todo", %{conn: conn} do + {:ok, index_live, _html} = live(conn, ~p"/todos") + + assert index_live |> element("a", "New Todo") |> render_click() =~ + "New Todo" + + assert_patch(index_live, ~p"/todos/new") + + assert index_live + |> form("#todo-form", todo: @invalid_attrs) + |> render_change() =~ "can't be blank" + + assert index_live + |> form("#todo-form", todo: @create_attrs) + |> render_submit() + + assert_patch(index_live, ~p"/todos") + + html = render(index_live) + assert html =~ "Todo created successfully" + assert html =~ "some description" + end + + test "updates todo in listing", %{conn: conn, todo: todo} do + {:ok, index_live, _html} = live(conn, ~p"/todos") + + assert index_live |> element("#todos-#{todo.id} a", "Edit") |> render_click() =~ + "Edit Todo" + + assert_patch(index_live, ~p"/todos/#{todo}/edit") + + assert index_live + |> form("#todo-form", todo: @invalid_attrs) + |> render_change() =~ "can't be blank" + + assert index_live + |> form("#todo-form", todo: @update_attrs) + |> render_submit() + + assert_patch(index_live, ~p"/todos") + + html = render(index_live) + assert html =~ "Todo updated successfully" + assert html =~ "some updated description" + end + + test "deletes todo in listing", %{conn: conn, todo: todo} do + {:ok, index_live, _html} = live(conn, ~p"/todos") + + assert index_live |> element("#todos-#{todo.id} a", "Delete") |> render_click() + refute has_element?(index_live, "#todos-#{todo.id}") + end + end + + describe "Show" do + setup [:create_todo] + + test "displays todo", %{conn: conn, todo: todo} do + {:ok, _show_live, html} = live(conn, ~p"/todos/#{todo}") + + assert html =~ "Show Todo" + assert html =~ todo.description + end + + test "updates todo within modal", %{conn: conn, todo: todo} do + {:ok, show_live, _html} = live(conn, ~p"/todos/#{todo}") + + assert show_live |> element("a", "Edit") |> render_click() =~ + "Edit Todo" + + assert_patch(show_live, ~p"/todos/#{todo}/show/edit") + + assert show_live + |> form("#todo-form", todo: @invalid_attrs) + |> render_change() =~ "can't be blank" + + assert show_live + |> form("#todo-form", todo: @update_attrs) + |> render_submit() + + assert_patch(show_live, ~p"/todos/#{todo}") + + html = render(show_live) + assert html =~ "Todo updated successfully" + assert html =~ "some updated description" + end + end +end diff --git a/test/homeman_web/live/user_live_test.exs b/test/homeman_web/live/user_live_test.exs new file mode 100644 index 0000000..427c2ac --- /dev/null +++ b/test/homeman_web/live/user_live_test.exs @@ -0,0 +1,113 @@ +defmodule HomemanWeb.UserLiveTest do + use HomemanWeb.ConnCase + + import Phoenix.LiveViewTest + import Homeman.AccountsFixtures + + @create_attrs %{name: "some name", color: "some color", avatar_url: "some avatar_url"} + @update_attrs %{name: "some updated name", color: "some updated color", avatar_url: "some updated avatar_url"} + @invalid_attrs %{name: nil, color: nil, avatar_url: nil} + + defp create_user(_) do + user = user_fixture() + %{user: user} + end + + describe "Index" do + setup [:create_user] + + test "lists all users", %{conn: conn, user: user} do + {:ok, _index_live, html} = live(conn, ~p"/users") + + assert html =~ "Listing Users" + assert html =~ user.name + end + + test "saves new user", %{conn: conn} do + {:ok, index_live, _html} = live(conn, ~p"/users") + + assert index_live |> element("a", "New User") |> render_click() =~ + "New User" + + assert_patch(index_live, ~p"/users/new") + + assert index_live + |> form("#user-form", user: @invalid_attrs) + |> render_change() =~ "can't be blank" + + assert index_live + |> form("#user-form", user: @create_attrs) + |> render_submit() + + assert_patch(index_live, ~p"/users") + + html = render(index_live) + assert html =~ "User created successfully" + assert html =~ "some name" + end + + test "updates user in listing", %{conn: conn, user: user} do + {:ok, index_live, _html} = live(conn, ~p"/users") + + assert index_live |> element("#users-#{user.id} a", "Edit") |> render_click() =~ + "Edit User" + + assert_patch(index_live, ~p"/users/#{user}/edit") + + assert index_live + |> form("#user-form", user: @invalid_attrs) + |> render_change() =~ "can't be blank" + + assert index_live + |> form("#user-form", user: @update_attrs) + |> render_submit() + + assert_patch(index_live, ~p"/users") + + html = render(index_live) + assert html =~ "User updated successfully" + assert html =~ "some updated name" + end + + test "deletes user in listing", %{conn: conn, user: user} do + {:ok, index_live, _html} = live(conn, ~p"/users") + + assert index_live |> element("#users-#{user.id} a", "Delete") |> render_click() + refute has_element?(index_live, "#users-#{user.id}") + end + end + + describe "Show" do + setup [:create_user] + + test "displays user", %{conn: conn, user: user} do + {:ok, _show_live, html} = live(conn, ~p"/users/#{user}") + + assert html =~ "Show User" + assert html =~ user.name + end + + test "updates user within modal", %{conn: conn, user: user} do + {:ok, show_live, _html} = live(conn, ~p"/users/#{user}") + + assert show_live |> element("a", "Edit") |> render_click() =~ + "Edit User" + + assert_patch(show_live, ~p"/users/#{user}/show/edit") + + assert show_live + |> form("#user-form", user: @invalid_attrs) + |> render_change() =~ "can't be blank" + + assert show_live + |> form("#user-form", user: @update_attrs) + |> render_submit() + + assert_patch(show_live, ~p"/users/#{user}") + + html = render(show_live) + assert html =~ "User updated successfully" + assert html =~ "some updated name" + end + end +end diff --git a/test/support/fixtures/accounts_fixtures.ex b/test/support/fixtures/accounts_fixtures.ex new file mode 100644 index 0000000..2d15fb7 --- /dev/null +++ b/test/support/fixtures/accounts_fixtures.ex @@ -0,0 +1,55 @@ +defmodule Homeman.AccountsFixtures do + @moduledoc """ + This module defines test helpers for creating + entities via the `Homeman.Accounts` context. + """ + + @doc """ + Generate a user. + """ + def user_fixture(attrs \\ %{}) do + {:ok, user} = + attrs + |> Enum.into(%{ + avatar_url: "some avatar_url", + color: "some color", + name: "some name" + }) + |> Homeman.Accounts.create_user() + + user + end + + @doc """ + Generate a todo. + """ + def todo_fixture(attrs \\ %{}) do + {:ok, todo} = + attrs + |> Enum.into(%{ + completed_at: ~N[2024-01-21 05:40:00], + description: "some description", + emoji: "some emoji" + }) + |> Homeman.Accounts.create_todo() + + todo + end + + @doc """ + Generate a task. + """ + def task_fixture(attrs \\ %{}) do + {:ok, task} = + attrs + |> Enum.into(%{ + completed_at: ~N[2024-01-21 05:40:00], + description: "some description", + emoji: "some emoji", + phase: "some phase" + }) + |> Homeman.Accounts.create_task() + + task + end +end