From 3d706598616b990e1ad6230498444138b7cc10c3 Mon Sep 17 00:00:00 2001 From: Matthew Dillon Date: Wed, 8 Jul 2020 20:52:20 -0700 Subject: [PATCH] NEW: Tasks LiveComponent (#27) --- assets/css/app.scss | 4 + lib/planner/tasks.ex | 17 ++ lib/planner_web.ex | 6 +- lib/planner_web/live/tasks_components.ex | 235 ++++++++++++++++++ lib/planner_web/live/tasks_live.ex | 97 ++++++++ lib/planner_web/router.ex | 6 +- .../templates/layout/root.html.leex | 2 +- mix.exs | 4 +- 8 files changed, 364 insertions(+), 7 deletions(-) create mode 100644 lib/planner_web/live/tasks_components.ex create mode 100644 lib/planner_web/live/tasks_live.ex diff --git a/assets/css/app.scss b/assets/css/app.scss index a564146..3d80207 100644 --- a/assets/css/app.scss +++ b/assets/css/app.scss @@ -4,6 +4,10 @@ cursor: pointer; } +.tasks { + margin-left: 0em !important; +} + .tasks li { @extend .py-1, .px-5; diff --git a/lib/planner/tasks.ex b/lib/planner/tasks.ex index a1c8407..481a2f9 100644 --- a/lib/planner/tasks.ex +++ b/lib/planner/tasks.ex @@ -1,5 +1,6 @@ defmodule Planner.Tasks do import Ecto.Query + alias Ecto.UUID alias Planner.Repo alias Planner.Tasks.Task @@ -59,6 +60,8 @@ defmodule Planner.Tasks do 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 get_task!(id) |> Repo.delete() @@ -69,4 +72,18 @@ defmodule Planner.Tasks do |> Task.finish_task() |> Repo.update() 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 diff --git a/lib/planner_web.ex b/lib/planner_web.ex index bb60531..10b0d14 100644 --- a/lib/planner_web.ex +++ b/lib/planner_web.ex @@ -38,9 +38,6 @@ defmodule PlannerWeb do import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1] import Phoenix.LiveView.Helpers - # Internal View Utils - import PlannerWeb.Util - # Include shared imports and aliases for views unquote(view_helpers()) end @@ -91,6 +88,9 @@ defmodule PlannerWeb do import PlannerWeb.ErrorHelpers import PlannerWeb.Gettext alias PlannerWeb.Router.Helpers, as: Routes + + # Internal View Utils + import PlannerWeb.Util end end diff --git a/lib/planner_web/live/tasks_components.ex b/lib/planner_web/live/tasks_components.ex new file mode 100644 index 0000000..22cb21b --- /dev/null +++ b/lib/planner_web/live/tasks_components.ex @@ -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""" +
+ <%= f = form_for(@changeset, "#", [phx_submit: "new-task"]) %> +
+
+ <%= text_input(f, + :value, + placeholder: "add new task", + class: "input", autocomplete: "off" + )%> +
+ <%= error_tag(f, :value) %> +
+ + + +
+ """ + end +end + +defmodule TaskComponent do + use Phoenix.LiveComponent + + import PlannerWeb.Util + + def render(assigns) do + ~L""" +
  • +
    +
    + +
    +
    + <%= 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 %> +
    + <%= md_to_html(@task.value) %> +
    + <% end %> + <%= if(not is_nil(@task.due_at)) do %> +
    + + due: <%= @task.due_at %> + +
    + <% end %> + <% end %> +
    +
    +
  • + """ + end +end + +defmodule TaskDetailsComponent do + use PlannerWeb, :live_component + + def render(assigns) do + ~L""" +
    + <%= 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 %> +
    + <%= if(not is_nil(@task.due_at)) do %> + + due: <%= @task.due_at %> + <% end %> + <%= if(is_nil(@task.filed_at)) do %> + + unfiled + + <% end %> +
    + <% end %> + +
    + <%= md_to_html(@task.value) %> +
    + +
    + updated: <%= @task.updated_at %> + created: <%= @task.inserted_at %> +
    + +
    + <%= live_patch("edit", + to: @route_func_3.(@socket, :edit, @task.id), + class: "button is-dark is-small" + ) %> + + delete + +
    + +
    + """ + 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""" +
    + <%= f = form_for(@changeset, "#", [phx_submit: "save-task"]) %> + <%= hidden_input(f, :id) %> + +
    +
    + <%= textarea(f, + :value, + required: true, + class: "textarea", + placeholder: "task", + autocomplete: "off" + ) %> +
    + <%= error_tag(f, :value) %> +
    + +
    + <%= label(f, :due_at, class: "label") do %> + due (YYYY-MM-DD HH:MM:SS) + <% end %> +
    + <%= text_input(f, + :due_at, + class: "input", + placeholder: "YYYY-MM-DD HH:MM:SS", + autocomplete: "off" + ) %> +
    + <%= error_tag(f, :due_at) %> +
    + +
    + <%= 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) %> +
    + +
    + <%= submit("save", class: "button is-dark is-small") %> +
    + +
    + """ + end +end diff --git a/lib/planner_web/live/tasks_live.ex b/lib/planner_web/live/tasks_live.ex new file mode 100644 index 0000000..94e6c3c --- /dev/null +++ b/lib/planner_web/live/tasks_live.ex @@ -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""" +
    + <%= 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 + )%> +
    + """ + 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 diff --git a/lib/planner_web/router.ex b/lib/planner_web/router.ex index 4b9e2f1..89dea4d 100644 --- a/lib/planner_web/router.ex +++ b/lib/planner_web/router.ex @@ -54,7 +54,11 @@ defmodule PlannerWeb.Router do 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) put("/users/settings/update_password", UserSettingsController, :update_password) diff --git a/lib/planner_web/templates/layout/root.html.leex b/lib/planner_web/templates/layout/root.html.leex index 84f3ccd..62e032a 100644 --- a/lib/planner_web/templates/layout/root.html.leex +++ b/lib/planner_web/templates/layout/root.html.leex @@ -22,7 +22,7 @@