NEW: Tasks LiveComponent (#27)

This commit is contained in:
Matthew Ryan Dillon 2020-07-08 20:52:20 -07:00 committed by GitHub
parent a91f1924b2
commit 3d70659861
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 364 additions and 7 deletions

View file

@ -4,6 +4,10 @@
cursor: pointer; cursor: pointer;
} }
.tasks {
margin-left: 0em !important;
}
.tasks li { .tasks li {
@extend .py-1, .px-5; @extend .py-1, .px-5;

View file

@ -1,5 +1,6 @@
defmodule Planner.Tasks do defmodule Planner.Tasks do
import Ecto.Query import Ecto.Query
alias Ecto.UUID
alias Planner.Repo alias Planner.Repo
alias Planner.Tasks.Task alias Planner.Tasks.Task
@ -59,6 +60,8 @@ defmodule Planner.Tasks do
def get_task!(id), do: Repo.get!(Task, id) def get_task!(id), do: Repo.get!(Task, id)
def exists?(id), do: Repo.exists?(from(t in Task, where: t.id == ^id))
def delete_task_by_id!(id) do def delete_task_by_id!(id) do
get_task!(id) get_task!(id)
|> Repo.delete() |> Repo.delete()
@ -69,4 +72,18 @@ defmodule Planner.Tasks do
|> Task.finish_task() |> Task.finish_task()
|> Repo.update() |> Repo.update()
end end
def verify_task_id_from_url(task_id) do
task_id =
case UUID.dump(task_id) do
# don't actually want the dumped UUID, so discard
{:ok, _} -> task_id
:error -> :error
end
case task_id do
:error -> :error
_ -> exists?(task_id)
end
end
end end

View file

@ -38,9 +38,6 @@ defmodule PlannerWeb do
import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1] import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1]
import Phoenix.LiveView.Helpers import Phoenix.LiveView.Helpers
# Internal View Utils
import PlannerWeb.Util
# Include shared imports and aliases for views # Include shared imports and aliases for views
unquote(view_helpers()) unquote(view_helpers())
end end
@ -91,6 +88,9 @@ defmodule PlannerWeb do
import PlannerWeb.ErrorHelpers import PlannerWeb.ErrorHelpers
import PlannerWeb.Gettext import PlannerWeb.Gettext
alias PlannerWeb.Router.Helpers, as: Routes alias PlannerWeb.Router.Helpers, as: Routes
# Internal View Utils
import PlannerWeb.Util
end end
end end

View file

@ -0,0 +1,235 @@
defmodule TasksComponent do
use PlannerWeb, :live_component
alias Planner.Tasks
alias Planner.Tasks.Task
def update(%{:changeset => changeset, :id => _id}, socket) do
{:ok, assign(socket, :changeset, changeset)}
end
def update(assigns, socket) do
socket =
socket
|> assign(assigns)
|> assign(:changeset, Tasks.change_task(%Task{}))
{:ok, socket}
end
def render(assigns) do
~L"""
<div class="content">
<%= f = form_for(@changeset, "#", [phx_submit: "new-task"]) %>
<div class="field">
<div class="control">
<%= text_input(f,
:value,
placeholder: "add new task",
class: "input", autocomplete: "off"
)%>
</div>
<%= error_tag(f, :value) %>
</div>
</form>
<ul class="tasks">
<%= for task <- @tasks do %>
<%= live_component(@socket,
TaskComponent,
id: "task:#{task.id}",
task: task,
live_action: @live_action,
is_active: @active_task == task.id,
route_func_2: @route_func_2,
route_func_3: @route_func_3
)%>
<% end %>
</ul>
</div>
"""
end
end
defmodule TaskComponent do
use Phoenix.LiveComponent
import PlannerWeb.Util
def render(assigns) do
~L"""
<li>
<div>
<div class="is-pulled-left">
<button
type="button"
role="checkbox"
class="doit"
phx-click="finish-task"
phx-value-task-id="<%= @task.id %>">
</button>
</div>
<div class="ml-5-5">
<%= if(@is_active) do %>
<%= case @live_action do %>
<% :show -> %>
<%= live_component(@socket,
TaskDetailsComponent,
id: "task_details:#{@task.id}",
task: @task,
route_func_2: @route_func_2,
route_func_3: @route_func_3
)%>
<% :edit -> %>
<%= live_component(@socket,
TaskEditComponent,
id: "task_edit:#{@task.id}",
task: @task
)%>
<% end %>
<% else %>
<%= live_patch(to: @route_func_3.(@socket, :show, @task.id),
style: "display: block;"
) do %>
<div class="value ">
<%= md_to_html(@task.value) %>
</div>
<% end %>
<%= if(not is_nil(@task.due_at)) do %>
<div class="tags mb-0">
<span class="tag">
due: <%= @task.due_at %>
</span>
</div>
<% end %>
<% end %>
</div>
</div>
</li>
"""
end
end
defmodule TaskDetailsComponent do
use PlannerWeb, :live_component
def render(assigns) do
~L"""
<div class="box">
<%= live_patch("",
to: @route_func_2.(@socket, :index),
class: "delete is-pulled-right"
) %>
<%= if(not is_nil(@task.due_at) or is_nil(@task.filed_at)) do %>
<div class="tags">
<%= if(not is_nil(@task.due_at)) do %>
<span class="tag is-warning">
due: <%= @task.due_at %>
</span><% end %>
<%= if(is_nil(@task.filed_at)) do %>
<span class="tag is-danger">
unfiled
</span>
<% end %>
</div>
<% end %>
<div class="mb-5">
<%= md_to_html(@task.value) %>
</div>
<div class="tags">
<span class="tag is-light">updated: <%= @task.updated_at %></span>
<span class="tag is-light">created: <%= @task.inserted_at %></span>
</div>
<div class="buttons has-addons">
<%= live_patch("edit",
to: @route_func_3.(@socket, :edit, @task.id),
class: "button is-dark is-small"
) %>
<a
class="button is-dark is-small"
phx-click="delete-task"
phx-value-task-id="<%= @task.id %>">
delete
</a>
</div>
</div>
"""
end
end
defmodule TaskEditComponent do
use PlannerWeb, :live_component
alias Planner.Tasks
def update(%{:changeset => changeset, :id => _id}, socket) do
{:ok, assign(socket, :changeset, changeset)}
end
def update(assigns, socket) do
socket =
socket
|> assign(assigns)
|> assign(:changeset, Tasks.change_task(assigns.task))
{:ok, socket}
end
def render(assigns) do
~L"""
<div class="box">
<%= f = form_for(@changeset, "#", [phx_submit: "save-task"]) %>
<%= hidden_input(f, :id) %>
<div class="field">
<div class="control">
<%= textarea(f,
:value,
required: true,
class: "textarea",
placeholder: "task",
autocomplete: "off"
) %>
</div>
<%= error_tag(f, :value) %>
</div>
<div class="field">
<%= label(f, :due_at, class: "label") do %>
due (YYYY-MM-DD HH:MM:SS)
<% end %>
<div class="control">
<%= text_input(f,
:due_at,
class: "input",
placeholder: "YYYY-MM-DD HH:MM:SS",
autocomplete: "off"
) %>
</div>
<%= error_tag(f, :due_at) %>
</div>
<div class="field">
<%= label(f, :finished_at, class: "label") do %>
<%= if(is_nil(@task.finished_at)) do %>
<%= checkbox(f, :finished_at) %>
<% else %>
<%= checkbox(f, :finished_at, checked_value: @task.finished_at) %>
<% end %>
finished
<% end %>
<%= error_tag(f, :finished_at) %>
</div>
<div class="control">
<%= submit("save", class: "button is-dark is-small") %>
</div>
</form>
</div>
"""
end
end

View file

