From 84d6b005e466d1bb69f2b8ac00a5e99c039a067a Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 28 Jan 2024 21:18:19 +0100 Subject: [PATCH 01/42] Update Readme --- README.md | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a2d3a82..e7c52ad 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,30 @@ -# cklist +# ✔️ cklist -Awesome checklists are about to come here. Stay tuned. +Awesome :heavy_check_mark: checklists are about to come here. Stay tuned... -## Contributing +## 🤝 Contributing -Contributions are welcome. +We are just getting started. Feel free to reach out if you feel like joining. -- cklists are based on the awesome [Phoenix Framework](https://www.phoenixframework.org/). -- Elixir & Erlang versions are managed with [asdf](https://asdf-vm.com/) in [.tool-versions](.tool-versions). -- We use [PostgreSQL](https://www.postgresql.org/) as database backend. For local development, we assume a user `cklist` exists (see [config/dev.exs](./config/dev.exs)). The authentication system makes use of the `citext` extension of PostgreSQL. If DB migration complains about missing the `citext` extension, try search for and installing the `postgres-contrib` package. +### Setup notes + +#### Framework + +cklists are based on the awesome [Phoenix Framework](https://www.phoenixframework.org/). + +#### Versions + +Elixir, Erlang, and npm versions are managed with [asdf](https://asdf-vm.com/) in [.tool-versions](.tool-versions). + +#### Database + +We use [PostgreSQL](https://www.postgresql.org/) as database backend. Phoenix' authentication system makes use of the `citext` extension of PostgreSQL. If DB migration complains about missing the `citext` extension, try search for and installing the `postgres-contrib` package. + +Database secrets can be stored in `.env`-style configuration. This is useful for both, local development and deployments. To override the database configuration, put the following contents + +```none +DATABASE_URL="ecto://cklist:cklist@localhost/cklist_dev" +TEST_DATABASE_URL="ecto://cklist:cklist@localhost/cklist_test" +``` + +in a file `envs/.env` and adapt the defaults to your liking. From 818f34a33968aa8f556887c06a9f11515eab4c1c Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 28 Jan 2024 20:25:04 +0100 Subject: [PATCH 02/42] Ignore VSCode configuration folder --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 899f4fa..62755bd 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ npm-debug.log /assets/node_modules/ .env envs/ + +# VSCode configuration +/.vscode/ From f528a4154def2d4067685ded54dcb7e4c3ccab6f Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 28 Jan 2024 20:57:26 +0100 Subject: [PATCH 03/42] Basic checklist ressource I ran the following `mix` command to auto-generate a `checklist` ressource ``` mix phx.gen.html Checklists Checklist checklists title:string description:string user_id:references:users document:map ``` --- lib/cklist/checklists.ex | 104 ++++++++++++++++++ lib/cklist/checklists/checklist.ex | 20 ++++ .../controllers/checklist_controller.ex | 62 +++++++++++ lib/cklist_web/controllers/checklist_html.ex | 13 +++ .../checklist_html/checklist_form.html.heex | 10 ++ .../controllers/checklist_html/edit.html.heex | 8 ++ .../checklist_html/index.html.heex | 25 +++++ .../controllers/checklist_html/new.html.heex | 8 ++ .../controllers/checklist_html/show.html.heex | 17 +++ lib/cklist_web/router.ex | 2 + .../20240128194147_create_checklists.exs | 16 +++ test/cklist/checklists_test.exs | 63 +++++++++++ .../controllers/checklist_controller_test.exs | 84 ++++++++++++++ test/support/fixtures/checklists_fixtures.ex | 22 ++++ 14 files changed, 454 insertions(+) create mode 100644 lib/cklist/checklists.ex create mode 100644 lib/cklist/checklists/checklist.ex create mode 100644 lib/cklist_web/controllers/checklist_controller.ex create mode 100644 lib/cklist_web/controllers/checklist_html.ex create mode 100644 lib/cklist_web/controllers/checklist_html/checklist_form.html.heex create mode 100644 lib/cklist_web/controllers/checklist_html/edit.html.heex create mode 100644 lib/cklist_web/controllers/checklist_html/index.html.heex create mode 100644 lib/cklist_web/controllers/checklist_html/new.html.heex create mode 100644 lib/cklist_web/controllers/checklist_html/show.html.heex create mode 100644 priv/repo/migrations/20240128194147_create_checklists.exs create mode 100644 test/cklist/checklists_test.exs create mode 100644 test/cklist_web/controllers/checklist_controller_test.exs create mode 100644 test/support/fixtures/checklists_fixtures.ex diff --git a/lib/cklist/checklists.ex b/lib/cklist/checklists.ex new file mode 100644 index 0000000..08d87b0 --- /dev/null +++ b/lib/cklist/checklists.ex @@ -0,0 +1,104 @@ +defmodule Cklist.Checklists do + @moduledoc """ + The Checklists context. + """ + + import Ecto.Query, warn: false + alias Cklist.Repo + + alias Cklist.Checklists.Checklist + + @doc """ + Returns the list of checklists. + + ## Examples + + iex> list_checklists() + [%Checklist{}, ...] + + """ + def list_checklists do + Repo.all(Checklist) + end + + @doc """ + Gets a single checklist. + + Raises `Ecto.NoResultsError` if the Checklist does not exist. + + ## Examples + + iex> get_checklist!(123) + %Checklist{} + + iex> get_checklist!(456) + ** (Ecto.NoResultsError) + + """ + def get_checklist!(id), do: Repo.get!(Checklist, id) + + @doc """ + Creates a checklist. + + ## Examples + + iex> create_checklist(%{field: value}) + {:ok, %Checklist{}} + + iex> create_checklist(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_checklist(attrs \\ %{}) do + %Checklist{} + |> Checklist.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a checklist. + + ## Examples + + iex> update_checklist(checklist, %{field: new_value}) + {:ok, %Checklist{}} + + iex> update_checklist(checklist, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_checklist(%Checklist{} = checklist, attrs) do + checklist + |> Checklist.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a checklist. + + ## Examples + + iex> delete_checklist(checklist) + {:ok, %Checklist{}} + + iex> delete_checklist(checklist) + {:error, %Ecto.Changeset{}} + + """ + def delete_checklist(%Checklist{} = checklist) do + Repo.delete(checklist) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking checklist changes. + + ## Examples + + iex> change_checklist(checklist) + %Ecto.Changeset{data: %Checklist{}} + + """ + def change_checklist(%Checklist{} = checklist, attrs \\ %{}) do + Checklist.changeset(checklist, attrs) + end +end diff --git a/lib/cklist/checklists/checklist.ex b/lib/cklist/checklists/checklist.ex new file mode 100644 index 0000000..f371a43 --- /dev/null +++ b/lib/cklist/checklists/checklist.ex @@ -0,0 +1,20 @@ +defmodule Cklist.Checklists.Checklist do + use Ecto.Schema + import Ecto.Changeset + + schema "checklists" do + field :description, :string + field :title, :string + field :document, :map + field :user_id, :id + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(checklist, attrs) do + checklist + |> cast(attrs, [:title, :description, :document]) + |> validate_required([:title, :description]) + end +end diff --git a/lib/cklist_web/controllers/checklist_controller.ex b/lib/cklist_web/controllers/checklist_controller.ex new file mode 100644 index 0000000..4fff10b --- /dev/null +++ b/lib/cklist_web/controllers/checklist_controller.ex @@ -0,0 +1,62 @@ +defmodule CklistWeb.ChecklistController do + use CklistWeb, :controller + + alias Cklist.Checklists + alias Cklist.Checklists.Checklist + + def index(conn, _params) do + checklists = Checklists.list_checklists() + render(conn, :index, checklists: checklists) + end + + def new(conn, _params) do + changeset = Checklists.change_checklist(%Checklist{}) + render(conn, :new, changeset: changeset) + end + + def create(conn, %{"checklist" => checklist_params}) do + case Checklists.create_checklist(checklist_params) do + {:ok, checklist} -> + conn + |> put_flash(:info, "Checklist created successfully.") + |> redirect(to: ~p"/checklists/#{checklist}") + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, :new, changeset: changeset) + end + end + + def show(conn, %{"id" => id}) do + checklist = Checklists.get_checklist!(id) + render(conn, :show, checklist: checklist) + end + + def edit(conn, %{"id" => id}) do + checklist = Checklists.get_checklist!(id) + changeset = Checklists.change_checklist(checklist) + render(conn, :edit, checklist: checklist, changeset: changeset) + end + + def update(conn, %{"id" => id, "checklist" => checklist_params}) do + checklist = Checklists.get_checklist!(id) + + case Checklists.update_checklist(checklist, checklist_params) do + {:ok, checklist} -> + conn + |> put_flash(:info, "Checklist updated successfully.") + |> redirect(to: ~p"/checklists/#{checklist}") + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, :edit, checklist: checklist, changeset: changeset) + end + end + + def delete(conn, %{"id" => id}) do + checklist = Checklists.get_checklist!(id) + {:ok, _checklist} = Checklists.delete_checklist(checklist) + + conn + |> put_flash(:info, "Checklist deleted successfully.") + |> redirect(to: ~p"/checklists") + end +end diff --git a/lib/cklist_web/controllers/checklist_html.ex b/lib/cklist_web/controllers/checklist_html.ex new file mode 100644 index 0000000..21a0cbf --- /dev/null +++ b/lib/cklist_web/controllers/checklist_html.ex @@ -0,0 +1,13 @@ +defmodule CklistWeb.ChecklistHTML do + use CklistWeb, :html + + embed_templates "checklist_html/*" + + @doc """ + Renders a checklist form. + """ + attr :changeset, Ecto.Changeset, required: true + attr :action, :string, required: true + + def checklist_form(assigns) +end diff --git a/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex b/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex new file mode 100644 index 0000000..8e878a4 --- /dev/null +++ b/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex @@ -0,0 +1,10 @@ +<.simple_form :let={f} for={@changeset} action={@action}> + <.error :if={@changeset.action}> + Oops, something went wrong! Please check the errors below. + + <.input field={f[:title]} type="text" label="Title" /> + <.input field={f[:description]} type="text" label="Description" /> + <:actions> + <.button>Save Checklist + + diff --git a/lib/cklist_web/controllers/checklist_html/edit.html.heex b/lib/cklist_web/controllers/checklist_html/edit.html.heex new file mode 100644 index 0000000..46187dc --- /dev/null +++ b/lib/cklist_web/controllers/checklist_html/edit.html.heex @@ -0,0 +1,8 @@ +<.header> + Edit Checklist <%= @checklist.id %> + <:subtitle>Use this form to manage checklist records in your database. + + +<.checklist_form changeset={@changeset} action={~p"/checklists/#{@checklist}"} /> + +<.back navigate={~p"/checklists"}>Back to checklists diff --git a/lib/cklist_web/controllers/checklist_html/index.html.heex b/lib/cklist_web/controllers/checklist_html/index.html.heex new file mode 100644 index 0000000..aaf88c0 --- /dev/null +++ b/lib/cklist_web/controllers/checklist_html/index.html.heex @@ -0,0 +1,25 @@ +<.header> + Listing Checklists + <:actions> + <.link href={~p"/checklists/new"}> + <.button>New Checklist + + + + +<.table id="checklists" rows={@checklists} row_click={&JS.navigate(~p"/checklists/#{&1}")}> + <:col :let={checklist} label="Title"><%= checklist.title %> + <:col :let={checklist} label="Description"><%= checklist.description %> + <:col :let={checklist} label="Document"><%= checklist.document %> + <:action :let={checklist}> +
+ <.link navigate={~p"/checklists/#{checklist}"}>Show +
+ <.link navigate={~p"/checklists/#{checklist}/edit"}>Edit + + <:action :let={checklist}> + <.link href={~p"/checklists/#{checklist}"} method="delete" data-confirm="Are you sure?"> + Delete + + + diff --git a/lib/cklist_web/controllers/checklist_html/new.html.heex b/lib/cklist_web/controllers/checklist_html/new.html.heex new file mode 100644 index 0000000..5031dbd --- /dev/null +++ b/lib/cklist_web/controllers/checklist_html/new.html.heex @@ -0,0 +1,8 @@ +<.header> + New Checklist + <:subtitle>Use this form to manage checklist records in your database. + + +<.checklist_form changeset={@changeset} action={~p"/checklists"} /> + +<.back navigate={~p"/checklists"}>Back to checklists diff --git a/lib/cklist_web/controllers/checklist_html/show.html.heex b/lib/cklist_web/controllers/checklist_html/show.html.heex new file mode 100644 index 0000000..2c7cecc --- /dev/null +++ b/lib/cklist_web/controllers/checklist_html/show.html.heex @@ -0,0 +1,17 @@ +<.header> + Checklist <%= @checklist.id %> + <:subtitle>This is a checklist record from your database. + <:actions> + <.link href={~p"/checklists/#{@checklist}/edit"}> + <.button>Edit checklist + + + + +<.list> + <:item title="Title"><%= @checklist.title %> + <:item title="Description"><%= @checklist.description %> + <:item title="Document"><%= @checklist.document %> + + +<.back navigate={~p"/checklists"}>Back to checklists diff --git a/lib/cklist_web/router.ex b/lib/cklist_web/router.ex index b1255e8..cc70693 100644 --- a/lib/cklist_web/router.ex +++ b/lib/cklist_web/router.ex @@ -24,6 +24,8 @@ defmodule CklistWeb.Router do pipe_through :browser get "/", PageController, :home + + resources "/checklists", ChecklistController end # Other scopes may use custom stacks. diff --git a/priv/repo/migrations/20240128194147_create_checklists.exs b/priv/repo/migrations/20240128194147_create_checklists.exs new file mode 100644 index 0000000..8cbbd49 --- /dev/null +++ b/priv/repo/migrations/20240128194147_create_checklists.exs @@ -0,0 +1,16 @@ +defmodule Cklist.Repo.Migrations.CreateChecklists do + use Ecto.Migration + + def change do + create table(:checklists) do + add :title, :string + add :description, :string + add :document, :map + add :user_id, references(:users, on_delete: :nothing) + + timestamps(type: :utc_datetime) + end + + create index(:checklists, [:user_id]) + end +end diff --git a/test/cklist/checklists_test.exs b/test/cklist/checklists_test.exs new file mode 100644 index 0000000..1c0d643 --- /dev/null +++ b/test/cklist/checklists_test.exs @@ -0,0 +1,63 @@ +defmodule Cklist.ChecklistsTest do + use Cklist.DataCase + + alias Cklist.Checklists + + describe "checklists" do + alias Cklist.Checklists.Checklist + + import Cklist.ChecklistsFixtures + + @invalid_attrs %{description: nil, title: nil, document: nil} + + test "list_checklists/0 returns all checklists" do + checklist = checklist_fixture() + assert Checklists.list_checklists() == [checklist] + end + + test "get_checklist!/1 returns the checklist with given id" do + checklist = checklist_fixture() + assert Checklists.get_checklist!(checklist.id) == checklist + end + + test "create_checklist/1 with valid data creates a checklist" do + valid_attrs = %{description: "some description", title: "some title", document: %{}} + + assert {:ok, %Checklist{} = checklist} = Checklists.create_checklist(valid_attrs) + assert checklist.description == "some description" + assert checklist.title == "some title" + assert checklist.document == %{} + end + + test "create_checklist/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Checklists.create_checklist(@invalid_attrs) + end + + test "update_checklist/2 with valid data updates the checklist" do + checklist = checklist_fixture() + update_attrs = %{description: "some updated description", title: "some updated title", document: %{}} + + assert {:ok, %Checklist{} = checklist} = Checklists.update_checklist(checklist, update_attrs) + assert checklist.description == "some updated description" + assert checklist.title == "some updated title" + assert checklist.document == %{} + end + + test "update_checklist/2 with invalid data returns error changeset" do + checklist = checklist_fixture() + assert {:error, %Ecto.Changeset{}} = Checklists.update_checklist(checklist, @invalid_attrs) + assert checklist == Checklists.get_checklist!(checklist.id) + end + + test "delete_checklist/1 deletes the checklist" do + checklist = checklist_fixture() + assert {:ok, %Checklist{}} = Checklists.delete_checklist(checklist) + assert_raise Ecto.NoResultsError, fn -> Checklists.get_checklist!(checklist.id) end + end + + test "change_checklist/1 returns a checklist changeset" do + checklist = checklist_fixture() + assert %Ecto.Changeset{} = Checklists.change_checklist(checklist) + end + end +end diff --git a/test/cklist_web/controllers/checklist_controller_test.exs b/test/cklist_web/controllers/checklist_controller_test.exs new file mode 100644 index 0000000..79176fb --- /dev/null +++ b/test/cklist_web/controllers/checklist_controller_test.exs @@ -0,0 +1,84 @@ +defmodule CklistWeb.ChecklistControllerTest do + use CklistWeb.ConnCase + + import Cklist.ChecklistsFixtures + + @create_attrs %{description: "some description", title: "some title", document: %{}} + @update_attrs %{description: "some updated description", title: "some updated title", document: %{}} + @invalid_attrs %{description: nil, title: nil, document: nil} + + describe "index" do + test "lists all checklists", %{conn: conn} do + conn = get(conn, ~p"/checklists") + assert html_response(conn, 200) =~ "Listing Checklists" + end + end + + describe "new checklist" do + test "renders form", %{conn: conn} do + conn = get(conn, ~p"/checklists/new") + assert html_response(conn, 200) =~ "New Checklist" + end + end + + describe "create checklist" do + test "redirects to show when data is valid", %{conn: conn} do + conn = post(conn, ~p"/checklists", checklist: @create_attrs) + + assert %{id: id} = redirected_params(conn) + assert redirected_to(conn) == ~p"/checklists/#{id}" + + conn = get(conn, ~p"/checklists/#{id}") + assert html_response(conn, 200) =~ "Checklist #{id}" + end + + test "renders errors when data is invalid", %{conn: conn} do + conn = post(conn, ~p"/checklists", checklist: @invalid_attrs) + assert html_response(conn, 200) =~ "New Checklist" + end + end + + describe "edit checklist" do + setup [:create_checklist] + + test "renders form for editing chosen checklist", %{conn: conn, checklist: checklist} do + conn = get(conn, ~p"/checklists/#{checklist}/edit") + assert html_response(conn, 200) =~ "Edit Checklist" + end + end + + describe "update checklist" do + setup [:create_checklist] + + test "redirects when data is valid", %{conn: conn, checklist: checklist} do + conn = put(conn, ~p"/checklists/#{checklist}", checklist: @update_attrs) + assert redirected_to(conn) == ~p"/checklists/#{checklist}" + + conn = get(conn, ~p"/checklists/#{checklist}") + assert html_response(conn, 200) =~ "some updated description" + end + + test "renders errors when data is invalid", %{conn: conn, checklist: checklist} do + conn = put(conn, ~p"/checklists/#{checklist}", checklist: @invalid_attrs) + assert html_response(conn, 200) =~ "Edit Checklist" + end + end + + describe "delete checklist" do + setup [:create_checklist] + + test "deletes chosen checklist", %{conn: conn, checklist: checklist} do + conn = delete(conn, ~p"/checklists/#{checklist}") + assert redirected_to(conn) == ~p"/checklists" + + assert_error_sent 404, fn -> + get(conn, ~p"/checklists/#{checklist}") + end + end + end + + defp create_checklist(_) do + checklist = checklist_fixture() + %{checklist: checklist} + end +end diff --git a/test/support/fixtures/checklists_fixtures.ex b/test/support/fixtures/checklists_fixtures.ex new file mode 100644 index 0000000..aed7ae5 --- /dev/null +++ b/test/support/fixtures/checklists_fixtures.ex @@ -0,0 +1,22 @@ +defmodule Cklist.ChecklistsFixtures do + @moduledoc """ + This module defines test helpers for creating + entities via the `Cklist.Checklists` context. + """ + + @doc """ + Generate a checklist. + """ + def checklist_fixture(attrs \\ %{}) do + {:ok, checklist} = + attrs + |> Enum.into(%{ + description: "some description", + document: %{}, + title: "some title" + }) + |> Cklist.Checklists.create_checklist() + + checklist + end +end From 613707521a6291a749b6dacc5e60f3efa7bc639f Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 28 Jan 2024 21:28:39 +0100 Subject: [PATCH 04/42] Only :index of ChecklistController is public --- lib/cklist_web/router.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/cklist_web/router.ex b/lib/cklist_web/router.ex index cc70693..983b1f7 100644 --- a/lib/cklist_web/router.ex +++ b/lib/cklist_web/router.ex @@ -25,7 +25,7 @@ defmodule CklistWeb.Router do get "/", PageController, :home - resources "/checklists", ChecklistController + resources "/checklists", ChecklistController, only: [:index] end # Other scopes may use custom stacks. @@ -74,6 +74,8 @@ defmodule CklistWeb.Router do live "/user/settings", UserSettingsLive, :edit live "/user/settings/confirm_email/:token", UserSettingsLive, :confirm_email end + + resources "/checklists", ChecklistController, except: [:index] end scope "/", CklistWeb do From d921abf91f600e17cb29c9cf353926f18fcbe3f6 Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 28 Jan 2024 22:43:18 +0100 Subject: [PATCH 05/42] Basic HTML rebranding Some rebranding of the default phoenix template to fit our needs. Icons from uxwing, e.g. https://uxwing.com/check-mark-box-icon/. --- .../components/layouts/app.html.heex | 51 +++-- .../components/layouts/root.html.heex | 41 ---- .../controllers/page_html/home.html.heex | 216 ++++++------------ priv/static/images/explore.svg | 1 + priv/static/images/log_in.svg | 1 + priv/static/images/log_out.svg | 1 + priv/static/images/logo.svg | 7 +- priv/static/images/register.svg | 1 + priv/static/images/settings.svg | 1 + 9 files changed, 112 insertions(+), 208 deletions(-) create mode 100644 priv/static/images/explore.svg create mode 100644 priv/static/images/log_in.svg create mode 100644 priv/static/images/log_out.svg create mode 100644 priv/static/images/register.svg create mode 100644 priv/static/images/settings.svg diff --git a/lib/cklist_web/components/layouts/app.html.heex b/lib/cklist_web/components/layouts/app.html.heex index e23bfc8..049a9f8 100644 --- a/lib/cklist_web/components/layouts/app.html.heex +++ b/lib/cklist_web/components/layouts/app.html.heex @@ -1,26 +1,51 @@
- + -

- v<%= Application.spec(:phoenix, :vsn) %> +

+ cklist

diff --git a/lib/cklist_web/components/layouts/root.html.heex b/lib/cklist_web/components/layouts/root.html.heex index 8e2ac1d..5837573 100644 --- a/lib/cklist_web/components/layouts/root.html.heex +++ b/lib/cklist_web/components/layouts/root.html.heex @@ -12,47 +12,6 @@ - <%= @inner_content %> diff --git a/lib/cklist_web/controllers/page_html/home.html.heex b/lib/cklist_web/controllers/page_html/home.html.heex index e9fc48d..9f72cd9 100644 --- a/lib/cklist_web/controllers/page_html/home.html.heex +++ b/lib/cklist_web/controllers/page_html/home.html.heex @@ -40,49 +40,85 @@
- -

- Phoenix Framework - - v<%= Application.spec(:phoenix, :vsn) %> - + + + +

+ cklist

- Peace of mind from prototype to production. + Peace of mind. Every time.

- Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale. + Setup checklists once, run them many times. Share them with family and friends.

diff --git a/priv/static/images/explore.svg b/priv/static/images/explore.svg new file mode 100644 index 0000000..5d1d33e --- /dev/null +++ b/priv/static/images/explore.svg @@ -0,0 +1 @@ +lookup \ No newline at end of file diff --git a/priv/static/images/log_in.svg b/priv/static/images/log_in.svg new file mode 100644 index 0000000..2164268 --- /dev/null +++ b/priv/static/images/log_in.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/priv/static/images/log_out.svg b/priv/static/images/log_out.svg new file mode 100644 index 0000000..d0e9a48 --- /dev/null +++ b/priv/static/images/log_out.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/priv/static/images/logo.svg b/priv/static/images/logo.svg index 9f26bab..64e3d6b 100644 --- a/priv/static/images/logo.svg +++ b/priv/static/images/logo.svg @@ -1,6 +1 @@ - + \ No newline at end of file diff --git a/priv/static/images/register.svg b/priv/static/images/register.svg new file mode 100644 index 0000000..9417947 --- /dev/null +++ b/priv/static/images/register.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/priv/static/images/settings.svg b/priv/static/images/settings.svg new file mode 100644 index 0000000..1a4a018 --- /dev/null +++ b/priv/static/images/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file From b686492b2e38ef36cf96c42ff21e4b1288938cf3 Mon Sep 17 00:00:00 2001 From: Aaron Seigo Date: Mon, 29 Jan 2024 16:22:42 +0100 Subject: [PATCH 06/42] Users -> user --- lib/cklist_web/components/layouts/app.html.heex | 12 ++++++------ lib/cklist_web/controllers/page_html/home.html.heex | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/cklist_web/components/layouts/app.html.heex b/lib/cklist_web/components/layouts/app.html.heex index 049a9f8..1e51412 100644 --- a/lib/cklist_web/components/layouts/app.html.heex +++ b/lib/cklist_web/components/layouts/app.html.heex @@ -9,18 +9,18 @@

- <%= if @current_users do %> + <%= if @current_user do %>

- <%= @current_users.email %> + <%= @current_user.email %>

<.link - href={~p"/users/settings"} + href={~p"/user/settings"} class="hover:text-zinc-700" > Settings <.link - href={~p"/users/log_out"} + href={~p"/user/log_out"} method="delete" class="hover:text-zinc-700" > @@ -28,13 +28,13 @@ <% else %> <.link - href={~p"/users/register"} + href={~p"/user/register"} class="hover:text-zinc-700" > Register <.link - href={~p"/users/log_in"} + href={~p"/user/log_in"} class="hover:text-zinc-700" > Log in diff --git a/lib/cklist_web/controllers/page_html/home.html.heex b/lib/cklist_web/controllers/page_html/home.html.heex index 9f72cd9..9118625 100644 --- a/lib/cklist_web/controllers/page_html/home.html.heex +++ b/lib/cklist_web/controllers/page_html/home.html.heex @@ -67,9 +67,9 @@ - <%= if @current_users do %> + <%= if @current_user do %> <.link - href={~p"/users/settings"} + href={~p"/user/settings"} class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6" > @@ -80,7 +80,7 @@ <.link - href={~p"/users/log_out"} + href={~p"/user/log_out"} method="delete" class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6" > @@ -93,7 +93,7 @@ <% else %> <.link - href={~p"/users/register"} + href={~p"/user/register"} class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6" > @@ -104,7 +104,7 @@ <.link - href={~p"/users/log_in"} + href={~p"/user/log_in"} class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6" > From ef741b55d78c937d3ff9830bb0adcdf46845996b Mon Sep 17 00:00:00 2001 From: Aaron Seigo Date: Mon, 29 Jan 2024 17:34:57 +0100 Subject: [PATCH 07/42] email is no longer shown on the page when authed --- test/cklist_web/live/user_registration_live_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cklist_web/live/user_registration_live_test.exs b/test/cklist_web/live/user_registration_live_test.exs index f6ff04c..365b2ee 100644 --- a/test/cklist_web/live/user_registration_live_test.exs +++ b/test/cklist_web/live/user_registration_live_test.exs @@ -50,7 +50,7 @@ defmodule CklistWeb.UserRegistrationLiveTest do # Now do a logged in request and assert on the menu conn = get(conn, "/") response = html_response(conn, 200) - assert response =~ email + # assert response =~ email assert response =~ "Settings" assert response =~ "Log out" end From e1b2f0ae809782a625358e80440e48f77f039efe Mon Sep 17 00:00:00 2001 From: Aaron Seigo Date: Mon, 29 Jan 2024 17:35:16 +0100 Subject: [PATCH 08/42] fix test --- test/cklist_web/controllers/page_controller_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cklist_web/controllers/page_controller_test.exs b/test/cklist_web/controllers/page_controller_test.exs index e5c0f4c..4ba6b46 100644 --- a/test/cklist_web/controllers/page_controller_test.exs +++ b/test/cklist_web/controllers/page_controller_test.exs @@ -3,6 +3,6 @@ defmodule CklistWeb.PageControllerTest do test "GET /", %{conn: conn} do conn = get(conn, ~p"/") - assert html_response(conn, 200) =~ "Peace of mind from prototype to production" + assert html_response(conn, 200) =~ "Peace of mind" end end From 534c99af2ba3160f087152d77bcbc59c5695262b Mon Sep 17 00:00:00 2001 From: Aaron Seigo Date: Mon, 29 Jan 2024 17:35:50 +0100 Subject: [PATCH 09/42] authenticate for these tests --- .../controllers/checklist_controller_test.exs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/test/cklist_web/controllers/checklist_controller_test.exs b/test/cklist_web/controllers/checklist_controller_test.exs index 79176fb..3d7595b 100644 --- a/test/cklist_web/controllers/checklist_controller_test.exs +++ b/test/cklist_web/controllers/checklist_controller_test.exs @@ -2,11 +2,30 @@ defmodule CklistWeb.ChecklistControllerTest do use CklistWeb.ConnCase import Cklist.ChecklistsFixtures + import Cklist.AccountsFixtures @create_attrs %{description: "some description", title: "some title", document: %{}} - @update_attrs %{description: "some updated description", title: "some updated title", document: %{}} + @update_attrs %{ + description: "some updated description", + title: "some updated title", + document: %{} + } @invalid_attrs %{description: nil, title: nil, document: nil} + @remember_me_cookie "_cklist_web_user_remember_me" + + setup %{conn: conn} do + user_token = Cklist.Accounts.generate_user_session_token(user_fixture()) + + %{ + conn: + conn + |> init_test_session(%{}) + |> put_session(:user_token, user_token) + |> put_req_cookie(@remember_me_cookie, user_token) + } + end + describe "index" do test "lists all checklists", %{conn: conn} do conn = get(conn, ~p"/checklists") From 9c39f1c10de8de9af35c1ff0ad550782677df201 Mon Sep 17 00:00:00 2001 From: Aaron Seigo Date: Mon, 29 Jan 2024 17:36:10 +0100 Subject: [PATCH 10/42] maps need to be serialized --- lib/cklist_web/controllers/checklist_html/show.html.heex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cklist_web/controllers/checklist_html/show.html.heex b/lib/cklist_web/controllers/checklist_html/show.html.heex index 2c7cecc..e73f1f0 100644 --- a/lib/cklist_web/controllers/checklist_html/show.html.heex +++ b/lib/cklist_web/controllers/checklist_html/show.html.heex @@ -11,7 +11,7 @@ <.list> <:item title="Title"><%= @checklist.title %> <:item title="Description"><%= @checklist.description %> - <:item title="Document"><%= @checklist.document %> + <:item title="Document"><%= "#{inspect @checklist.document}" %> <.back navigate={~p"/checklists"}>Back to checklists From 85744b36262457a01b9218b0e12df0e12824a3f2 Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Mon, 5 Feb 2024 21:06:46 +0100 Subject: [PATCH 11/42] Save user_id when creating a new checklist --- lib/cklist/checklists/checklist.ex | 4 ++-- lib/cklist_web/controllers/checklist_controller.ex | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/cklist/checklists/checklist.ex b/lib/cklist/checklists/checklist.ex index f371a43..2e44ce7 100644 --- a/lib/cklist/checklists/checklist.ex +++ b/lib/cklist/checklists/checklist.ex @@ -14,7 +14,7 @@ defmodule Cklist.Checklists.Checklist do @doc false def changeset(checklist, attrs) do checklist - |> cast(attrs, [:title, :description, :document]) - |> validate_required([:title, :description]) + |> cast(attrs, [:title, :description, :document, :user_id]) + |> validate_required([:title, :description, :user_id]) end end diff --git a/lib/cklist_web/controllers/checklist_controller.ex b/lib/cklist_web/controllers/checklist_controller.ex index 4fff10b..65b4b70 100644 --- a/lib/cklist_web/controllers/checklist_controller.ex +++ b/lib/cklist_web/controllers/checklist_controller.ex @@ -15,6 +15,8 @@ defmodule CklistWeb.ChecklistController do end def create(conn, %{"checklist" => checklist_params}) do + checklist_params = Map.put(checklist_params, "user_id", conn.assigns.current_user.id) + case Checklists.create_checklist(checklist_params) do {:ok, checklist} -> conn From 5a9741ea8d80d5a4e026aa28d04b1efcc7a4b64f Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Mon, 5 Feb 2024 21:12:24 +0100 Subject: [PATCH 12/42] WIP: also save some standard document --- lib/cklist/checklists/checklist.ex | 2 +- lib/cklist_web/controllers/checklist_controller.ex | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/cklist/checklists/checklist.ex b/lib/cklist/checklists/checklist.ex index 2e44ce7..8c2064d 100644 --- a/lib/cklist/checklists/checklist.ex +++ b/lib/cklist/checklists/checklist.ex @@ -15,6 +15,6 @@ defmodule Cklist.Checklists.Checklist do def changeset(checklist, attrs) do checklist |> cast(attrs, [:title, :description, :document, :user_id]) - |> validate_required([:title, :description, :user_id]) + |> validate_required([:title, :description, :document, :user_id]) end end diff --git a/lib/cklist_web/controllers/checklist_controller.ex b/lib/cklist_web/controllers/checklist_controller.ex index 65b4b70..4d2e039 100644 --- a/lib/cklist_web/controllers/checklist_controller.ex +++ b/lib/cklist_web/controllers/checklist_controller.ex @@ -16,6 +16,13 @@ defmodule CklistWeb.ChecklistController do def create(conn, %{"checklist" => checklist_params}) do checklist_params = Map.put(checklist_params, "user_id", conn.assigns.current_user.id) + checklist_params = Map.put(checklist_params, "document", %{ + version: "0.1", + sequential: false, + steps: [ + %{ name: "one thing" }, + %{ name: "that other thing" } + ]}) case Checklists.create_checklist(checklist_params) do {:ok, checklist} -> From 51973eb7b1e0fb09ad9f6afdff229184e54d0f78 Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Mon, 5 Feb 2024 21:47:38 +0100 Subject: [PATCH 13/42] Show all properties of the checklist --- .../controllers/checklist_html/index.html.heex | 1 - lib/cklist_web/controllers/checklist_html/show.html.heex | 9 ++++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/cklist_web/controllers/checklist_html/index.html.heex b/lib/cklist_web/controllers/checklist_html/index.html.heex index aaf88c0..99fc7e0 100644 --- a/lib/cklist_web/controllers/checklist_html/index.html.heex +++ b/lib/cklist_web/controllers/checklist_html/index.html.heex @@ -10,7 +10,6 @@ <.table id="checklists" rows={@checklists} row_click={&JS.navigate(~p"/checklists/#{&1}")}> <:col :let={checklist} label="Title"><%= checklist.title %> <:col :let={checklist} label="Description"><%= checklist.description %> - <:col :let={checklist} label="Document"><%= checklist.document %> <:action :let={checklist}>
<.link navigate={~p"/checklists/#{checklist}"}>Show diff --git a/lib/cklist_web/controllers/checklist_html/show.html.heex b/lib/cklist_web/controllers/checklist_html/show.html.heex index e73f1f0..70f20e2 100644 --- a/lib/cklist_web/controllers/checklist_html/show.html.heex +++ b/lib/cklist_web/controllers/checklist_html/show.html.heex @@ -11,7 +11,14 @@ <.list> <:item title="Title"><%= @checklist.title %> <:item title="Description"><%= @checklist.description %> - <:item title="Document"><%= "#{inspect @checklist.document}" %> + <:item title="Sequential"><%= @checklist.document["sequential"] %> + <:item title="Steps"> +
    + <%= for step <- @checklist.document["steps"] do %> +
  • <%= step["name"] %>
  • + <% end %> +
+ <.back navigate={~p"/checklists"}>Back to checklists From cc80fe90aae89e1b3eb68a1fc90b9589a95cdeb2 Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Mon, 5 Feb 2024 23:04:40 +0100 Subject: [PATCH 14/42] WIP: Add buttons to run / edit / delete checklists --- lib/cklist_web/components/core_components.ex | 2 +- .../controllers/checklist_html/index.html.heex | 11 +++++++++-- priv/static/images/delete.svg | 1 + priv/static/images/edit.svg | 1 + priv/static/images/play.svg | 1 + 5 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 priv/static/images/delete.svg create mode 100644 priv/static/images/edit.svg create mode 100644 priv/static/images/play.svg diff --git a/lib/cklist_web/components/core_components.ex b/lib/cklist_web/components/core_components.ex index eed9736..f6d94cb 100644 --- a/lib/cklist_web/components/core_components.ex +++ b/lib/cklist_web/components/core_components.ex @@ -501,7 +501,7 @@ defmodule CklistWeb.CoreComponents do
- +
<.link navigate={~p"/checklists/#{checklist}"}>Show
- <.link navigate={~p"/checklists/#{checklist}/edit"}>Edit + <.link navigate={~p"/checklists/#{checklist}/run"}> + Run this checklist + + + <:action :let={checklist}> + <.link navigate={~p"/checklists/#{checklist}/edit"}> + Edit this checklist + <:action :let={checklist}> <.link href={~p"/checklists/#{checklist}"} method="delete" data-confirm="Are you sure?"> - Delete + Delete this checklist diff --git a/priv/static/images/delete.svg b/priv/static/images/delete.svg new file mode 100644 index 0000000..be5bb7d --- /dev/null +++ b/priv/static/images/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/priv/static/images/edit.svg b/priv/static/images/edit.svg new file mode 100644 index 0000000..56d9fc0 --- /dev/null +++ b/priv/static/images/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/priv/static/images/play.svg b/priv/static/images/play.svg new file mode 100644 index 0000000..f86f45f --- /dev/null +++ b/priv/static/images/play.svg @@ -0,0 +1 @@ + \ No newline at end of file From 6a2a20b14b0e599e6594fafe9b88275ac0a420ac Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 11 Feb 2024 18:22:49 +0100 Subject: [PATCH 15/42] Add access property on checklist Checklists are personal by default and onyl show up publicly (e.g. when not signed it) or for other signed in users once made public. --- lib/cklist/checklists.ex | 10 ++++-- lib/cklist/checklists/checklist.ex | 5 +-- .../controllers/checklist_controller.ex | 2 +- .../checklist_html/checklist_form.html.heex | 1 + .../controllers/checklist_html/show.html.heex | 1 + .../20240211152500_add_access_property.exs | 9 +++++ test/cklist/checklists_test.exs | 10 +++--- .../controllers/checklist_controller_test.exs | 12 +++++-- test/support/fixtures/checklists_fixtures.ex | 34 ++++++++++++++++++- 9 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 priv/repo/migrations/20240211152500_add_access_property.exs diff --git a/lib/cklist/checklists.ex b/lib/cklist/checklists.ex index 08d87b0..12e315f 100644 --- a/lib/cklist/checklists.ex +++ b/lib/cklist/checklists.ex @@ -17,9 +17,15 @@ defmodule Cklist.Checklists do [%Checklist{}, ...] """ - def list_checklists do - Repo.all(Checklist) + def list_checklists(nil) do + query = from l in Checklist, where: l.access == :public + Repo.all(query) end + def list_checklists(user) do + query = from l in Checklist, where: l.user_id == ^user.id or l.access == :public + Repo.all(query) + end + @doc """ Gets a single checklist. diff --git a/lib/cklist/checklists/checklist.ex b/lib/cklist/checklists/checklist.ex index 8c2064d..7f86ecc 100644 --- a/lib/cklist/checklists/checklist.ex +++ b/lib/cklist/checklists/checklist.ex @@ -7,6 +7,7 @@ defmodule Cklist.Checklists.Checklist do field :title, :string field :document, :map field :user_id, :id + field :access, Ecto.Enum, values: [:public, :personal] timestamps(type: :utc_datetime) end @@ -14,7 +15,7 @@ defmodule Cklist.Checklists.Checklist do @doc false def changeset(checklist, attrs) do checklist - |> cast(attrs, [:title, :description, :document, :user_id]) - |> validate_required([:title, :description, :document, :user_id]) + |> cast(attrs, [:title, :description, :document, :user_id, :access]) + |> validate_required([:title, :description, :document, :user_id, :access]) end end diff --git a/lib/cklist_web/controllers/checklist_controller.ex b/lib/cklist_web/controllers/checklist_controller.ex index 4d2e039..d5180e8 100644 --- a/lib/cklist_web/controllers/checklist_controller.ex +++ b/lib/cklist_web/controllers/checklist_controller.ex @@ -5,7 +5,7 @@ defmodule CklistWeb.ChecklistController do alias Cklist.Checklists.Checklist def index(conn, _params) do - checklists = Checklists.list_checklists() + checklists = Checklists.list_checklists(conn.assigns.current_user) render(conn, :index, checklists: checklists) end diff --git a/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex b/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex index 8e878a4..7ee5446 100644 --- a/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex +++ b/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex @@ -4,6 +4,7 @@ <.input field={f[:title]} type="text" label="Title" /> <.input field={f[:description]} type="text" label="Description" /> + <.input field={f[:access]} type="select" options={["Personal": "personal", "Public": "public"]} label="Access" /> <:actions> <.button>Save Checklist diff --git a/lib/cklist_web/controllers/checklist_html/show.html.heex b/lib/cklist_web/controllers/checklist_html/show.html.heex index 70f20e2..07d04e6 100644 --- a/lib/cklist_web/controllers/checklist_html/show.html.heex +++ b/lib/cklist_web/controllers/checklist_html/show.html.heex @@ -11,6 +11,7 @@ <.list> <:item title="Title"><%= @checklist.title %> <:item title="Description"><%= @checklist.description %> + <:item title="Access"><%= @checklist.access %> <:item title="Sequential"><%= @checklist.document["sequential"] %> <:item title="Steps">
    diff --git a/priv/repo/migrations/20240211152500_add_access_property.exs b/priv/repo/migrations/20240211152500_add_access_property.exs new file mode 100644 index 0000000..0de09e9 --- /dev/null +++ b/priv/repo/migrations/20240211152500_add_access_property.exs @@ -0,0 +1,9 @@ +defmodule Cklist.Repo.Migrations.AddAccessProperty do + use Ecto.Migration + + def change do + alter table("checklists") do + add :access, :string + end + end +end diff --git a/test/cklist/checklists_test.exs b/test/cklist/checklists_test.exs index 1c0d643..f5d5824 100644 --- a/test/cklist/checklists_test.exs +++ b/test/cklist/checklists_test.exs @@ -7,12 +7,14 @@ defmodule Cklist.ChecklistsTest do alias Cklist.Checklists.Checklist import Cklist.ChecklistsFixtures + import Cklist.AccountsFixtures @invalid_attrs %{description: nil, title: nil, document: nil} - test "list_checklists/0 returns all checklists" do - checklist = checklist_fixture() - assert Checklists.list_checklists() == [checklist] + test "list_checklists/1 returns all checklists except private ones of other users" do + id = user_fixture().id + checklists = multiple_checklist_fixture(%{id1: id, id2: user_fixture().id}) + assert Checklists.list_checklists(%{id: id}) == checklists end test "get_checklist!/1 returns the checklist with given id" do @@ -21,7 +23,7 @@ defmodule Cklist.ChecklistsTest do end test "create_checklist/1 with valid data creates a checklist" do - valid_attrs = %{description: "some description", title: "some title", document: %{}} + valid_attrs = %{description: "some description", title: "some title", user_id: 1, access: "personal", document: %{}} assert {:ok, %Checklist{} = checklist} = Checklists.create_checklist(valid_attrs) assert checklist.description == "some description" diff --git a/test/cklist_web/controllers/checklist_controller_test.exs b/test/cklist_web/controllers/checklist_controller_test.exs index 3d7595b..5a2b0be 100644 --- a/test/cklist_web/controllers/checklist_controller_test.exs +++ b/test/cklist_web/controllers/checklist_controller_test.exs @@ -4,11 +4,19 @@ defmodule CklistWeb.ChecklistControllerTest do import Cklist.ChecklistsFixtures import Cklist.AccountsFixtures - @create_attrs %{description: "some description", title: "some title", document: %{}} + @create_attrs %{ + description: "some description", + title: "some title", + document: %{}, + user_id: 1, + access: "personal" + } @update_attrs %{ description: "some updated description", title: "some updated title", - document: %{} + document: %{}, + user_id: 1, + access: "personal" } @invalid_attrs %{description: nil, title: nil, document: nil} diff --git a/test/support/fixtures/checklists_fixtures.ex b/test/support/fixtures/checklists_fixtures.ex index aed7ae5..224099c 100644 --- a/test/support/fixtures/checklists_fixtures.ex +++ b/test/support/fixtures/checklists_fixtures.ex @@ -13,10 +13,42 @@ defmodule Cklist.ChecklistsFixtures do |> Enum.into(%{ description: "some description", document: %{}, - title: "some title" + title: "some title", + access: "personal", + user_id: 1 }) |> Cklist.Checklists.create_checklist() checklist end + + @doc """ + Generates multiple checklists of different users. + """ + def multiple_checklist_fixture(users) do + attrs = %{} + {:ok, checklist1} = + attrs + |> Enum.into(%{ + description: "some description", + document: %{}, + title: "some title", + access: "personal", + user_id: users.id1 + }) + |> Cklist.Checklists.create_checklist() + + {:ok, checklist2} = + attrs + |> Enum.into(%{ + description: "some description", + document: %{}, + title: "some other title", + access: "public", + user_id: users.id2 + }) + |> Cklist.Checklists.create_checklist() + + [checklist1, checklist2] + end end From 6bbaae20f23d2fcf0022a859754526f7625d55a6 Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 18 Feb 2024 17:27:59 +0100 Subject: [PATCH 16/42] WIP: Run simple, non-sequential cheklists --- lib/cklist_web.ex | 1 + lib/cklist_web/components/my_components.ex | 26 +++++++++ .../checklist_html/index.html.heex | 6 +-- .../controllers/checklist_html/show.html.heex | 13 ++++- lib/cklist_web/live/cklist_run_live.ex | 54 +++++++++++++++++++ lib/cklist_web/router.ex | 2 + 6 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 lib/cklist_web/components/my_components.ex create mode 100644 lib/cklist_web/live/cklist_run_live.ex diff --git a/lib/cklist_web.ex b/lib/cklist_web.ex index 1c5c75b..3351dce 100644 --- a/lib/cklist_web.ex +++ b/lib/cklist_web.ex @@ -85,6 +85,7 @@ defmodule CklistWeb do import Phoenix.HTML # Core UI components and translation import CklistWeb.CoreComponents + import CklistWeb.MyComponents import Cklist.Gettext # Shortcut for generating JS commands diff --git a/lib/cklist_web/components/my_components.ex b/lib/cklist_web/components/my_components.ex new file mode 100644 index 0000000..6aba4dc --- /dev/null +++ b/lib/cklist_web/components/my_components.ex @@ -0,0 +1,26 @@ +defmodule CklistWeb.MyComponents do + @moduledoc """ + Provides custom UI components for the CKlist project. + """ + + use Phoenix.Component + + @doc """ + Renders a progress bar. + + ## Example + + <.progress percent="45" /> + """ + attr :done, :integer, required: true + attr :steps, :integer, required: true + + def progress(assigns) do + ~H""" + <%= @done %> / <%= @steps %> +
    +
    +
    + """ + end +end diff --git a/lib/cklist_web/controllers/checklist_html/index.html.heex b/lib/cklist_web/controllers/checklist_html/index.html.heex index 5169296..f9df7b4 100644 --- a/lib/cklist_web/controllers/checklist_html/index.html.heex +++ b/lib/cklist_web/controllers/checklist_html/index.html.heex @@ -15,17 +15,17 @@ <.link navigate={~p"/checklists/#{checklist}"}>Show
<.link navigate={~p"/checklists/#{checklist}/run"}> - Run this checklist + Run this checklist <:action :let={checklist}> <.link navigate={~p"/checklists/#{checklist}/edit"}> - Edit this checklist + Edit this checklist <:action :let={checklist}> <.link href={~p"/checklists/#{checklist}"} method="delete" data-confirm="Are you sure?"> - Delete this checklist + Delete this checklist diff --git a/lib/cklist_web/controllers/checklist_html/show.html.heex b/lib/cklist_web/controllers/checklist_html/show.html.heex index 07d04e6..c7b1107 100644 --- a/lib/cklist_web/controllers/checklist_html/show.html.heex +++ b/lib/cklist_web/controllers/checklist_html/show.html.heex @@ -2,8 +2,19 @@ Checklist <%= @checklist.id %> <:subtitle>This is a checklist record from your database. <:actions> + <.link href={~p"/checklists/#{@checklist}/run"}> + <.button> + Run +   + Run this checklist + + <.link href={~p"/checklists/#{@checklist}/edit"}> - <.button>Edit checklist + <.button> + Edit +   + Run this checklist + diff --git a/lib/cklist_web/live/cklist_run_live.ex b/lib/cklist_web/live/cklist_run_live.ex new file mode 100644 index 0000000..60f1c7f --- /dev/null +++ b/lib/cklist_web/live/cklist_run_live.ex @@ -0,0 +1,54 @@ +defmodule CklistWeb.CklistRunLive do + use CklistWeb, :live_view + + alias Cklist.Checklists + + def render(assigns) do + case assigns.checklist.document do + %{"version" => "0.1", "sequential" => true} -> render_sequential(assigns) + %{"version" => "0.1", "sequential" => false} -> render_unordered(assigns) + end + end + + def render_sequential(assigns) do + ~H""" + <.header> + Checklist <%= @checklist.id %> + <:subtitle>This renders a sequential checklist. + + """ + end + + def render_unordered(assigns) do + ~H""" + <.header> + <%= @checklist.title %> + <:subtitle><%= @checklist.description %> + + +

 

