NEW: Initial task mgmt (#4)

This commit is contained in:
Matthew Ryan Dillon 2020-06-14 15:26:14 -07:00 committed by GitHub
parent 9990771c18
commit 61de82e24e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 261 additions and 104 deletions

View file

@ -13,3 +13,19 @@ import "../css/app.scss"
// import socket from "./socket" // import socket from "./socket"
// //
import "phoenix_html" import "phoenix_html"
import {Socket} from "phoenix"
import LiveSocket from "phoenix_live_view"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
// Connect if there are any LiveViews on the page
liveSocket.connect()
// Expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000)
// The latency simulator is enabled for the duration of the browser session.
// Call disableLatencySim() to disable:
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket

View file

@ -1,63 +0,0 @@
// NOTE: The contents of this file will only be executed if
// you uncomment its entry in "assets/js/app.js".
// To use Phoenix channels, the first step is to import Socket,
// and connect at the socket path in "lib/web/endpoint.ex".
//
// Pass the token on params as below. Or remove it
// from the params if you are not using authentication.
import {Socket} from "phoenix"
let socket = new Socket("/socket", {params: {token: window.userToken}})
// When you connect, you'll often need to authenticate the client.
// For example, imagine you have an authentication plug, `MyAuth`,
// which authenticates the session and assigns a `:current_user`.
// If the current user exists you can assign the user's token in
// the connection for use in the layout.
//
// In your "lib/web/router.ex":
//
// pipeline :browser do
// ...
// plug MyAuth
// plug :put_user_token
// end
//
// defp put_user_token(conn, _) do
// if current_user = conn.assigns[:current_user] do
// token = Phoenix.Token.sign(conn, "user socket", current_user.id)
// assign(conn, :user_token, token)
// else
// conn
// end
// end
//
// Now you need to pass this token to JavaScript. You can do so
// inside a script tag in "lib/web/templates/layout/app.html.eex":
//
// <script>window.userToken = "<%= assigns[:user_token] %>";</script>
//
// You will need to verify the user token in the "connect/3" function
// in "lib/web/channels/user_socket.ex":
//
// def connect(%{"token" => token}, socket, _connect_info) do
// # max_age: 1209600 is equivalent to two weeks in seconds
// case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
// {:ok, user_id} ->
// {:ok, assign(socket, :user, user_id)}
// {:error, reason} ->
// :error
// end
// end
//
// Finally, connect to the socket:
socket.connect()
// Now that you are connected, you can join channels with a topic:
let channel = socket.channel("topic:subtopic", {})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
export default socket

View file

@ -5336,6 +5336,9 @@
"phoenix_html": { "phoenix_html": {
"version": "file:../deps/phoenix_html" "version": "file:../deps/phoenix_html"
}, },
"phoenix_live_view": {
"version": "file:../deps/phoenix_live_view"
},
"picomatch": { "picomatch": {
"version": "2.2.2", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",

View file

@ -8,7 +8,8 @@
}, },
"dependencies": { "dependencies": {
"phoenix": "file:../deps/phoenix", "phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html" "phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.0.0", "@babel/core": "^7.0.0",

39
lib/planner/tasks.ex Normal file
View file

@ -0,0 +1,39 @@
defmodule Planner.Tasks do
import Ecto.Query
alias Planner.Repo
alias Planner.Tasks.Task
def add_task(attrs) do
%Task{}
|> Task.changeset(attrs)
|> Repo.insert()
end
def list_all_tasks, do: Repo.all(Task)
def list_unfinished_tasks do
from(
t in Task,
where: is_nil(t.finished_at)
)
|> Repo.all()
end
def change_task(%Task{} = task) do
task
|> Task.changeset(%{})
end
def get_task!(id), do: Repo.get!(Task, id)
def delete_task_by_id!(id) do
get_task!(id)
|> Repo.delete()
end
def finish_task_by_id!(id) do
get_task!(id)
|> Task.finish_task()
|> Repo.update()
end
end

30
lib/planner/tasks/task.ex Normal file
View file

@ -0,0 +1,30 @@
defmodule Planner.Tasks.Task do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "tasks" do
field(:value, :string)
field(:filed_at, :naive_datetime)
field(:finished_at, :naive_datetime)
field(:due_at, :naive_datetime)
timestamps()
end
@doc false
def changeset(task, attrs) do
task
|> cast(attrs, [:value, :filed_at, :finished_at, :due_at])
|> validate_required([:value])
|> validate_length(:value, min: 3)
end
@doc false
def finish_task(task) do
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
# TODO, this should check if `finished_at` is not nil, first
change(task, finished_at: now)
end
end

View file

@ -23,6 +23,7 @@ defmodule PlannerWeb do
import Plug.Conn import Plug.Conn
import PlannerWeb.Gettext import PlannerWeb.Gettext
import Phoenix.LiveView.Controller
alias PlannerWeb.Router.Helpers, as: Routes alias PlannerWeb.Router.Helpers, as: Routes
end end
end end
@ -35,18 +36,37 @@ defmodule PlannerWeb do
# Import convenience functions from controllers # Import convenience functions from controllers
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
# Include shared imports and aliases for views # Include shared imports and aliases for views
unquote(view_helpers()) unquote(view_helpers())
end end
end end
def live_view do
quote do
use Phoenix.LiveView,
layout: {PlannerWeb.LayoutView, "live.html"}
unquote(view_helpers())
end
end
def live_component do
quote do
use Phoenix.LiveComponent
unquote(view_helpers())
end
end
def router do def router do
quote do quote do
use Phoenix.Router use Phoenix.Router
import Plug.Conn import Plug.Conn
import Phoenix.Controller import Phoenix.Controller
import Phoenix.LiveView.Router
end end
end end

View file

@ -1,7 +0,0 @@
defmodule PlannerWeb.PageController do
use PlannerWeb, :controller
def index(conn, _params) do
render(conn, "index.html")
end
end

View file

@ -0,0 +1,83 @@
defmodule PlannerWeb.LandingLive do
use Phoenix.LiveView, layout: {PlannerWeb.LayoutView, "live.html"}
use Phoenix.HTML
import PlannerWeb.ErrorHelpers
alias Planner.Tasks
alias Planner.Tasks.Task
def mount(_params, _session, socket) do
socket =
socket
# |> put_flash(:info, "hello world")
|> assign(:new_task_changeset, Tasks.change_task(%Task{}))
|> assign(:tasks, Tasks.list_unfinished_tasks())
{:ok, socket}
end
def render(assigns) do
~L"""
<%= f = form_for(@new_task_changeset, "#", [phx_submit: :save_new_task]) %>
<%= label f, :value, "New Task" %>
<%= text_input f, :value %>
<%= error_tag f, :value %>
<%= submit "Create" %>
</form>
<hr>
<table>
<thead>
<tr>
<th colspan="3">tasks</th>
</tr>
</thead>
<tbody>
<%= for task <- @tasks do %>
<tr>
<td><%= task.value %></td>
<td><button phx-click="delete_task" phx-value-task_id="<%= task.id %>">delete</button></td>
<td><button phx-click="finish_task" phx-value-task_id="<%= task.id %>">done</button></td>
</tr>
<% end %>
</tbody>
</table>
"""
end
def handle_event("save_new_task", %{"task" => task_params}, socket) do
case Tasks.add_task(task_params) do
{:ok, task} ->
{:noreply,
socket
|> put_flash(:info, "task created")
|> assign(:tasks, Tasks.list_unfinished_tasks())}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply,
socket
|> assign(new_task_changeset: changeset)}
end
end
def handle_event("delete_task", %{"task_id" => task_id}, socket) do
Tasks.delete_task_by_id!(task_id)
{:noreply,
socket
|> put_flash(:info, "task deleted")
|> assign(:tasks, Tasks.list_unfinished_tasks())}
end
def handle_event("finish_task", %{"task_id" => task_id}, socket) do
Tasks.finish_task_by_id!(task_id)
{:noreply,
socket
|> put_flash(:info, "task completed")
|> assign(:tasks, Tasks.list_unfinished_tasks())}
end
end

View file

@ -6,7 +6,8 @@ defmodule PlannerWeb.Router do
pipeline :browser do pipeline :browser do
plug(:accepts, ["html"]) plug(:accepts, ["html"])
plug(:fetch_session) plug(:fetch_session)
plug(:fetch_flash) plug(:fetch_live_flash)
plug(:put_root_layout, {PlannerWeb.LayoutView, :root})
plug(:protect_from_forgery) plug(:protect_from_forgery)
plug(:put_secure_browser_headers) plug(:put_secure_browser_headers)
plug(:fetch_current_user) plug(:fetch_current_user)
@ -51,7 +52,7 @@ defmodule PlannerWeb.Router do
scope "/", PlannerWeb do scope "/", PlannerWeb do
pipe_through([:browser, :require_authenticated_user]) pipe_through([:browser, :require_authenticated_user])
get("/", PageController, :index) live("/", LandingLive, :index)
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

@ -1,28 +1,5 @@
<!DOCTYPE html> <main role="main" class="container">
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Planner · Phoenix Framework</title>
<link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
<script defer type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
</head>
<body>
<ul>
<%= if @current_user do %>
<li><%= @current_user.email %></li>
<li><%= link "Settings", to: Routes.user_settings_path(@conn, :edit) %></li>
<li><%= link "Logout", to: Routes.user_session_path(@conn, :delete), method: :delete %></li>
<% else %>
<li><%= link "Login", to: Routes.user_session_path(@conn, :new) %></li>
<% end %>
</ul>
<main role="main" class="container">
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p> <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p> <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<%= @inner_content %> <%= @inner_content %>
</main> </main>
</body>
</html>

View file

@ -0,0 +1,11 @@
<main role="main" class="container">
<p class="alert alert-info" role="alert"
phx-click="lv:clear-flash"
phx-value-key="info"><%= live_flash(@flash, :info) %></p>
<p class="alert alert-danger" role="alert"
phx-click="lv:clear-flash"
phx-value-key="error"><%= live_flash(@flash, :error) %></p>
<%= @inner_content %>
</main>

View file

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Planner</title>
<link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
<%= csrf_meta_tag() %>
<script defer type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
</head>
<body>
<h1>Planner</h1>
<ul>
<%= if @current_user do %>
<li><%= @current_user.email %></li>
<li><%= link "Settings", to: Routes.user_settings_path(@conn, :edit) %></li>
<li><%= link "Logout", to: Routes.user_session_path(@conn, :delete), method: :delete %></li>
<% else %>
<li><%= link "Login", to: Routes.user_session_path(@conn, :new) %></li>
<% end %>
</ul>
<hr>
<%= @inner_content %>
</body>
</html>

View file

@ -1 +0,0 @@
<h1>Planner</h1>

View file

@ -46,7 +46,9 @@ defmodule Planner.MixProject do
{:telemetry_poller, "~> 0.4"}, {:telemetry_poller, "~> 0.4"},
{: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"},
{:floki, ">= 0.0.0", only: :test}
] ]
end end

View file

@ -10,7 +10,9 @@
"ecto_sql": {:hex, :ecto_sql, "3.4.4", "d28bac2d420f708993baed522054870086fd45016a9d09bb2cd521b9c48d32ea", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "edb49af715dd72f213b66adfd0f668a43c17ed510b5d9ac7528569b23af57fe8"}, "ecto_sql": {:hex, :ecto_sql, "3.4.4", "d28bac2d420f708993baed522054870086fd45016a9d09bb2cd521b9c48d32ea", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "edb49af715dd72f213b66adfd0f668a43c17ed510b5d9ac7528569b23af57fe8"},
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"}, "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
"file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"}, "file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"},
"floki": {:hex, :floki, "0.26.0", "4df88977e2e357c6720e1b650f613444bfb48c5acfc6a0c646ab007d08ad13bf", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "e7b66ce7feef5518a9cd9fc7b52dd62a64028bd9cb6d6ad282a0f0fc90a4ae52"},
"gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"}, "gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"},
"html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"}, "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
"phoenix": {:hex, :phoenix, "1.5.3", "bfe0404e48ea03dfe17f141eff34e1e058a23f15f109885bbdcf62be303b49ff", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e16febeb9640d8b33895a691a56481464b82836d338bb3a23125cd7b6157c25"}, "phoenix": {:hex, :phoenix, "1.5.3", "bfe0404e48ea03dfe17f141eff34e1e058a23f15f109885bbdcf62be303b49ff", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e16febeb9640d8b33895a691a56481464b82836d338bb3a23125cd7b6157c25"},

View file

@ -0,0 +1,15 @@
defmodule Planner.Repo.Migrations.InitialTasks do
use Ecto.Migration
def change do
create table(:tasks, primary_key: false) do
add(:id, :binary_id, primary_key: true)
add(:value, :text, null: false)
add(:filed_at, :naive_datetime)
add(:finished_at, :naive_datetime)
add(:due_at, :naive_datetime)
timestamps()
end
end
end