@ -0,0 +1,97 @@
defmodule PlannerWeb.TasksLive do
use PlannerWeb, :live_view
alias Planner.Tasks
def mount(_params, _session, socket) do
socket =
socket
|> assign(:tasks, Tasks.list_unfinished_tasks())
|> assign(:active_task, nil)
{:ok, socket}
end
def handle_params(%{"id" => task_id}, _, socket) do
case Tasks.verify_task_id_from_url(task_id) do
true -> {:noreply, assign(socket, :active_task, task_id)}
_ -> {:noreply, push_patch(socket, to: Routes.tasks_path(socket, :index))}
end
end
def handle_params(_, _, socket) do
{:noreply, assign(socket, :active_task, nil)}
end
def render(assigns) do
~L"""
<div phx-window-keydown="keydown" phx-key="Escape">
<%= live_component(@socket,
TasksComponent,
id: :all_unfinished_tasks,
live_action: @live_action,
tasks: @tasks,
active_task: @active_task,
route_func_2: &Routes.tasks_path/2,
route_func_3: &Routes.tasks_path/3
)%>
</div>
"""
end
def handle_event("keydown", _params, socket) do
case socket.assigns.live_action do
:index -> {:noreply, socket}
_ -> {:noreply, push_patch(socket, to: Routes.tasks_path(socket, :index))}
end
end
def handle_event("save-task", %{"task" => task_params}, socket) do
task = Tasks.get_task!(task_params["id"])
case Tasks.update_task(task, task_params) do
{:ok, task} ->
# I suspect splicing in the updated task isn't much faster than just refreshing the whole list
socket =
socket
|> refresh_tasks_and_flash_msg("task \"#{task.value}\" updated")
{:noreply, push_patch(socket, to: Routes.tasks_path(socket, :show, task.id))}
{:error, changeset} ->
send_update(TaskEditComponent, id: "task_edit:#{task.id}", changeset: changeset)
{:noreply, socket}
end
end
def handle_event("finish-task", %{"task-id" => task_id}, socket) do
{_, task} = Tasks.finish_task_by_id!(task_id)
{:noreply, refresh_tasks_and_flash_msg(socket, "task \"#{task.value}\" completed")}
end
def handle_event("delete-task", %{"task-id" => task_id}, socket) do
{_, task} = Tasks.delete_task_by_id!(task_id)
{:noreply, refresh_tasks_and_flash_msg(socket, "task \"#{task.value}\" deleted")}
end
def handle_event("new-task", %{"task" => task_params}, socket) do
case Tasks.add_task(task_params) do
{:ok, task} ->
socket =
socket
|> refresh_tasks_and_flash_msg("task \"#{task.value}\" created")
{:noreply, push_patch(socket, to: Routes.tasks_path(socket, :show, task.id))}
{:error, %Ecto.Changeset{} = changeset} ->
send_update(TasksComponent, id: :all_unfinished_tasks, changeset: changeset)
{:noreply, socket}
end
end
defp refresh_tasks_and_flash_msg(socket, msg) do
socket
|> assign(:tasks, Tasks.list_unfinished_tasks())
|> put_flash(:info, msg)
end
end

View file

@ -54,7 +54,11 @@ defmodule PlannerWeb.Router do
live("/", LandingLive, :index) live("/", LandingLive, :index)
resources("/tasks", TaskController) live("/tasks", TasksLive, :index)
live("/tasks/:id", TasksLive, :show)
live("/tasks/:id/edit", TasksLive, :edit)
resources("/tasks-old", TaskController)
get("/users/settings", UserSettingsController, :edit) get("/users/settings", UserSettingsController, :edit)
put("/users/settings/update_password", UserSettingsController, :update_password) put("/users/settings/update_password", UserSettingsController, :update_password)

View file

@ -22,7 +22,7 @@
<div id="nvbr" class="navbar-menu"> <div id="nvbr" class="navbar-menu">
<div class="navbar-start"> <div class="navbar-start">
<%= link "tasks", to: Routes.task_path(@conn, :index), class: "navbar-item" %> <%= link "tasks", to: Routes.tasks_path(@conn, :index), class: "navbar-item" %>
</div> </div>
<div class="navbar-end"> <div class="navbar-end">

View file

@ -34,7 +34,7 @@ defmodule Planner.MixProject do
defp deps do defp deps do
[ [
{:bcrypt_elixir, "~> 2.0"}, {:bcrypt_elixir, "~> 2.0"},
{:phoenix, "~> 1.5.1"}, {:phoenix, "~> 1.5.3"},
{:phoenix_ecto, "~> 4.1"}, {:phoenix_ecto, "~> 4.1"},
{:earmark, "~> 1.4.5"}, {:earmark, "~> 1.4.5"},
{:ecto_sql, "~> 3.4"}, {:ecto_sql, "~> 3.4"},
@ -48,7 +48,7 @@ defmodule Planner.MixProject do
{:gettext, "~> 0.11"}, {:gettext, "~> 0.11"},
{:jason, "~> 1.0"}, {:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"}, {:plug_cowboy, "~> 2.0"},
{:phoenix_live_view, "~> 0.13.2"}, {:phoenix_live_view, "~> 0.13.3"},
{:floki, ">= 0.0.0", only: :test} {:floki, ">= 0.0.0", only: :test}
] ]
end end