+ <.progress steps={@steps} done={@done} /> + +
+ <%= for step <- @checklist.document["steps"] do %> + <.input type="checkbox" label={step["name"]} name={step["name"]} phx-change="step_done" /> + <% end %> +
+ + <.back navigate={~p"/checklists/#{@checklist}"}>Back to checklist + """ + end + + def mount(%{"id" => id}, _session, socket) do + checklist = Checklists.get_checklist!(id) + socket = socket + |> assign(:checklist, checklist) + |> assign(:steps, length(checklist.document["steps"])) + |> assign(:done, 0) + {:ok, socket} + end + + def handle_event("step_done", _params, socket) do + {:noreply, update(socket, :done, &(&1 + 1))} + end +end diff --git a/lib/cklist_web/router.ex b/lib/cklist_web/router.ex index 983b1f7..6df5810 100644 --- a/lib/cklist_web/router.ex +++ b/lib/cklist_web/router.ex @@ -73,6 +73,8 @@ defmodule CklistWeb.Router do on_mount: [{CklistWeb.UserAuth, :ensure_authenticated}] do live "/user/settings", UserSettingsLive, :edit live "/user/settings/confirm_email/:token", UserSettingsLive, :confirm_email + + live "/checklists/:id/run", CklistRunLive end resources "/checklists", ChecklistController, except: [:index] From d18a1d0547c8320ade0cd2e8f500fe44a335aa47 Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 25 Feb 2024 21:22:12 +0100 Subject: [PATCH 17/42] Track state of checked tasks --- lib/cklist_web/components/my_components.ex | 13 +++-- .../checklist_html/checklist_form.html.heex | 2 +- .../controllers/checklist_html/run.html.heex | 28 +++++++++++ lib/cklist_web/live/cklist_run_live.ex | 50 ++++++------------- 4 files changed, 50 insertions(+), 43 deletions(-) create mode 100644 lib/cklist_web/controllers/checklist_html/run.html.heex diff --git a/lib/cklist_web/components/my_components.ex b/lib/cklist_web/components/my_components.ex index 6aba4dc..f48b758 100644 --- a/lib/cklist_web/components/my_components.ex +++ b/lib/cklist_web/components/my_components.ex @@ -1,6 +1,6 @@ defmodule CklistWeb.MyComponents do @moduledoc """ - Provides custom UI components for the CKlist project. + Provides custom UI components for the Cklist project. """ use Phoenix.Component @@ -10,16 +10,15 @@ defmodule CklistWeb.MyComponents do ## Example - <.progress percent="45" /> + <.progress_bar steps=5 done=2 /> """ - attr :done, :integer, required: true attr :steps, :integer, required: true + attr :done, :integer, required: true - def progress(assigns) do + def progress_bar(assigns) do ~H""" - <%= @done %> / <%= @steps %> -
-
+
+
""" end diff --git a/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex b/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex index 7ee5446..19df9ab 100644 --- a/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex +++ b/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex @@ -4,7 +4,7 @@ <.input field={f[:title]} type="text" label="Title" /> <.input field={f[:description]} type="text" label="Description" /> - <.input field={f[:access]} type="select" options={["Personal": "personal", "Public": "public"]} label="Access" /> + <.input field={f[:access]} type="select" options={[Personal: "personal", Public: "public"]} label="Access" /> <:actions> <.button>Save Checklist diff --git a/lib/cklist_web/controllers/checklist_html/run.html.heex b/lib/cklist_web/controllers/checklist_html/run.html.heex new file mode 100644 index 0000000..46e3c14 --- /dev/null +++ b/lib/cklist_web/controllers/checklist_html/run.html.heex @@ -0,0 +1,28 @@ +<.header> + <%= @checklist.title %> + <:subtitle><%= @checklist.description %> + + +<.progress_bar steps={@steps} done={@done} /> + +<%= if @checklist.document["sequential"] == false do %> + +
+ <.input + :for={step <- @checklist.document["steps"]} + phx-change="step_done" + checked={Map.get(@step_state, step["name"], false)} + id={step["name"]} + type="checkbox" label={step["name"]} + name={step["name"]} + /> +
+ +<% else %> + +This one is sequential. + +<% end %> + +<.back navigate={~p"/checklists/#{@checklist}"}>Back to checklist + \ No newline at end of file diff --git a/lib/cklist_web/live/cklist_run_live.ex b/lib/cklist_web/live/cklist_run_live.ex index 60f1c7f..6cc978b 100644 --- a/lib/cklist_web/live/cklist_run_live.ex +++ b/lib/cklist_web/live/cklist_run_live.ex @@ -4,39 +4,7 @@ defmodule CklistWeb.CklistRunLive do alias Cklist.Checklists def render(assigns) do - case assigns.checklist.document do - %{"version" => "0.1", "sequential" => true} -> render_sequential(assigns) - %{"version" => "0.1", "sequential" => false} -> render_unordered(assigns) - end - end - - def render_sequential(assigns) do - ~H""" - <.header> - Checklist <%= @checklist.id %> - <:subtitle>This renders a sequential checklist. - - """ - end - - def render_unordered(assigns) do - ~H""" - <.header> - <%= @checklist.title %> - <:subtitle><%= @checklist.description %> - - -

 

- <.progress steps={@steps} done={@done} /> - -
- <%= for step <- @checklist.document["steps"] do %> - <.input type="checkbox" label={step["name"]} name={step["name"]} phx-change="step_done" /> - <% end %> -
- - <.back navigate={~p"/checklists/#{@checklist}"}>Back to checklist - """ + CklistWeb.ChecklistHTML.run(assigns) end def mount(%{"id" => id}, _session, socket) do @@ -45,10 +13,22 @@ defmodule CklistWeb.CklistRunLive do |> assign(:checklist, checklist) |> assign(:steps, length(checklist.document["steps"])) |> assign(:done, 0) + |> assign(:step_state, %{}) {:ok, socket} end - def handle_event("step_done", _params, socket) do - {:noreply, update(socket, :done, &(&1 + 1))} + def handle_event("step_done", params, %{assigns: assigns} = socket) do + [step_name] = params["_target"] + updated_state = Map.put(assigns.step_state, step_name, Map.get(params, step_name) == "true") + + { + :noreply, + socket + |> assign(:step_state, updated_state) + |> assign(:done, Enum.reduce(updated_state, 0, &is_done/2)) + } end + + defp is_done({_, true}, count), do: count + 1 + defp is_done(_, count), do: count end From 8948f5cee019b1d95b5c8078c1d6b8b4242b41ff Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 25 Feb 2024 22:18:15 +0100 Subject: [PATCH 18/42] Completed checklists don't change anymore --- lib/cklist_web/components/my_components.ex | 2 +- .../controllers/checklist_html/run.html.heex | 38 +++++++++++++------ lib/cklist_web/live/cklist_run_live.ex | 9 ++++- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/lib/cklist_web/components/my_components.ex b/lib/cklist_web/components/my_components.ex index f48b758..cac4e40 100644 --- a/lib/cklist_web/components/my_components.ex +++ b/lib/cklist_web/components/my_components.ex @@ -17,7 +17,7 @@ defmodule CklistWeb.MyComponents do def progress_bar(assigns) do ~H""" -
+
""" diff --git a/lib/cklist_web/controllers/checklist_html/run.html.heex b/lib/cklist_web/controllers/checklist_html/run.html.heex index 46e3c14..d62e090 100644 --- a/lib/cklist_web/controllers/checklist_html/run.html.heex +++ b/lib/cklist_web/controllers/checklist_html/run.html.heex @@ -3,20 +3,36 @@ <:subtitle><%= @checklist.description %> -<.progress_bar steps={@steps} done={@done} /> +<.progress_bar steps={@steps} done={@steps_done} /> <%= if @checklist.document["sequential"] == false do %> -
- <.input - :for={step <- @checklist.document["steps"]} - phx-change="step_done" - checked={Map.get(@step_state, step["name"], false)} - id={step["name"]} - type="checkbox" label={step["name"]} - name={step["name"]} - /> -
+
+ <.input + :for={step <- @checklist.document["steps"]} + phx-change="step_done" + checked={Map.get(@step_state, step["name"], false)} + id={step["name"]} + type="checkbox" label={step["name"]} + name={step["name"]} + disabled={@completed} + /> +
+ +
+ +
+ +
+

Congratulations! 🎉

+

You finished the checklist.

+
<% else %> diff --git a/lib/cklist_web/live/cklist_run_live.ex b/lib/cklist_web/live/cklist_run_live.ex index 6cc978b..0c47a52 100644 --- a/lib/cklist_web/live/cklist_run_live.ex +++ b/lib/cklist_web/live/cklist_run_live.ex @@ -12,11 +12,16 @@ defmodule CklistWeb.CklistRunLive do socket = socket |> assign(:checklist, checklist) |> assign(:steps, length(checklist.document["steps"])) - |> assign(:done, 0) + |> assign(:steps_done, 0) + |> assign(:completed, false) |> assign(:step_state, %{}) {:ok, socket} end + def handle_event("completed", _params, socket) do + { :noreply, assign(socket, :completed, true) } + end + def handle_event("step_done", params, %{assigns: assigns} = socket) do [step_name] = params["_target"] updated_state = Map.put(assigns.step_state, step_name, Map.get(params, step_name) == "true") @@ -25,7 +30,7 @@ defmodule CklistWeb.CklistRunLive do :noreply, socket |> assign(:step_state, updated_state) - |> assign(:done, Enum.reduce(updated_state, 0, &is_done/2)) + |> assign(:steps_done, Enum.reduce(updated_state, 0, &is_done/2)) } end From a47a568b6c87975e3876025425ddbc80a49faf01 Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 25 Feb 2024 22:26:37 +0100 Subject: [PATCH 19/42] Generated checklist are now sequential --- lib/cklist_web/controllers/checklist_controller.ex | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/cklist_web/controllers/checklist_controller.ex b/lib/cklist_web/controllers/checklist_controller.ex index d5180e8..8ced794 100644 --- a/lib/cklist_web/controllers/checklist_controller.ex +++ b/lib/cklist_web/controllers/checklist_controller.ex @@ -18,10 +18,11 @@ defmodule CklistWeb.ChecklistController do checklist_params = Map.put(checklist_params, "user_id", conn.assigns.current_user.id) checklist_params = Map.put(checklist_params, "document", %{ version: "0.1", - sequential: false, + sequential: true, steps: [ - %{ name: "one thing" }, - %{ name: "that other thing" } + %{ name: "first thing" }, + %{ name: "second thing" }, + %{ name: "third thing" }, ]}) case Checklists.create_checklist(checklist_params) do From 12d3a728b9c078a23c6a8b6c81069ea8cfc3afad Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Mon, 26 Feb 2024 00:00:39 +0100 Subject: [PATCH 20/42] Simple UI for sequential chcklists --- .../controllers/checklist_html/run.html.heex | 37 ++++++++++++++-- lib/cklist_web/live/cklist_run_live.ex | 43 +++++++++++++++++-- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/lib/cklist_web/controllers/checklist_html/run.html.heex b/lib/cklist_web/controllers/checklist_html/run.html.heex index d62e090..0c95919 100644 --- a/lib/cklist_web/controllers/checklist_html/run.html.heex +++ b/lib/cklist_web/controllers/checklist_html/run.html.heex @@ -19,10 +19,10 @@ /> -
+
+ + +
<% end %> diff --git a/lib/cklist_web/live/cklist_run_live.ex b/lib/cklist_web/live/cklist_run_live.ex index 0c47a52..3e6ec56 100644 --- a/lib/cklist_web/live/cklist_run_live.ex +++ b/lib/cklist_web/live/cklist_run_live.ex @@ -15,11 +15,10 @@ defmodule CklistWeb.CklistRunLive do |> assign(:steps_done, 0) |> assign(:completed, false) |> assign(:step_state, %{}) - {:ok, socket} - end - def handle_event("completed", _params, socket) do - { :noreply, assign(socket, :completed, true) } + |> assign(:current_step, Enum.at(checklist.document["steps"], 0)) + |> assign(:next_step, Enum.at(checklist.document["steps"], 1)) + {:ok, socket} end def handle_event("step_done", params, %{assigns: assigns} = socket) do @@ -34,6 +33,42 @@ defmodule CklistWeb.CklistRunLive do } end + def handle_event("completed", _params, socket) do + { :noreply, assign(socket, :completed, true) } + end + + def handle_event("abort", _params, %{assigns: assigns} = socket) do + { + :noreply, + socket + |> put_flash(:info, "Aborting checklist run") + |> redirect(to: ~p"/checklists/#{assigns.checklist}") + } + end + + def handle_event("next_step", _params, %{assigns: assigns} = socket) do + case assigns.completed do + true -> + { + :noreply, + socket + |> put_flash(:info, "Well done!") + |> redirect(to: ~p"/checklists/#{assigns.checklist}") + } + false -> + updated_steps_done = assigns.steps_done + 1 + completed = assigns.steps === updated_steps_done + { + :noreply, + socket + |> assign(:steps_done, updated_steps_done) + |> assign(:current_step, Enum.at(assigns.checklist.document["steps"], updated_steps_done)) + |> assign(:next_step, Enum.at(assigns.checklist.document["steps"], updated_steps_done + 1)) + |> assign(:completed, completed) + } + end + end + defp is_done({_, true}, count), do: count + 1 defp is_done(_, count), do: count end From 7201568e060d5969a5491bf1387416b178af58c2 Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Mon, 26 Feb 2024 00:25:06 +0100 Subject: [PATCH 21/42] Same look & feel as for sequential checklists --- .../controllers/checklist_html/run.html.heex | 29 +++++++++++-------- lib/cklist_web/live/cklist_run_live.ex | 8 ++--- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/cklist_web/controllers/checklist_html/run.html.heex b/lib/cklist_web/controllers/checklist_html/run.html.heex index 0c95919..fa8a4f7 100644 --- a/lib/cklist_web/controllers/checklist_html/run.html.heex +++ b/lib/cklist_web/controllers/checklist_html/run.html.heex @@ -19,19 +19,26 @@ /> -
+
+

Congratulations! 🎉

+

You finished the checklist.

+
+ +
-
-
-

Congratulations! 🎉

-

You finished the checklist.

+
<% else %> @@ -52,7 +59,7 @@ <% end %>
-
+
""" end + + @doc """ + Renders an abort button. + + ## Example + + <.abort_button hidden={@completed} /> + """ + attr :class, :string, default: nil + attr :hidden, :boolean, default: false + + def abort_button(assigns) do + ~H""" + <.button + class={"bg-gray-300 hover:bg-gray-400 #{if @hidden, do: "hidden", else: ""} #{@class}"} + phx-click="abort" + data-confirm="Are you sure?" + > + Abort + + """ + end + + @doc """ + Renders a button to go to the next step. + + ## Example + + <.next_button hidden={not @completed}> + Awesome! + + """ + attr :class, :string, default: nil + attr :hidden, :boolean, default: false + slot :inner_block, default: "Next" + + def next_button(assigns) do + ~H""" + <.button + class={"bg-lime-500 hover:bg-lime-400 #{if @hidden, do: "hidden", else: ""} #{@class}"} + phx-click="next_step" + > + <%= render_slot(@inner_block) %> + + """ + end + + attr :class, :string, default: nil + attr :rest, :global, doc: "arbitrary HTML attributes to apply to the button tag" + slot :inner_block, required: true + defp button(assigns) do + ~H""" + + """ + end end diff --git a/lib/cklist_web/controllers/checklist_html/run.html.heex b/lib/cklist_web/controllers/checklist_html/run.html.heex index fa8a4f7..b7da4c5 100644 --- a/lib/cklist_web/controllers/checklist_html/run.html.heex +++ b/lib/cklist_web/controllers/checklist_html/run.html.heex @@ -5,8 +5,7 @@ <.progress_bar steps={@steps} done={@steps_done} /> -<%= if @checklist.document["sequential"] == false do %> - +
<.input :for={step <- @checklist.document["steps"]} @@ -25,56 +24,37 @@
- + <.abort_button class="basis-1/3" hidden={@completed} /> - +
+
-<% else %> - -
- <%= if @completed do %> -

Look at all the things you did:

-
    - <%= for step <- @checklist.document["steps"] do %> -
  • <%= step["name"] %>
  • +
    +
    + <%= if @completed do %> +

    Look at all the things you did:

    +
      + <%= for step <- @checklist.document["steps"] do %> +
    • <%= step["name"] %>
    • + <% end %> +
    + <% else %> +

    Current step: <%= @current_step["name"] %>

    + <%= if @next_step do %> +

    Next step: <%= @next_step["name"] %>

    <% end %> -
- <% else %> -

Current step: <%= @current_step["name"] %>

- <%= if @next_step do %> -

Next step: <%= @next_step["name"] %>

<% end %> - <% end %> -
+
-
- +
+ <.abort_button class="basis-1/3" hidden={@completed} /> - + <.next_button class="basis-1/3"> + <%= if @completed, do: "Awesome!", else: "Done" %> + +
- -<% end %> \ No newline at end of file From 3a835e0e422d63c7870bc9a2adaae08a0015ba8b Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Mon, 4 Mar 2024 21:39:06 +0100 Subject: [PATCH 23/42] Use seeds file to pre-populate dev database --- .../controllers/checklist_controller.ex | 10 +-- priv/repo/seeds.exs | 84 ++++++++++++++++++- 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/lib/cklist_web/controllers/checklist_controller.ex b/lib/cklist_web/controllers/checklist_controller.ex index 8ced794..88d8a21 100644 --- a/lib/cklist_web/controllers/checklist_controller.ex +++ b/lib/cklist_web/controllers/checklist_controller.ex @@ -16,14 +16,8 @@ defmodule CklistWeb.ChecklistController do def create(conn, %{"checklist" => checklist_params}) do checklist_params = Map.put(checklist_params, "user_id", conn.assigns.current_user.id) - checklist_params = Map.put(checklist_params, "document", %{ - version: "0.1", - sequential: true, - steps: [ - %{ name: "first thing" }, - %{ name: "second thing" }, - %{ name: "third thing" }, - ]}) + + # TODO: insert document here case Checklists.create_checklist(checklist_params) do {:ok, checklist} -> diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 067eff9..f944f87 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -10,9 +10,89 @@ # We recommend using the bang functions (`insert!`, `update!` # and so on) as they will fail if something goes wrong. -Cklist.Repo.insert!(%Cklist.Accounts.User{ - email: "audry@cklist.org", +alias Cklist.{Repo, Accounts, Checklists} + +roman = Repo.insert!(%Accounts.User{ + email: "roman@cklist.org", password: "romanholiday", hashed_password: Bcrypt.hash_pwd_salt("romanholiday"), confirmed_at: NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) }) + +aaron = Repo.insert!(%Accounts.User{ + email: "aaron@cklist.org", + password: "aaronholiday", + hashed_password: Bcrypt.hash_pwd_salt("aaronholiday"), + confirmed_at: NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) +}) + +Repo.insert!(%Checklists.Checklist{ + title: "First things first!", + description: "Sequential checklist", + access: :public, + user_id: aaron.id, + + document: %{ + version: "0.1", + sequential: true, + steps: [ + %{ name: "First thing" }, + %{ name: "Second thing" }, + %{ name: "Third thing" }, + ] + } +}) + +Repo.insert!(%Checklists.Checklist{ + title: "Routine", + description: "Morning todo list", + access: :personal, + user_id: roman.id, + + document: %{ + version: "0.1", + sequential: false, + steps: [ + %{ name: "Get up" }, + %{ name: "Eat breakfast" }, + %{ name: "Have a shower" }, + %{ name: "Brush teeth" }, + ] + } +}) + +Repo.insert!(%Checklists.Checklist{ + title: "Shopping list", + description: "All you need for carbonara", + access: :personal, + user_id: roman.id, + + document: %{ + version: "0.1", + sequential: false, + steps: [ + %{ name: "Guanciale" }, + %{ name: "Pecorino romano" }, + %{ name: "Eggs" }, + %{ name: "Spaghetti" }, + %{ name: "Salt" }, + %{ name: "Pepper" }, + ] + } +}) + +Repo.insert!(%Checklists.Checklist{ + title: "To do list", + description: "Things to do", + access: :public, + user_id: aaron.id, + + document: %{ + version: "0.1", + sequential: false, + steps: [ + %{ name: "One thing" }, + %{ name: "That other thing" }, + ] + } +}) From 5dcdbabb5d8d97e3a07b8f7cab3b2b1285e7af7f Mon Sep 17 00:00:00 2001 From: Roman Cattaneo Date: Sun, 10 Mar 2024 23:26:42 +0100 Subject: [PATCH 24/42] Log to database (#24) * WIP: notes where to log to database * user has many cklists and cklists belong to a user * Add tables for checklist runs and activities * Working checklist runs & activity logs --------- Co-authored-by: Roman Cattaneo <> --- lib/cklist/accounts/user.ex | 2 + lib/cklist/checklists.ex | 36 ++++++++++++- lib/cklist/checklists/activity.ex | 22 ++++++++ lib/cklist/checklists/checklist.ex | 4 +- lib/cklist/checklists/run.ex | 16 ++++++ lib/cklist_web/live/cklist_run_live.ex | 52 +++++++++++++------ .../20240310152508_create_checklist_run.exs | 9 ++++ .../20240310153254_create_activity_log.exs | 14 +++++ 8 files changed, 137 insertions(+), 18 deletions(-) create mode 100644 lib/cklist/checklists/activity.ex create mode 100644 lib/cklist/checklists/run.ex create mode 100644 priv/repo/migrations/20240310152508_create_checklist_run.exs create mode 100644 priv/repo/migrations/20240310153254_create_activity_log.exs diff --git a/lib/cklist/accounts/user.ex b/lib/cklist/accounts/user.ex index 9447257..0c42bb7 100644 --- a/lib/cklist/accounts/user.ex +++ b/lib/cklist/accounts/user.ex @@ -8,6 +8,8 @@ defmodule Cklist.Accounts.User do field :hashed_password, :string, redact: true field :confirmed_at, :naive_datetime + has_many :checklists, Cklist.Checklists.Checklist + timestamps(type: :utc_datetime) end diff --git a/lib/cklist/checklists.ex b/lib/cklist/checklists.ex index 12e315f..ca8207b 100644 --- a/lib/cklist/checklists.ex +++ b/lib/cklist/checklists.ex @@ -4,9 +4,10 @@ defmodule Cklist.Checklists do """ import Ecto.Query, warn: false - alias Cklist.Repo - + alias Cklist.Checklists.Run + alias Cklist.Checklists.Activity alias Cklist.Checklists.Checklist + alias Cklist.Repo @doc """ Returns the list of checklists. @@ -107,4 +108,35 @@ defmodule Cklist.Checklists do def change_checklist(%Checklist{} = checklist, attrs \\ %{}) do Checklist.changeset(checklist, attrs) end + + def log_run_start(checklist, user) do + {:ok, run} = %Run{} + |> Run.changeset(%{checklist_id: checklist.id}) + |> Repo.insert() + + %Activity{} + |> Activity.changeset(%{run_id: run.id, user_id: user.id, event: %{type: "checklist_start"}}) + |> Repo.insert() + + # return current run + run + end + + def log_run_abort(run, user) do + %Activity{} + |> Activity.changeset(%{run_id: run.id, user_id: user.id, event: %{type: "checklist_abort"}}) + |> Repo.insert() + end + + def log_run_complete(run, user) do + %Activity{} + |> Activity.changeset(%{run_id: run.id, user_id: user.id, event: %{type: "checklist_complete"}}) + |> Repo.insert() + end + + def log_step_complete(run, user, step_id, is_done \\ true) do + %Activity{} + |> Activity.changeset(%{run_id: run.id, user_id: user.id, event: %{type: "step_done", step_id: step_id, done: is_done}}) + |> Repo.insert() + end end diff --git a/lib/cklist/checklists/activity.ex b/lib/cklist/checklists/activity.ex new file mode 100644 index 0000000..2923100 --- /dev/null +++ b/lib/cklist/checklists/activity.ex @@ -0,0 +1,22 @@ +defmodule Cklist.Checklists.Activity do + use Ecto.Schema + import Ecto.Changeset + + schema "activity" do + # We assume events have a type. Any other properties are optional and type-dependent. + field :event, :map + + belongs_to :run, Cklist.Checklists.Run + belongs_to :user, Cklist.Accounts.User + + # We don't expect activities to be modified. + timestamps([type: :utc_datetime, updated_at: false]) + end + + @doc false + def changeset(activity, attrs) do + activity + |> cast(attrs, [:event, :run_id, :user_id]) + |> validate_required([:event, :run_id, :user_id]) + end +end diff --git a/lib/cklist/checklists/checklist.ex b/lib/cklist/checklists/checklist.ex index 7f86ecc..3b618d3 100644 --- a/lib/cklist/checklists/checklist.ex +++ b/lib/cklist/checklists/checklist.ex @@ -6,9 +6,11 @@ defmodule Cklist.Checklists.Checklist do field :description, :string field :title, :string field :document, :map - field :user_id, :id field :access, Ecto.Enum, values: [:public, :personal] + belongs_to :user, Cklist.Accounts.User + has_many :runs, Cklist.Checklists.Run + timestamps(type: :utc_datetime) end diff --git a/lib/cklist/checklists/run.ex b/lib/cklist/checklists/run.ex new file mode 100644 index 0000000..7b22ada --- /dev/null +++ b/lib/cklist/checklists/run.ex @@ -0,0 +1,16 @@ +defmodule Cklist.Checklists.Run do + use Ecto.Schema + import Ecto.Changeset + + schema "runs" do + belongs_to :checklist, Cklist.Checklists.Checklist + has_many :activities, Cklist.Checklists.Activity + end + + @doc false + def changeset(run, attrs) do + run + |> cast(attrs, [:checklist_id]) + |> validate_required([:checklist_id]) + end +end diff --git a/lib/cklist_web/live/cklist_run_live.ex b/lib/cklist_web/live/cklist_run_live.ex index 02c528a..2d10f18 100644 --- a/lib/cklist_web/live/cklist_run_live.ex +++ b/lib/cklist_web/live/cklist_run_live.ex @@ -8,9 +8,14 @@ defmodule CklistWeb.CklistRunLive do end def mount(%{"id" => id}, _session, socket) do + user = socket.assigns.current_user + checklist = Checklists.get_checklist!(id) + run = Checklists.log_run_start(checklist, user) + socket = socket |> assign(:checklist, checklist) + |> assign(:run, run) |> assign(:steps, length(checklist.document["steps"])) |> assign(:steps_done, 0) |> assign(:completed, false) @@ -21,21 +26,9 @@ defmodule CklistWeb.CklistRunLive do {:ok, socket} end - def handle_event("step_done", params, %{assigns: assigns} = socket) do - [step_name] = params["_target"] - updated_state = Map.put(assigns.step_state, step_name, Map.get(params, step_name) == "true") - updated_steps_done = Enum.reduce(updated_state, 0, &is_done/2) - - { - :noreply, - socket - |> assign(:step_state, updated_state) - |> assign(:steps_done, updated_steps_done) - |> assign(:completed, updated_steps_done === assigns.steps) - } - end - + # Handles aborting a checklist. def handle_event("abort", _params, %{assigns: assigns} = socket) do + Checklists.log_run_abort(assigns.run, assigns.current_user) { :noreply, socket @@ -44,6 +37,7 @@ defmodule CklistWeb.CklistRunLive do } end + # Handles "next_step" event for sequential checklists. def handle_event("next_step", _params, %{assigns: assigns} = socket) do case assigns.completed do true -> @@ -54,19 +48,47 @@ defmodule CklistWeb.CklistRunLive do |> redirect(to: ~p"/checklists/#{assigns.checklist}") } false -> + Checklists.log_step_complete(assigns.run, assigns.current_user, assigns.steps_done) updated_steps_done = assigns.steps_done + 1 completed = assigns.steps === updated_steps_done + if completed do + Checklists.log_run_complete(assigns.run, assigns.current_user) + end { :noreply, socket - |> assign(:steps_done, updated_steps_done) |> assign(:current_step, Enum.at(assigns.checklist.document["steps"], updated_steps_done)) |> assign(:next_step, Enum.at(assigns.checklist.document["steps"], updated_steps_done + 1)) + |> assign(:steps_done, updated_steps_done) |> assign(:completed, completed) } end end + # Handles "step_done" events for non-sequential checklists. + def handle_event("step_done", params, %{assigns: assigns} = socket) do + [step_name] = params["_target"] + is_done = Map.get(params, step_name) == "true" + updated_state = Map.put(assigns.step_state, step_name, is_done) + updated_steps_done = Enum.reduce(updated_state, 0, &is_done/2) + + step_id = Enum.find_index(assigns.checklist.document["steps"], &(&1["name"] == step_name)) + Checklists.log_step_complete(assigns.run, assigns.current_user, step_id, is_done) + + completed = updated_steps_done === assigns.steps + if completed do + Checklists.log_run_complete(assigns.run, assigns.current_user) + end + + { + :noreply, + socket + |> assign(:step_state, updated_state) + |> assign(:steps_done, updated_steps_done) + |> assign(:completed, completed) + } + end + defp is_done({_, true}, count), do: count + 1 defp is_done(_, count), do: count end diff --git a/priv/repo/migrations/20240310152508_create_checklist_run.exs b/priv/repo/migrations/20240310152508_create_checklist_run.exs new file mode 100644 index 0000000..55b40cb --- /dev/null +++ b/priv/repo/migrations/20240310152508_create_checklist_run.exs @@ -0,0 +1,9 @@ +defmodule Cklist.Repo.Migrations.CreateChecklistRun do + use Ecto.Migration + + def change do + create table(:runs) do + add :checklist_id, references(:checklists) + end + end +end diff --git a/priv/repo/migrations/20240310153254_create_activity_log.exs b/priv/repo/migrations/20240310153254_create_activity_log.exs new file mode 100644 index 0000000..78af720 --- /dev/null +++ b/priv/repo/migrations/20240310153254_create_activity_log.exs @@ -0,0 +1,14 @@ +defmodule Cklist.Repo.Migrations.CreateActivityLog do + use Ecto.Migration + + def change do + create table(:activity) do + add :event, :map + + add :user_id, references(:users) + add :run_id, references(:runs) + + timestamps([type: :utc_datetime, updated_at: false]) + end + end +end From 16f31bd9cd0a9b8add310dea8cbe8d52b3d0652f Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 17 Mar 2024 21:36:04 +0100 Subject: [PATCH 25/42] WIP: UI to create checklists --- lib/cklist_web/controllers/checklist_controller.ex | 8 ++++++-- .../controllers/checklist_html/checklist_form.html.heex | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/cklist_web/controllers/checklist_controller.ex b/lib/cklist_web/controllers/checklist_controller.ex index 88d8a21..0c5f9d4 100644 --- a/lib/cklist_web/controllers/checklist_controller.ex +++ b/lib/cklist_web/controllers/checklist_controller.ex @@ -15,9 +15,13 @@ defmodule CklistWeb.ChecklistController do end def create(conn, %{"checklist" => checklist_params}) do - checklist_params = Map.put(checklist_params, "user_id", conn.assigns.current_user.id) + data = %{} + |> Map.put("sequential", Map.get(checklist_params, "sequential")) + |> Map.put("steps", Map.get(checklist_params, "steps", [])) - # TODO: insert document here + checklist_params = checklist_params + |> Map.put("user_id", conn.assigns.current_user.id) + |> Map.put("document", data) case Checklists.create_checklist(checklist_params) do {:ok, checklist} -> diff --git a/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex b/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex index 19df9ab..f74ba4e 100644 --- a/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex +++ b/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex @@ -5,6 +5,7 @@ <.input field={f[:title]} type="text" label="Title" /> <.input field={f[:description]} type="text" label="Description" /> <.input field={f[:access]} type="select" options={[Personal: "personal", Public: "public"]} label="Access" /> + <.input field={f[:sequential]} type="select" options={[True: true, False: false]} label="Sequential" /> <:actions> <.button>Save Checklist From a31dbc725cb36634649497132cccde63adf3b51a Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sat, 23 Mar 2024 18:27:41 +0100 Subject: [PATCH 26/42] Move new cklist to live view --- .../controllers/checklist_controller.ex | 6 ------ lib/cklist_web/live/cklist_new_live.ex | 17 +++++++++++++++++ lib/cklist_web/router.ex | 3 ++- 3 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 lib/cklist_web/live/cklist_new_live.ex diff --git a/lib/cklist_web/controllers/checklist_controller.ex b/lib/cklist_web/controllers/checklist_controller.ex index 0c5f9d4..a68a4eb 100644 --- a/lib/cklist_web/controllers/checklist_controller.ex +++ b/lib/cklist_web/controllers/checklist_controller.ex @@ -2,18 +2,12 @@ defmodule CklistWeb.ChecklistController do use CklistWeb, :controller alias Cklist.Checklists - alias Cklist.Checklists.Checklist def index(conn, _params) do checklists = Checklists.list_checklists(conn.assigns.current_user) render(conn, :index, checklists: checklists) end - def new(conn, _params) do - changeset = Checklists.change_checklist(%Checklist{}) - render(conn, :new, changeset: changeset) - end - def create(conn, %{"checklist" => checklist_params}) do data = %{} |> Map.put("sequential", Map.get(checklist_params, "sequential")) diff --git a/lib/cklist_web/live/cklist_new_live.ex b/lib/cklist_web/live/cklist_new_live.ex new file mode 100644 index 0000000..366c23f --- /dev/null +++ b/lib/cklist_web/live/cklist_new_live.ex @@ -0,0 +1,17 @@ +defmodule CklistWeb.CklistNewLive do + use CklistWeb, :live_view + + alias Cklist.Checklists + alias Cklist.Checklists.Checklist + + def render(assigns) do + CklistWeb.ChecklistHTML.new(assigns) + end + + def mount(_params, _session, socket) do + changeset = Checklists.change_checklist(%Checklist{}) + socket = socket + |> assign(:changeset, changeset) + {:ok, socket} + end +end diff --git a/lib/cklist_web/router.ex b/lib/cklist_web/router.ex index 6df5810..b901026 100644 --- a/lib/cklist_web/router.ex +++ b/lib/cklist_web/router.ex @@ -74,10 +74,11 @@ defmodule CklistWeb.Router do live "/user/settings", UserSettingsLive, :edit live "/user/settings/confirm_email/:token", UserSettingsLive, :confirm_email + live "/checklists/new", CklistNewLive live "/checklists/:id/run", CklistRunLive end - resources "/checklists", ChecklistController, except: [:index] + resources "/checklists", ChecklistController, except: [:index, :new] end scope "/", CklistWeb do From 6bc8fade31436d7faf10f8d092876c03dc884a08 Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 24 Mar 2024 16:46:23 +0100 Subject: [PATCH 27/42] WIP live updating checklist steps --- .../checklist_html/checklist_form.html.heex | 11 ++++++- lib/cklist_web/live/cklist_new_live.ex | 32 ++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex b/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex index f74ba4e..ff889b0 100644 --- a/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex +++ b/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex @@ -5,7 +5,16 @@ <.input field={f[:title]} type="text" label="Title" /> <.input field={f[:description]} type="text" label="Description" /> <.input field={f[:access]} type="select" options={[Personal: "personal", Public: "public"]} label="Access" /> - <.input field={f[:sequential]} type="select" options={[True: true, False: false]} label="Sequential" /> + <.input name="sequential" value={f[:document].value.sequential} type="select" options={[True: true, False: false]} label="Sequential" /> + + + <%= for {step, index} <- Enum.with_index(f[:document].value.steps) do %> + <.input name={"step-#{index}"} type="text" value={step.name} phx-change="step_changed" /> + <% end %> + <.input name="new_step" type="text" value="" phx-change="new_step" placeholder="Add new step" /> + <:actions> <.button>Save Checklist diff --git a/lib/cklist_web/live/cklist_new_live.ex b/lib/cklist_web/live/cklist_new_live.ex index 366c23f..2e9ac23 100644 --- a/lib/cklist_web/live/cklist_new_live.ex +++ b/lib/cklist_web/live/cklist_new_live.ex @@ -9,9 +9,39 @@ defmodule CklistWeb.CklistNewLive do end def mount(_params, _session, socket) do - changeset = Checklists.change_checklist(%Checklist{}) + changeset = Checklists.change_checklist( + %Checklist{}, + %{ document: %{ sequential: true, steps: [%{name: "one"}, %{name: "two"}] } } + ) + + IO.inspect(changeset) + socket = socket |> assign(:changeset, changeset) {:ok, socket} end + + # leave text field + def handle_event("new_step", params, socket) do + document = socket.assigns.changeset.changes.document + + IO.inspect(socket.assigns.changeset) + + {:ok, changeset} = Checklists.update_checklist( + socket.assigns.changeset, + %{ document: %{ + sequential: document.sequential, + steps: [document.steps | %{name: params["new_step"]}] + }} + ) + + socket = socket + |> assign(:changeset, changeset) + { :noreply, socket } + end + + def handle_event("step_changed", params, socket) do + IO.inspect(params) + { :noreply, socket } + end end From 93368fc28e6b4863b84eab54716e48419e5bc54e Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 7 Apr 2024 22:58:21 +0200 Subject: [PATCH 28/42] WIP: half-working live view for saving new checklists --- lib/cklist_web/components/my_components.ex | 15 ++++ .../controllers/checklist_controller.ex | 18 +++-- .../checklist_html/checklist_form.html.heex | 6 +- .../controllers/checklist_html/new.html.heex | 2 +- lib/cklist_web/live/cklist_new_live.ex | 70 ++++++++++++++++--- 5 files changed, 93 insertions(+), 18 deletions(-) diff --git a/lib/cklist_web/components/my_components.ex b/lib/cklist_web/components/my_components.ex index 740f468..f63d75c 100644 --- a/lib/cklist_web/components/my_components.ex +++ b/lib/cklist_web/components/my_components.ex @@ -69,6 +69,21 @@ defmodule CklistWeb.MyComponents do """ end + attr :class, :string, default: nil + attr :rest, :global, doc: "arbitrary HTML attributes to apply to the button tag" + slot :inner_block, required: true + def my_button(assigns) do + ~H""" + <.button + type="button" + class={"px-2 #{@class}"} + {@rest} + > + <%= render_slot(@inner_block) %> + + """ + end + attr :class, :string, default: nil attr :rest, :global, doc: "arbitrary HTML attributes to apply to the button tag" slot :inner_block, required: true diff --git a/lib/cklist_web/controllers/checklist_controller.ex b/lib/cklist_web/controllers/checklist_controller.ex index a68a4eb..36a0d8d 100644 --- a/lib/cklist_web/controllers/checklist_controller.ex +++ b/lib/cklist_web/controllers/checklist_controller.ex @@ -8,14 +8,22 @@ defmodule CklistWeb.ChecklistController do render(conn, :index, checklists: checklists) end - def create(conn, %{"checklist" => checklist_params}) do - data = %{} - |> Map.put("sequential", Map.get(checklist_params, "sequential")) - |> Map.put("steps", Map.get(checklist_params, "steps", [])) + def create(conn, params) do + IO.inspect(params) + checklist_params = params["checklist"] + IO.inspect(checklist_params) + steps = Map.filter(params, fn {key, _val} -> String.starts_with?(key, "step-") end) + + document = %{ + sequential: Map.get(params, "sequential", true), + steps: Enum.map(Map.values(steps), fn val -> %{name: val} end) + } checklist_params = checklist_params |> Map.put("user_id", conn.assigns.current_user.id) - |> Map.put("document", data) + |> Map.put("document", document) + + IO.inspect(checklist_params) case Checklists.create_checklist(checklist_params) do {:ok, checklist} -> diff --git a/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex b/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex index ff889b0..20fea91 100644 --- a/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex +++ b/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex @@ -1,4 +1,4 @@ -<.simple_form :let={f} for={@changeset} action={@action}> +<.simple_form :let={f} for={@changeset} phx-submit="save_checklist"> <.error :if={@changeset.action}> Oops, something went wrong! Please check the errors below. @@ -13,7 +13,9 @@ <%= for {step, index} <- Enum.with_index(f[:document].value.steps) do %> <.input name={"step-#{index}"} type="text" value={step.name} phx-change="step_changed" /> <% end %> - <.input name="new_step" type="text" value="" phx-change="new_step" placeholder="Add new step" /> + + <.my_button phx-click="step_add" class="bg-lime-500 hover:bg-lime-400">Add step + <.my_button phx-click="step_remove" class="bg-red-500 hover:bg-red-600">Remove step <:actions> <.button>Save Checklist diff --git a/lib/cklist_web/controllers/checklist_html/new.html.heex b/lib/cklist_web/controllers/checklist_html/new.html.heex index 5031dbd..a8072f7 100644 --- a/lib/cklist_web/controllers/checklist_html/new.html.heex +++ b/lib/cklist_web/controllers/checklist_html/new.html.heex @@ -3,6 +3,6 @@ <:subtitle>Use this form to manage checklist records in your database. -<.checklist_form changeset={@changeset} action={~p"/checklists"} /> +<.checklist_form changeset={@changeset} /> <.back navigate={~p"/checklists"}>Back to checklists diff --git a/lib/cklist_web/live/cklist_new_live.ex b/lib/cklist_web/live/cklist_new_live.ex index 2e9ac23..dc897a5 100644 --- a/lib/cklist_web/live/cklist_new_live.ex +++ b/lib/cklist_web/live/cklist_new_live.ex @@ -21,27 +21,77 @@ defmodule CklistWeb.CklistNewLive do {:ok, socket} end - # leave text field - def handle_event("new_step", params, socket) do + def handle_event("step_changed", _params, socket) do + IO.inspect("Step changed") + { :noreply, socket } + end + + def handle_event("step_add", _params, socket) do + IO.inspect("Adding new step") document = socket.assigns.changeset.changes.document - IO.inspect(socket.assigns.changeset) + {_, changeset} = Checklists.update_checklist( + socket.assigns.changeset.data, + %{ document: %{ + steps: document.steps ++ [%{name: "New step"}], + sequential: document.sequential + }} + ) + + socket = socket + |> assign(:changeset, changeset) + + { :noreply, socket } + end + + def handle_event("step_remove", _params, socket) do + IO.inspect("Removing one step") - {:ok, changeset} = Checklists.update_checklist( - socket.assigns.changeset, + document = socket.assigns.changeset.changes.document + new_steps = List.delete_at(document.steps, length(document.steps)-1) + + {_, changeset} = Checklists.update_checklist( + socket.assigns.changeset.data, %{ document: %{ - sequential: document.sequential, - steps: [document.steps | %{name: params["new_step"]}] + steps: new_steps, + sequential: document.sequential }} ) socket = socket - |> assign(:changeset, changeset) + |> assign(:changeset, changeset) + { :noreply, socket } end - def handle_event("step_changed", params, socket) do + def handle_event("save_checklist", params, socket) do + IO.inspect("saving checklist") + IO.inspect(params) - { :noreply, socket } + checklist_params = params["checklist"] + IO.inspect(checklist_params) + steps = Map.filter(params, fn {key, _val} -> String.starts_with?(key, "step-") end) + + document = %{ + sequential: Map.get(params, "sequential", true), + steps: Enum.map(Map.values(steps), fn val -> %{name: val} end) + } + + checklist_params = checklist_params + |> Map.put("user_id", socket.assigns.current_user.id) + |> Map.put("document", document) + + IO.inspect(checklist_params) + + case Checklists.create_checklist(checklist_params) do + {:ok, checklist} -> + {:noreply, socket + |> put_flash(:info, "Checklist created successfully.") + |> redirect(to: ~p"/checklists/#{checklist}")} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, socket + |> assign(:changeset, changeset)} + end end end From 79da2f5a05e83272a5462e195b9a48103e0e75fb Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Mon, 15 Apr 2024 14:54:51 +0200 Subject: [PATCH 29/42] WIP fix issue with undefined enum values Use change_checklist instead of update_checklist (which is for updating in the database, meaning the changeset needs to be valid). --- lib/cklist_web/live/cklist_new_live.ex | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/cklist_web/live/cklist_new_live.ex b/lib/cklist_web/live/cklist_new_live.ex index dc897a5..6976929 100644 --- a/lib/cklist_web/live/cklist_new_live.ex +++ b/lib/cklist_web/live/cklist_new_live.ex @@ -30,7 +30,7 @@ defmodule CklistWeb.CklistNewLive do IO.inspect("Adding new step") document = socket.assigns.changeset.changes.document - {_, changeset} = Checklists.update_checklist( + changeset = Checklists.change_checklist( socket.assigns.changeset.data, %{ document: %{ steps: document.steps ++ [%{name: "New step"}], @@ -48,12 +48,11 @@ defmodule CklistWeb.CklistNewLive do IO.inspect("Removing one step") document = socket.assigns.changeset.changes.document - new_steps = List.delete_at(document.steps, length(document.steps)-1) - {_, changeset} = Checklists.update_checklist( + changeset = Checklists.change_checklist( socket.assigns.changeset.data, %{ document: %{ - steps: new_steps, + steps: List.delete_at(document.steps, length(document.steps)-1), sequential: document.sequential }} ) @@ -67,9 +66,7 @@ defmodule CklistWeb.CklistNewLive do def handle_event("save_checklist", params, socket) do IO.inspect("saving checklist") - IO.inspect(params) checklist_params = params["checklist"] - IO.inspect(checklist_params) steps = Map.filter(params, fn {key, _val} -> String.starts_with?(key, "step-") end) document = %{ @@ -81,8 +78,6 @@ defmodule CklistWeb.CklistNewLive do |> Map.put("user_id", socket.assigns.current_user.id) |> Map.put("document", document) - IO.inspect(checklist_params) - case Checklists.create_checklist(checklist_params) do {:ok, checklist} -> {:noreply, socket From 273ae1bdf32351c721a26a76a63d5f7d15a9a4ff Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Mon, 15 Apr 2024 18:38:29 +0200 Subject: [PATCH 30/42] WIP: First working version of checklist generation Doesn't show errors yet :/ but should work otherwise. Also looks way to hacky in my oppinion with all the conversions needed. Also needs cleanup (e.g. of checklitst_controller because it doesn't need the create method anymore). And we probably want to disable editing checklists for now (in favor of adding copy-on-write duplication in a later stage). --- lib/cklist/checklists.ex | 7 ++ lib/cklist/checklists/checklist.ex | 12 +- .../checklist_html/checklist_form.html.heex | 4 +- lib/cklist_web/live/cklist_new_live.ex | 113 +++++++++++++----- 4 files changed, 97 insertions(+), 39 deletions(-) diff --git a/lib/cklist/checklists.ex b/lib/cklist/checklists.ex index ca8207b..1fb159e 100644 --- a/lib/cklist/checklists.ex +++ b/lib/cklist/checklists.ex @@ -62,6 +62,13 @@ defmodule Cklist.Checklists do |> Repo.insert() end + @doc """ + Inserts a checklist + """ + def insert_checklist(changeset) do + Repo.insert(changeset) + end + @doc """ Updates a checklist. diff --git a/lib/cklist/checklists/checklist.ex b/lib/cklist/checklists/checklist.ex index 3b618d3..c191c9f 100644 --- a/lib/cklist/checklists/checklist.ex +++ b/lib/cklist/checklists/checklist.ex @@ -3,21 +3,21 @@ defmodule Cklist.Checklists.Checklist do import Ecto.Changeset schema "checklists" do - field :description, :string field :title, :string - field :document, :map + field :description, :string field :access, Ecto.Enum, values: [:public, :personal] + field :document, :map + + timestamps(type: :utc_datetime) belongs_to :user, Cklist.Accounts.User has_many :runs, Cklist.Checklists.Run - - timestamps(type: :utc_datetime) end @doc false def changeset(checklist, attrs) do checklist - |> cast(attrs, [:title, :description, :document, :user_id, :access]) - |> validate_required([:title, :description, :document, :user_id, :access]) + |> cast(attrs, [:title, :description, :access, :document, :user_id]) + |> validate_required([:title, :description, :access, :document, :user_id]) end end diff --git a/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex b/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex index 20fea91..decc56d 100644 --- a/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex +++ b/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex @@ -1,4 +1,4 @@ -<.simple_form :let={f} for={@changeset} phx-submit="save_checklist"> +<.simple_form :let={f} for={@changeset} phx-change="form_changed" phx-submit="save_checklist"> <.error :if={@changeset.action}> Oops, something went wrong! Please check the errors below. @@ -11,7 +11,7 @@ Steps <%= for {step, index} <- Enum.with_index(f[:document].value.steps) do %> - <.input name={"step-#{index}"} type="text" value={step.name} phx-change="step_changed" /> + <.input name={"step-#{index}"} type="text" value={step.name} /> <% end %> <.my_button phx-click="step_add" class="bg-lime-500 hover:bg-lime-400">Add step diff --git a/lib/cklist_web/live/cklist_new_live.ex b/lib/cklist_web/live/cklist_new_live.ex index 6976929..cc4d572 100644 --- a/lib/cklist_web/live/cklist_new_live.ex +++ b/lib/cklist_web/live/cklist_new_live.ex @@ -10,83 +10,134 @@ defmodule CklistWeb.CklistNewLive do def mount(_params, _session, socket) do changeset = Checklists.change_checklist( - %Checklist{}, - %{ document: %{ sequential: true, steps: [%{name: "one"}, %{name: "two"}] } } + %Checklist{ + access: :personal, + document: %{ + version: "0.1", + sequential: true, + steps: [%{ name: "one" }, %{ name: "two" }] + }, + user_id: socket.assigns.current_user.id, + } ) - IO.inspect(changeset) - socket = socket |> assign(:changeset, changeset) {:ok, socket} end - def handle_event("step_changed", _params, socket) do - IO.inspect("Step changed") + def handle_event("form_changed", params, socket) do + IO.inspect("Form changed") + + #IO.inspect(socket.assigns.changeset) + #IO.inspect(params) + + new_checklist = checklist_from(params, socket) + + changeset = Checklists.change_checklist( + socket.assigns.changeset.data, + Map.merge( + socket.assigns.changeset.changes, + Map.put(new_checklist, :document, document_from(params, socket)) + ) + ) + + #IO.inspect(changeset) + + socket = socket + |> assign(:changeset, changeset) + { :noreply, socket } end def handle_event("step_add", _params, socket) do IO.inspect("Adding new step") - document = socket.assigns.changeset.changes.document + document = latest_document(socket) changeset = Checklists.change_checklist( socket.assigns.changeset.data, - %{ document: %{ - steps: document.steps ++ [%{name: "New step"}], - sequential: document.sequential - }} + Map.merge( + socket.assigns.changeset.changes, + %{ document: %{ + sequential: document.sequential, + steps: document.steps ++ [%{name: "New step"}], + version: document.version, + }} + ) ) + IO.inspect(changeset) + socket = socket - |> assign(:changeset, changeset) + |> assign(:changeset, changeset) { :noreply, socket } end def handle_event("step_remove", _params, socket) do IO.inspect("Removing one step") - - document = socket.assigns.changeset.changes.document + document = latest_document(socket) changeset = Checklists.change_checklist( socket.assigns.changeset.data, - %{ document: %{ - steps: List.delete_at(document.steps, length(document.steps)-1), - sequential: document.sequential - }} + Map.merge( + socket.assigns.changeset.changes, + %{ document: %{ + sequential: document.sequential, + steps: List.delete_at(document.steps, length(document.steps) - 1), + version: document.version, + }} + ) ) + IO.inspect(changeset) + socket = socket |> assign(:changeset, changeset) { :noreply, socket } end - def handle_event("save_checklist", params, socket) do + def handle_event("save_checklist", _params, socket) do IO.inspect("saving checklist") - checklist_params = params["checklist"] - steps = Map.filter(params, fn {key, _val} -> String.starts_with?(key, "step-") end) - - document = %{ - sequential: Map.get(params, "sequential", true), - steps: Enum.map(Map.values(steps), fn val -> %{name: val} end) - } + # checklist_params = params["checklist"] + # |> Map.put("document", document_from(params, socket)) - checklist_params = checklist_params - |> Map.put("user_id", socket.assigns.current_user.id) - |> Map.put("document", document) + changeset = socket.assigns.changeset - case Checklists.create_checklist(checklist_params) do + case changeset.valid? && Checklists.insert_checklist(changeset) do {:ok, checklist} -> {:noreply, socket |> put_flash(:info, "Checklist created successfully.") |> redirect(to: ~p"/checklists/#{checklist}")} - {:error, %Ecto.Changeset{} = changeset} -> + false -> {:noreply, socket |> assign(:changeset, changeset)} end end + + defp checklist_from(params, _socket) do + %{ + title: params["checklist"]["title"], + description: params["checklist"]["description"], + access: (if params["checklist"]["access"] === "personal", do: :personal, else: :public) + } + end + + defp document_from(params, socket) do + document = latest_document(socket) + steps = Map.filter(params, fn {key, _val} -> String.starts_with?(key, "step-") end) + + %{ + sequential: Map.get(params, "sequential", document.sequential), + steps: Enum.map(Map.values(steps), fn val -> %{name: val} end), + version: document.version, + } + end + + defp latest_document(socket) do + Map.get(socket.assigns.changeset.changes, :document, socket.assigns.changeset.data.document) + end end From 475be3a5390d0de5cb58e6f25dee4ca923c2f7c9 Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Thu, 2 May 2024 13:34:47 +0200 Subject: [PATCH 31/42] Fix checklist creation and cleanup --- lib/cklist_web/live/cklist_new_live.ex | 20 +------------------- lib/cklist_web/live/cklist_run_live.ex | 2 ++ 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/lib/cklist_web/live/cklist_new_live.ex b/lib/cklist_web/live/cklist_new_live.ex index cc4d572..f2feb56 100644 --- a/lib/cklist_web/live/cklist_new_live.ex +++ b/lib/cklist_web/live/cklist_new_live.ex @@ -27,11 +27,6 @@ defmodule CklistWeb.CklistNewLive do end def handle_event("form_changed", params, socket) do - IO.inspect("Form changed") - - #IO.inspect(socket.assigns.changeset) - #IO.inspect(params) - new_checklist = checklist_from(params, socket) changeset = Checklists.change_checklist( @@ -42,8 +37,6 @@ defmodule CklistWeb.CklistNewLive do ) ) - #IO.inspect(changeset) - socket = socket |> assign(:changeset, changeset) @@ -51,7 +44,6 @@ defmodule CklistWeb.CklistNewLive do end def handle_event("step_add", _params, socket) do - IO.inspect("Adding new step") document = latest_document(socket) changeset = Checklists.change_checklist( @@ -66,8 +58,6 @@ defmodule CklistWeb.CklistNewLive do ) ) - IO.inspect(changeset) - socket = socket |> assign(:changeset, changeset) @@ -75,7 +65,6 @@ defmodule CklistWeb.CklistNewLive do end def handle_event("step_remove", _params, socket) do - IO.inspect("Removing one step") document = latest_document(socket) changeset = Checklists.change_checklist( @@ -90,8 +79,6 @@ defmodule CklistWeb.CklistNewLive do ) ) - IO.inspect(changeset) - socket = socket |> assign(:changeset, changeset) @@ -99,11 +86,6 @@ defmodule CklistWeb.CklistNewLive do end def handle_event("save_checklist", _params, socket) do - IO.inspect("saving checklist") - - # checklist_params = params["checklist"] - # |> Map.put("document", document_from(params, socket)) - changeset = socket.assigns.changeset case changeset.valid? && Checklists.insert_checklist(changeset) do @@ -131,7 +113,7 @@ defmodule CklistWeb.CklistNewLive do steps = Map.filter(params, fn {key, _val} -> String.starts_with?(key, "step-") end) %{ - sequential: Map.get(params, "sequential", document.sequential), + sequential: (if Map.get(params, "sequential", document.sequential) == "true", do: true, else: false), steps: Enum.map(Map.values(steps), fn val -> %{name: val} end), version: document.version, } diff --git a/lib/cklist_web/live/cklist_run_live.ex b/lib/cklist_web/live/cklist_run_live.ex index 2d10f18..b756e21 100644 --- a/lib/cklist_web/live/cklist_run_live.ex +++ b/lib/cklist_web/live/cklist_run_live.ex @@ -13,6 +13,8 @@ defmodule CklistWeb.CklistRunLive do checklist = Checklists.get_checklist!(id) run = Checklists.log_run_start(checklist, user) + IO.inspect(checklist) + socket = socket |> assign(:checklist, checklist) |> assign(:run, run) From f761e2ac212200478fc56b39b97a8abece9ab2ae Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Thu, 2 May 2024 14:43:48 +0200 Subject: [PATCH 32/42] Fix on_delete behavior Apparently, on_delete: :nothing doesn't really do nothing in postgres databases. Attempting to delete a cklist after running at least one instance thus threw an error complaining about issues with database constriants. This PR now propagates deletes from the checklist table through the runs table all the way to the activity table, cleaning up orphaned data on delete. The proper fix here will be soft deletes, which will be necessary as soon as we have a more complex sharing scheme. --- lib/cklist/checklists/checklist.ex | 2 +- lib/cklist/checklists/run.ex | 2 +- priv/repo/migrations/20240128194147_create_checklists.exs | 2 +- priv/repo/migrations/20240310152508_create_checklist_run.exs | 2 +- priv/repo/migrations/20240310153254_create_activity_log.exs | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/cklist/checklists/checklist.ex b/lib/cklist/checklists/checklist.ex index c191c9f..30108e7 100644 --- a/lib/cklist/checklists/checklist.ex +++ b/lib/cklist/checklists/checklist.ex @@ -11,7 +11,7 @@ defmodule Cklist.Checklists.Checklist do timestamps(type: :utc_datetime) belongs_to :user, Cklist.Accounts.User - has_many :runs, Cklist.Checklists.Run + has_many :runs, Cklist.Checklists.Run, on_delete: :delete_all end @doc false diff --git a/lib/cklist/checklists/run.ex b/lib/cklist/checklists/run.ex index 7b22ada..b5f4651 100644 --- a/lib/cklist/checklists/run.ex +++ b/lib/cklist/checklists/run.ex @@ -4,7 +4,7 @@ defmodule Cklist.Checklists.Run do schema "runs" do belongs_to :checklist, Cklist.Checklists.Checklist - has_many :activities, Cklist.Checklists.Activity + has_many :activities, Cklist.Checklists.Activity, on_delete: :delete_all end @doc false diff --git a/priv/repo/migrations/20240128194147_create_checklists.exs b/priv/repo/migrations/20240128194147_create_checklists.exs index 8cbbd49..2fb7147 100644 --- a/priv/repo/migrations/20240128194147_create_checklists.exs +++ b/priv/repo/migrations/20240128194147_create_checklists.exs @@ -6,7 +6,7 @@ defmodule Cklist.Repo.Migrations.CreateChecklists do add :title, :string add :description, :string add :document, :map - add :user_id, references(:users, on_delete: :nothing) + add :user_id, references(:users, on_delete: :delete_all) timestamps(type: :utc_datetime) end diff --git a/priv/repo/migrations/20240310152508_create_checklist_run.exs b/priv/repo/migrations/20240310152508_create_checklist_run.exs index 55b40cb..e3acfbf 100644 --- a/priv/repo/migrations/20240310152508_create_checklist_run.exs +++ b/priv/repo/migrations/20240310152508_create_checklist_run.exs @@ -3,7 +3,7 @@ defmodule Cklist.Repo.Migrations.CreateChecklistRun do def change do create table(:runs) do - add :checklist_id, references(:checklists) + add :checklist_id, references(:checklists, on_delete: :delete_all) end end end diff --git a/priv/repo/migrations/20240310153254_create_activity_log.exs b/priv/repo/migrations/20240310153254_create_activity_log.exs index 78af720..4299458 100644 --- a/priv/repo/migrations/20240310153254_create_activity_log.exs +++ b/priv/repo/migrations/20240310153254_create_activity_log.exs @@ -5,8 +5,8 @@ defmodule Cklist.Repo.Migrations.CreateActivityLog do create table(:activity) do add :event, :map - add :user_id, references(:users) - add :run_id, references(:runs) + add :user_id, references(:users, on_delete: :delete_all) + add :run_id, references(:runs, on_delete: :delete_all) timestamps([type: :utc_datetime, updated_at: false]) end From 240c1160506b4154844d401d2fa036f36128478e Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Thu, 2 May 2024 14:58:05 +0200 Subject: [PATCH 33/42] Disallow editing checklists --- lib/cklist_web/controllers/checklist_controller.ex | 6 ------ lib/cklist_web/controllers/checklist_html/edit.html.heex | 8 -------- lib/cklist_web/controllers/checklist_html/index.html.heex | 5 ----- lib/cklist_web/controllers/checklist_html/show.html.heex | 8 +------- 4 files changed, 1 insertion(+), 26 deletions(-) delete mode 100644 lib/cklist_web/controllers/checklist_html/edit.html.heex diff --git a/lib/cklist_web/controllers/checklist_controller.ex b/lib/cklist_web/controllers/checklist_controller.ex index 36a0d8d..9d37862 100644 --- a/lib/cklist_web/controllers/checklist_controller.ex +++ b/lib/cklist_web/controllers/checklist_controller.ex @@ -41,12 +41,6 @@ defmodule CklistWeb.ChecklistController do render(conn, :show, checklist: checklist) end - def edit(conn, %{"id" => id}) do - checklist = Checklists.get_checklist!(id) - changeset = Checklists.change_checklist(checklist) - render(conn, :edit, checklist: checklist, changeset: changeset) - end - def update(conn, %{"id" => id, "checklist" => checklist_params}) do checklist = Checklists.get_checklist!(id) diff --git a/lib/cklist_web/controllers/checklist_html/edit.html.heex b/lib/cklist_web/controllers/checklist_html/edit.html.heex deleted file mode 100644 index 46187dc..0000000 --- a/lib/cklist_web/controllers/checklist_html/edit.html.heex +++ /dev/null @@ -1,8 +0,0 @@ -<.header> - Edit Checklist <%= @checklist.id %> - <:subtitle>Use this form to manage checklist records in your database. - - -<.checklist_form changeset={@changeset} action={~p"/checklists/#{@checklist}"} /> - -<.back navigate={~p"/checklists"}>Back to checklists diff --git a/lib/cklist_web/controllers/checklist_html/index.html.heex b/lib/cklist_web/controllers/checklist_html/index.html.heex index f9df7b4..c49324f 100644 --- a/lib/cklist_web/controllers/checklist_html/index.html.heex +++ b/lib/cklist_web/controllers/checklist_html/index.html.heex @@ -18,11 +18,6 @@ Run this checklist - <:action :let={checklist}> - <.link navigate={~p"/checklists/#{checklist}/edit"}> - Edit this checklist - - <:action :let={checklist}> <.link href={~p"/checklists/#{checklist}"} method="delete" data-confirm="Are you sure?"> Delete this checklist diff --git a/lib/cklist_web/controllers/checklist_html/show.html.heex b/lib/cklist_web/controllers/checklist_html/show.html.heex index c7b1107..f5a4a6f 100644 --- a/lib/cklist_web/controllers/checklist_html/show.html.heex +++ b/lib/cklist_web/controllers/checklist_html/show.html.heex @@ -9,13 +9,6 @@ Run this checklist - <.link href={~p"/checklists/#{@checklist}/edit"}> - <.button> - Edit -   - Run this checklist - - @@ -31,6 +24,7 @@ <% end %> + <:item title="Document version"><%= @checklist.document["version"] %> <.back navigate={~p"/checklists"}>Back to checklists From a0efd15f96ec088c6f09cf57cca1b1519c1273c8 Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Mon, 6 May 2024 10:18:54 +0200 Subject: [PATCH 34/42] use heroicons --- .../controllers/checklist_html/index.html.heex | 6 +++--- .../controllers/checklist_html/show.html.heex | 2 +- .../controllers/page_html/home.html.heex | 16 ++++++++-------- priv/static/images/delete.svg | 1 - priv/static/images/edit.svg | 1 - priv/static/images/explore.svg | 1 - priv/static/images/log_in.svg | 1 - priv/static/images/log_out.svg | 1 - priv/static/images/play.svg | 1 - priv/static/images/register.svg | 1 - priv/static/images/settings.svg | 1 - 11 files changed, 12 insertions(+), 20 deletions(-) delete mode 100644 priv/static/images/delete.svg delete mode 100644 priv/static/images/edit.svg delete mode 100644 priv/static/images/explore.svg delete mode 100644 priv/static/images/log_in.svg delete mode 100644 priv/static/images/log_out.svg delete mode 100644 priv/static/images/play.svg delete mode 100644 priv/static/images/register.svg delete mode 100644 priv/static/images/settings.svg diff --git a/lib/cklist_web/controllers/checklist_html/index.html.heex b/lib/cklist_web/controllers/checklist_html/index.html.heex index c49324f..db6a6a7 100644 --- a/lib/cklist_web/controllers/checklist_html/index.html.heex +++ b/lib/cklist_web/controllers/checklist_html/index.html.heex @@ -15,12 +15,12 @@ <.link navigate={~p"/checklists/#{checklist}"}>Show
<.link navigate={~p"/checklists/#{checklist}/run"}> - Run this checklist + <.icon name="hero-play" /> <:action :let={checklist}> - <.link href={~p"/checklists/#{checklist}"} method="delete" data-confirm="Are you sure?"> - Delete this checklist + <.link href={~p"/checklists/#{checklist}"} method="delete" data-confirm="Are you sure you want to delete this checklist?"> + <.icon name="hero-trash" /> diff --git a/lib/cklist_web/controllers/checklist_html/show.html.heex b/lib/cklist_web/controllers/checklist_html/show.html.heex index f5a4a6f..6dbba68 100644 --- a/lib/cklist_web/controllers/checklist_html/show.html.heex +++ b/lib/cklist_web/controllers/checklist_html/show.html.heex @@ -6,7 +6,7 @@ <.button> Run   - Run this checklist + <.icon name="hero-play" class="my-0.5" /> diff --git a/lib/cklist_web/controllers/page_html/home.html.heex b/lib/cklist_web/controllers/page_html/home.html.heex index 9118625..b814b1c 100644 --- a/lib/cklist_web/controllers/page_html/home.html.heex +++ b/lib/cklist_web/controllers/page_html/home.html.heex @@ -61,9 +61,9 @@ > - - - Explore + + <.icon name="hero-list-bullet" class="w-8 h-8" /> + Explore checklists @@ -75,7 +75,7 @@ - + <.icon name="hero-cog-6-tooth" class="w-8 h-8" /> Settings @@ -87,7 +87,7 @@ - + <.icon name="hero-power" class="w-8 h-8" /> Log out @@ -99,7 +99,7 @@ - + <.icon name="hero-user-plus" class="w-8 h-8" /> Register @@ -110,7 +110,7 @@ - + <.icon name="hero-power" class="w-8 h-8" /> Log in @@ -124,7 +124,7 @@ -
- <.link navigate={~p"/checklists/#{checklist}"}>Show -
- <.link navigate={~p"/checklists/#{checklist}/run"}> - <.icon name="hero-play" /> - + <%= if @current_user do %> +
+ <.link navigate={~p"/checklists/#{checklist}"}>Show +
+ <.link navigate={~p"/checklists/#{checklist}/run"}> + <.icon name="hero-play" /> + + <% end %> <:action :let={checklist}> - <.link href={~p"/checklists/#{checklist}"} method="delete" data-confirm="Are you sure you want to delete this checklist?"> - <.icon name="hero-trash" /> - + <%= if @current_user do %> + <.link href={~p"/checklists/#{checklist}"} method="delete" data-confirm="Are you sure you want to delete this checklist?"> + <.icon name="hero-trash" /> + + <% end %> diff --git a/lib/cklist_web/controllers/page_html/home.html.heex b/lib/cklist_web/controllers/page_html/home.html.heex index b814b1c..3443d08 100644 --- a/lib/cklist_web/controllers/page_html/home.html.heex +++ b/lib/cklist_web/controllers/page_html/home.html.heex @@ -63,7 +63,7 @@
<.icon name="hero-list-bullet" class="w-8 h-8" /> - Explore checklists + Explore @@ -132,7 +132,7 @@ fill="#18181B" /> - GitHub + Contribute
From d4da980d9f0ef41554ad1199727d3e61f8d6e848 Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Mon, 6 May 2024 14:57:38 +0200 Subject: [PATCH 36/42] Work on cards for sequential checklists --- lib/cklist_web/components/my_components.ex | 27 ++++++++++++++++++- .../controllers/checklist_html/run.html.heex | 10 ++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/lib/cklist_web/components/my_components.ex b/lib/cklist_web/components/my_components.ex index f63d75c..213d0b1 100644 --- a/lib/cklist_web/components/my_components.ex +++ b/lib/cklist_web/components/my_components.ex @@ -23,6 +23,31 @@ defmodule CklistWeb.MyComponents do """ end + @doc """ + Renders one step of a sequential checklist as a card. + """ + def card(assigns) do + ~H""" +
+

<%= render_slot(@inner_block) %>

+
+ <.next_button class="basis-1/3">Done +
+
+ """ + end + + @doc """ + Renders a preview of one step of the next step of a sequential checklist + """ + def preview_card(assigns) do + ~H""" +
+

<%= render_slot(@inner_block) %>

+
+ """ + end + @doc """ Renders an abort button. @@ -89,7 +114,7 @@ defmodule CklistWeb.MyComponents do slot :inner_block, required: true defp button(assigns) do ~H""" - """ diff --git a/lib/cklist_web/controllers/checklist_html/run.html.heex b/lib/cklist_web/controllers/checklist_html/run.html.heex index b7da4c5..a183a27 100644 --- a/lib/cklist_web/controllers/checklist_html/run.html.heex +++ b/lib/cklist_web/controllers/checklist_html/run.html.heex @@ -42,9 +42,13 @@ <% end %> <% else %> -

Current step: <%= @current_step["name"] %>

+ <.card> + <%= @current_step["name"] %> + <%= if @next_step do %> -

Next step: <%= @next_step["name"] %>

+ <.preview_card> + <%= @next_step["name"] %> + <% end %> <% end %>
@@ -52,7 +56,7 @@
<.abort_button class="basis-1/3" hidden={@completed} /> - <.next_button class="basis-1/3"> + <.next_button class="basis-1/3" :if={@completed}> <%= if @completed, do: "Awesome!", else: "Done" %>
From 2402887d7f95acf3eb67cff48d7e21ce859f5c03 Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Mon, 6 May 2024 15:16:35 +0200 Subject: [PATCH 37/42] Tweakig buttons --- lib/cklist_web/components/my_components.ex | 8 ++++---- .../controllers/checklist_html/checklist_form.html.heex | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/cklist_web/components/my_components.ex b/lib/cklist_web/components/my_components.ex index 213d0b1..4b268e9 100644 --- a/lib/cklist_web/components/my_components.ex +++ b/lib/cklist_web/components/my_components.ex @@ -61,9 +61,9 @@ defmodule CklistWeb.MyComponents do def abort_button(assigns) do ~H""" <.button - class={"bg-gray-300 hover:bg-gray-400 #{if @hidden, do: "hidden", else: ""} #{@class}"} + class={"#{if @hidden, do: "hidden", else: ""} #{@class}"} phx-click="abort" - data-confirm="Are you sure?" + data-confirm="Are you sure you want to abort this checklist?" > Abort @@ -86,7 +86,7 @@ defmodule CklistWeb.MyComponents do def next_button(assigns) do ~H""" <.button - class={"bg-lime-500 hover:bg-lime-400 #{if @hidden, do: "hidden", else: ""} #{@class}"} + class={"#{if @hidden, do: "hidden", else: ""} #{@class}"} phx-click="next_step" > <%= render_slot(@inner_block) %> @@ -114,7 +114,7 @@ defmodule CklistWeb.MyComponents do slot :inner_block, required: true defp button(assigns) do ~H""" - """ diff --git a/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex b/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex index decc56d..435263b 100644 --- a/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex +++ b/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex @@ -14,8 +14,8 @@ <.input name={"step-#{index}"} type="text" value={step.name} /> <% end %> - <.my_button phx-click="step_add" class="bg-lime-500 hover:bg-lime-400">Add step - <.my_button phx-click="step_remove" class="bg-red-500 hover:bg-red-600">Remove step + <.my_button phx-click="step_add">Add step + <.my_button phx-click="step_remove">Remove step <:actions> <.button>Save Checklist From d8a21dcc2078bef6a84ae926dde30829f66c0a5b Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 12 May 2024 10:52:47 +0200 Subject: [PATCH 38/42] At least put a flash note if creation failed --- lib/cklist_web/live/cklist_new_live.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/cklist_web/live/cklist_new_live.ex b/lib/cklist_web/live/cklist_new_live.ex index f2feb56..3e2b49d 100644 --- a/lib/cklist_web/live/cklist_new_live.ex +++ b/lib/cklist_web/live/cklist_new_live.ex @@ -96,6 +96,7 @@ defmodule CklistWeb.CklistNewLive do false -> {:noreply, socket + |> put_flash(:error, "Checklist creation failed.") |> assign(:changeset, changeset)} end end From 59715a01c607fa07e5c16728fedd8ec2cb033a60 Mon Sep 17 00:00:00 2001 From: Roman Cattaneo <> Date: Sun, 12 May 2024 16:46:37 +0200 Subject: [PATCH 39/42] More components and other random cleanups --- lib/cklist_web/components/my_components.ex | 86 ++++++++++++++++--- .../controllers/checklist_controller.ex | 8 +- .../controllers/checklist_html/run.html.heex | 58 +------------ lib/cklist_web/live/cklist_run_live.ex | 2 - 4 files changed, 80 insertions(+), 74 deletions(-) diff --git a/lib/cklist_web/components/my_components.ex b/lib/cklist_web/components/my_components.ex index 4b268e9..93651cf 100644 --- a/lib/cklist_web/components/my_components.ex +++ b/lib/cklist_web/components/my_components.ex @@ -4,6 +4,7 @@ defmodule CklistWeb.MyComponents do """ use Phoenix.Component + import CklistWeb.CoreComponents @doc """ Renders a progress bar. @@ -23,15 +24,80 @@ defmodule CklistWeb.MyComponents do """ end + def sequential_run(assigns) do + ~H""" +
+
+ <%= if @completed do %> +

Look at all the things you did:

+ + <% else %> + <.card> + <%= @current_step["name"] %> + + <%= if @next_step do %> + <.preview_card> + <%= @next_step["name"] %> + + <% end %> + <% end %> +
+ +
+ <.abort_button class="basis-1/3" hidden={@completed} /> + + <.next_button class="basis-1/3" :if={@completed}> + <%= if @completed, do: "Awesome!" %> + +
+
+ """ + end + + def checklist_run(assigns) do + ~H""" +
+ + <.input + :for={step <- @checklist.document["steps"]} + phx-change="step_done" + checked={Map.get(@step_state, step["name"], false)} + id={step["name"]} + type="checkbox" label={step["name"]} + name={step["name"]} + disabled={@completed} + /> + + +
+

Congratulations! 🎉

+

You finished the checklist.

+
+ +
+ <.abort_button class="basis-1/3" hidden={@completed} /> + + <.next_button class="basis-1/3" hidden={not @completed}> + Awesome! + +
+
+ """ + end + @doc """ Renders one step of a sequential checklist as a card. """ def card(assigns) do ~H""" -
+

<%= render_slot(@inner_block) %>

- <.next_button class="basis-1/3">Done + <.next_button class="basis-1/3" />
""" @@ -42,7 +108,7 @@ defmodule CklistWeb.MyComponents do """ def preview_card(assigns) do ~H""" -
+

<%= render_slot(@inner_block) %>

""" @@ -81,7 +147,7 @@ defmodule CklistWeb.MyComponents do """ attr :class, :string, default: nil attr :hidden, :boolean, default: false - slot :inner_block, default: "Next" + slot :inner_block, required: false def next_button(assigns) do ~H""" @@ -89,30 +155,30 @@ defmodule CklistWeb.MyComponents do class={"#{if @hidden, do: "hidden", else: ""} #{@class}"} phx-click="next_step" > - <%= render_slot(@inner_block) %> + <%= render_slot(@inner_block) || "Next" %> """ end attr :class, :string, default: nil - attr :rest, :global, doc: "arbitrary HTML attributes to apply to the button tag" + attr :rest, :global, doc: "Arbitrary HTML attributes to apply to the button tag" slot :inner_block, required: true def my_button(assigns) do ~H""" - <.button + <.core_button type="button" class={"px-2 #{@class}"} {@rest} > <%= render_slot(@inner_block) %> - + """ end attr :class, :string, default: nil - attr :rest, :global, doc: "arbitrary HTML attributes to apply to the button tag" + attr :rest, :global, doc: "Arbitrary HTML attributes to apply to the button tag" slot :inner_block, required: true - defp button(assigns) do + defp core_button(assigns) do ~H"""
@@ -67,7 +63,7 @@ defmodule CklistWeb.MyComponents do phx-change="step_done" checked={Map.get(@step_state, step["name"], false)} id={step["name"]} - type="checkbox" label={step["name"]} + type="checkbox" label={"#{step["name"]}: #{Map.get(step, "description", "")}"} name={step["name"]} disabled={@completed} /> @@ -91,11 +87,20 @@ defmodule CklistWeb.MyComponents do @doc """ Renders one step of a sequential checklist as a card. + + ## Example + + <.card title="My card" description="Lorem ipsum dolor sit amet..." /> """ + attr :title, :string, required: true + attr :description, :string, required: false def card(assigns) do ~H"""
-

<%= render_slot(@inner_block) %>

+

<%= @title %>

+ <%= if @description do %> +

<%= @description %>

+ <% end %>
<.next_button class="basis-1/3" />
@@ -105,11 +110,15 @@ defmodule CklistWeb.MyComponents do @doc """ Renders a preview of one step of the next step of a sequential checklist + + ## Example + + <.preview_card title="My preview card" /> """ def preview_card(assigns) do ~H"""
-

