NEW: Tasks LiveComponent (#27)
This commit is contained in:
parent
a91f1924b2
commit
3d70659861
8 changed files with 364 additions and 7 deletions
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
235
lib/planner_web/live/tasks_components.ex
Normal file
235
lib/planner_web/live/tasks_components.ex
Normal 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
|
97
lib/planner_web/live/tasks_live.ex
Normal file
97
lib/planner_web/live/tasks_live.ex
Normal 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
|
|
@ -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)
|
||||||
|
|
|
@ -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">
|
||||||
|
|
4
mix.exs
4
mix.exs
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue