diff --git a/lib/radiator/outline.ex b/lib/radiator/outline.ex new file mode 100644 index 00000000..6f6e6ffe --- /dev/null +++ b/lib/radiator/outline.ex @@ -0,0 +1,104 @@ +defmodule Radiator.Outline do + @moduledoc """ + The Outline context. + """ + + import Ecto.Query, warn: false + alias Radiator.Repo + + alias Radiator.Outline.Node + + @doc """ + Returns the list of nodes. + + ## Examples + + iex> list_nodes() + [%Node{}, ...] + + """ + def list_nodes do + Repo.all(Node) + end + + @doc """ + Gets a single node. + + Raises `Ecto.NoResultsError` if the Node does not exist. + + ## Examples + + iex> get_node!(123) + %Node{} + + iex> get_node!(456) + ** (Ecto.NoResultsError) + + """ + def get_node!(id), do: Repo.get!(Node, id) + + @doc """ + Creates a node. + + ## Examples + + iex> create_node(%{field: value}) + {:ok, %Node{}} + + iex> create_node(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_node(attrs \\ %{}) do + %Node{} + |> Node.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a node. + + ## Examples + + iex> update_node(node, %{field: new_value}) + {:ok, %Node{}} + + iex> update_node(node, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_node(%Node{} = node, attrs) do + node + |> Node.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a node. + + ## Examples + + iex> delete_node(node) + {:ok, %Node{}} + + iex> delete_node(node) + {:error, %Ecto.Changeset{}} + + """ + def delete_node(%Node{} = node) do + Repo.delete(node) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking node changes. + + ## Examples + + iex> change_node(node) + %Ecto.Changeset{data: %Node{}} + + """ + def change_node(%Node{} = node, attrs \\ %{}) do + Node.changeset(node, attrs) + end +end diff --git a/lib/radiator/outline/node.ex b/lib/radiator/outline/node.ex new file mode 100644 index 00000000..ba3fd51c --- /dev/null +++ b/lib/radiator/outline/node.ex @@ -0,0 +1,22 @@ +defmodule Radiator.Outline.Node do + use Ecto.Schema + import Ecto.Changeset + + @primary_key {:uuid, :binary_id, autogenerate: true} + schema "outline_nodes" do + field :content, :string + + timestamps(type: :utc_datetime) + end + + @fields [ + :content + ] + + @doc false + def changeset(node, attrs) do + node + |> cast(attrs, @fields) + |> validate_required(@fields) + end +end diff --git a/lib/radiator_web/live/outline_live/index.ex b/lib/radiator_web/live/outline_live/index.ex index 817252d6..b5c8d652 100644 --- a/lib/radiator_web/live/outline_live/index.ex +++ b/lib/radiator_web/live/outline_live/index.ex @@ -1,10 +1,43 @@ defmodule RadiatorWeb.OutlineLive.Index do use RadiatorWeb, :live_view + alias Radiator.Outline + alias Radiator.Outline.Node + @impl true def mount(_params, _session, socket) do + node = %Node{} + changeset = Outline.change_node(node) + socket |> assign(:page_title, "Outline") + |> assign(:node, node) + |> assign(:form, to_form(changeset)) + |> stream_configure(:nodes, dom_id: &"node-#{&1.uuid}") + |> stream(:nodes, Outline.list_nodes()) |> reply(:ok) end + + @impl true + def handle_event("update", %{"node" => _params}, socket) do + socket + |> reply(:noreply) + end + + @impl true + def handle_event("next", %{"node" => params}, socket) do + {:ok, node} = Outline.create_node(params) + + socket + |> stream_insert(:nodes, node, at: 0) + |> reply(:noreply) + end + + @impl true + def handle_event("delete", %{"uuid" => uuid}, socket) do + node = Outline.get_node!(uuid) + {:ok, _} = Outline.delete_node(node) + + {:noreply, stream_delete(socket, :nodes, node)} + end end diff --git a/lib/radiator_web/live/outline_live/index.html.heex b/lib/radiator_web/live/outline_live/index.html.heex index 5e99304d..e2bce383 100644 --- a/lib/radiator_web/live/outline_live/index.html.heex +++ b/lib/radiator_web/live/outline_live/index.html.heex @@ -1,4 +1,25 @@

Outline

+

Inbox