<%= render_slot(@inner_block) %>

+

<%= @title %>

""" end diff --git a/lib/cklist_web/controllers/checklist_controller.ex b/lib/cklist_web/controllers/checklist_controller.ex index 17ab7cb..ce12e8f 100644 --- a/lib/cklist_web/controllers/checklist_controller.ex +++ b/lib/cklist_web/controllers/checklist_controller.ex @@ -11,10 +11,15 @@ defmodule CklistWeb.ChecklistController do def create(conn, params) do checklist_params = params["checklist"] steps = Map.filter(params, fn {key, _val} -> String.starts_with?(key, "step-") end) + descriptions = Map.filter(params, fn {key, _val} -> String.starts_with?(key, "desc-") end) + |> Enum.map(fn {key, val} -> {String.replace(key, "desc", "step"), val} end ) + |> Map.new() + steps = Map.merge(steps, descriptions, fn _key, name, desc -> %{name: name, description: desc} end) + |> Map.values() document = %{ sequential: Map.get(params, "sequential", true), - steps: Enum.map(Map.values(steps), fn val -> %{name: val} end) + steps: steps } checklist_params = checklist_params diff --git a/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex b/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex index 435263b..df75322 100644 --- a/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex +++ b/lib/cklist_web/controllers/checklist_html/checklist_form.html.heex @@ -11,7 +11,10 @@ Steps <%= for {step, index} <- Enum.with_index(f[:document].value.steps) do %> - <.input name={"step-#{index}"} type="text" value={step.name} /> +
+ <.input name={"step-#{index}"} type="text" value={step.name} /> + <.input name={"desc-#{index}"} type="text" value={step.description} /> +
<% end %> <.my_button phx-click="step_add">Add step diff --git a/lib/cklist_web/controllers/checklist_html/show.html.heex b/lib/cklist_web/controllers/checklist_html/show.html.heex index 6dbba68..578bd01 100644 --- a/lib/cklist_web/controllers/checklist_html/show.html.heex +++ b/lib/cklist_web/controllers/checklist_html/show.html.heex @@ -18,11 +18,14 @@ <:item title="Access"><%= @checklist.access %> <:item title="Sequential"><%= @checklist.document["sequential"] %> <:item title="Steps"> - + <:item title="Document version"><%= @checklist.document["version"] %> diff --git a/lib/cklist_web/live/cklist_new_live.ex b/lib/cklist_web/live/cklist_new_live.ex index 3e2b49d..9b2236f 100644 --- a/lib/cklist_web/live/cklist_new_live.ex +++ b/lib/cklist_web/live/cklist_new_live.ex @@ -15,7 +15,10 @@ defmodule CklistWeb.CklistNewLive do document: %{ version: "0.1", sequential: true, - steps: [%{ name: "one" }, %{ name: "two" }] + steps: [ + %{ name: "Step one", description: "Lorem ipsum" }, + %{ name: "Step two", description: "dolor sit amet" } + ] }, user_id: socket.assigns.current_user.id, } @@ -52,7 +55,7 @@ defmodule CklistWeb.CklistNewLive do socket.assigns.changeset.changes, %{ document: %{ sequential: document.sequential, - steps: document.steps ++ [%{name: "New step"}], + steps: document.steps ++ [%{name: "New step", description: "with description"}], version: document.version, }} ) @@ -112,10 +115,15 @@ defmodule CklistWeb.CklistNewLive do defp document_from(params, socket) do document = latest_document(socket) steps = Map.filter(params, fn {key, _val} -> String.starts_with?(key, "step-") end) + descriptions = Map.filter(params, fn {key, _val} -> String.starts_with?(key, "desc-") end) + |> Enum.map(fn {key, val} -> {String.replace(key, "desc", "step"), val} end ) + |> Map.new() + steps = Map.merge(steps, descriptions, fn _key, name, desc -> %{name: name, description: desc} end) + |> Map.values() %{ sequential: (if Map.get(params, "sequential", document.sequential) == "true", do: true, else: false), - steps: Enum.map(Map.values(steps), fn val -> %{name: val} end), + steps: steps, version: document.version, } end