+ + <.focus_wrap id="input-form-wrap"> + <.form id="inbox-form" for={@form} phx-change="update" phx-submit="next"> + <.input type="text" field={@form[:content]} /> + <.input type="hidden" field={@form[:uuid]} /> + + + +
diff --git a/priv/repo/migrations/20231120103619_create_outline_nodes.exs b/priv/repo/migrations/20231120103619_create_outline_nodes.exs new file mode 100644 index 00000000..d09590cf --- /dev/null +++ b/priv/repo/migrations/20231120103619_create_outline_nodes.exs @@ -0,0 +1,12 @@ +defmodule Radiator.Repo.Migrations.CreateOutlineNodes do + use Ecto.Migration + + def change do + create table(:outline_nodes, primary_key: false) do + add :uuid, :uuid, primary_key: true + add :content, :text + + timestamps(type: :utc_datetime) + end + end +end diff --git a/test/radiator/outline_test.exs b/test/radiator/outline_test.exs new file mode 100644 index 00000000..e80e74ab --- /dev/null +++ b/test/radiator/outline_test.exs @@ -0,0 +1,59 @@ +defmodule Radiator.OutlineTest do + use Radiator.DataCase + + alias Radiator.Outline + + describe "outline_nodes" do + alias Radiator.Outline.Node + + import Radiator.OutlineFixtures + + @invalid_attrs %{content: nil} + + test "list_nodes/0 returns all nodes" do + node = node_fixture() + assert Outline.list_nodes() == [node] + end + + test "get_node!/1 returns the node with given id" do + node = node_fixture() + assert Outline.get_node!(node.uuid) == node + end + + test "create_node/1 with valid data creates a node" do + valid_attrs = %{content: "some content"} + + assert {:ok, %Node{} = node} = Outline.create_node(valid_attrs) + assert node.content == "some content" + end + + test "create_node/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Outline.create_node(@invalid_attrs) + end + + test "update_node/2 with valid data updates the node" do + node = node_fixture() + update_attrs = %{content: "some updated content"} + + assert {:ok, %Node{} = node} = Outline.update_node(node, update_attrs) + assert node.content == "some updated content" + end + + test "update_node/2 with invalid data returns error changeset" do + node = node_fixture() + assert {:error, %Ecto.Changeset{}} = Outline.update_node(node, @invalid_attrs) + assert node == Outline.get_node!(node.uuid) + end + + test "delete_node/1 deletes the node" do + node = node_fixture() + assert {:ok, %Node{}} = Outline.delete_node(node) + assert_raise Ecto.NoResultsError, fn -> Outline.get_node!(node.uuid) end + end + + test "change_node/1 returns a node changeset" do + node = node_fixture() + assert %Ecto.Changeset{} = Outline.change_node(node) + end + end +end diff --git a/test/radiator_web/live/outline_live_test.exs b/test/radiator_web/live/outline_live_test.exs index 0174fd8a..e8fc6818 100644 --- a/test/radiator_web/live/outline_live_test.exs +++ b/test/radiator_web/live/outline_live_test.exs @@ -3,10 +3,12 @@ defmodule RadiatorWeb.OutlineLiveTest do import Phoenix.LiveViewTest import Radiator.AccountsFixtures + import Radiator.OutlineFixtures - describe "Outline page" do + describe "Outline page is restricted" do test "can render if user is logged in", %{conn: conn} do - {:ok, _live, html} = conn |> log_in_user(user_fixture()) |> live(~p"/admin/outline") + user = user_fixture() + {:ok, _live, html} = conn |> log_in_user(user) |> live(~p"/admin/outline") assert html =~ "Outline" end @@ -23,15 +25,38 @@ defmodule RadiatorWeb.OutlineLiveTest do describe "Outline page has an inbox" do setup %{conn: conn} do user = user_fixture() - %{conn: log_in_user(conn, user)} + node = node_fixture() + %{conn: log_in_user(conn, user), node: node} + end + + test "lists all nodes", %{conn: conn, node: node} do + {:ok, _live, html} = live(conn, ~p"/admin/outline") + + assert html =~ "Inbox" + assert html =~ node.content + end + + test "save new node", %{conn: conn} do + {:ok, live, _html} = live(conn, ~p"/admin/outline") + + assert live + |> form("#inbox-form", node: %{content: "new node content"}) + |> render_submit() + + assert live + |> element("ul#inbox") + |> render() =~ "new node content" end - test "inbox has a headline", %{conn: conn} do + test "delete existing node", %{conn: conn, node: node} do {:ok, live, _html} = live(conn, ~p"/admin/outline") assert live - |> element("h2", "Inbox") - |> render() =~ "Inbox" + |> element("#node-#{node.uuid} a", "Delete") + |> render_click() + + refute live + |> has_element?("#node-#{node.uuid}") end end end diff --git a/test/support/fixtures/outline_fixtures.ex b/test/support/fixtures/outline_fixtures.ex new file mode 100644 index 00000000..6150a4bf --- /dev/null +++ b/test/support/fixtures/outline_fixtures.ex @@ -0,0 +1,18 @@ +defmodule Radiator.OutlineFixtures do + @moduledoc """ + This module defines test helpers for creating + entities via the `Radiator.Outline` context. + """ + + @doc """ + Generate a node. + """ + def node_fixture(attrs \\ %{}) do + {:ok, node} = + attrs + |> Enum.into(%{content: "some content"}) + |> Radiator.Outline.create_node() + + node + end +end