From 6b828b2574b506fe18fdc2f689697b42d4bb34e8 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 14 Jun 2025 19:02:34 +0200 Subject: [PATCH] add context Core with resource Message --- lib/scopes/core.ex | 104 ++++++++++++++++ lib/scopes/core/message.ex | 21 ++++ .../live/message_live/form_component.ex | 85 +++++++++++++ lib/scopes_web/live/message_live/index.ex | 47 ++++++++ .../live/message_live/index.html.heex | 45 +++++++ lib/scopes_web/live/message_live/show.ex | 21 ++++ .../live/message_live/show.html.heex | 30 +++++ lib/scopes_web/router.ex | 7 ++ .../20250614165203_create_messages.exs | 15 +++ test/scopes/core_test.exs | 67 +++++++++++ test/scopes_web/live/message_live_test.exs | 113 ++++++++++++++++++ test/support/fixtures/core_fixtures.ex | 24 ++++ 12 files changed, 579 insertions(+) create mode 100644 lib/scopes/core.ex create mode 100644 lib/scopes/core/message.ex create mode 100644 lib/scopes_web/live/message_live/form_component.ex create mode 100644 lib/scopes_web/live/message_live/index.ex create mode 100644 lib/scopes_web/live/message_live/index.html.heex create mode 100644 lib/scopes_web/live/message_live/show.ex create mode 100644 lib/scopes_web/live/message_live/show.html.heex create mode 100644 priv/repo/migrations/20250614165203_create_messages.exs create mode 100644 test/scopes/core_test.exs create mode 100644 test/scopes_web/live/message_live_test.exs create mode 100644 test/support/fixtures/core_fixtures.ex diff --git a/lib/scopes/core.ex b/lib/scopes/core.ex new file mode 100644 index 0000000..b1dbe54 --- /dev/null +++ b/lib/scopes/core.ex @@ -0,0 +1,104 @@ +defmodule Scopes.Core do + @moduledoc """ + The Core context. + """ + + import Ecto.Query, warn: false + alias Scopes.Repo + + alias Scopes.Core.Message + + @doc """ + Returns the list of messages. + + ## Examples + + iex> list_messages() + [%Message{}, ...] + + """ + def list_messages do + Repo.all(Message) + end + + @doc """ + Gets a single message. + + Raises `Ecto.NoResultsError` if the Message does not exist. + + ## Examples + + iex> get_message!(123) + %Message{} + + iex> get_message!(456) + ** (Ecto.NoResultsError) + + """ + def get_message!(id), do: Repo.get!(Message, id) + + @doc """ + Creates a message. + + ## Examples + + iex> create_message(%{field: value}) + {:ok, %Message{}} + + iex> create_message(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_message(attrs \\ %{}) do + %Message{} + |> Message.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a message. + + ## Examples + + iex> update_message(message, %{field: new_value}) + {:ok, %Message{}} + + iex> update_message(message, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_message(%Message{} = message, attrs) do + message + |> Message.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a message. + + ## Examples + + iex> delete_message(message) + {:ok, %Message{}} + + iex> delete_message(message) + {:error, %Ecto.Changeset{}} + + """ + def delete_message(%Message{} = message) do + Repo.delete(message) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking message changes. + + ## Examples + + iex> change_message(message) + %Ecto.Changeset{data: %Message{}} + + """ + def change_message(%Message{} = message, attrs \\ %{}) do + Message.changeset(message, attrs) + end +end diff --git a/lib/scopes/core/message.ex b/lib/scopes/core/message.ex new file mode 100644 index 0000000..baa4926 --- /dev/null +++ b/lib/scopes/core/message.ex @@ -0,0 +1,21 @@ +defmodule Scopes.Core.Message do + use Ecto.Schema + import Ecto.Changeset + + schema "messages" do + field :data, :map + field :domain, :string + field :item, :string + field :action, :string + field :class, :string + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(message, attrs) do + message + |> cast(attrs, [:domain, :action, :class, :item, :data]) + |> validate_required([:domain, :action, :class, :item]) + end +end diff --git a/lib/scopes_web/live/message_live/form_component.ex b/lib/scopes_web/live/message_live/form_component.ex new file mode 100644 index 0000000..f215cfb --- /dev/null +++ b/lib/scopes_web/live/message_live/form_component.ex @@ -0,0 +1,85 @@ +defmodule ScopesWeb.MessageLive.FormComponent do + use ScopesWeb, :live_component + + alias Scopes.Core + + @impl true + def render(assigns) do + ~H""" +
+ <.header> + {@title} + <:subtitle>Use this form to manage message records in your database. + + + <.simple_form + for={@form} + id="message-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + > + <.input field={@form[:domain]} type="text" label="Domain" /> + <.input field={@form[:action]} type="text" label="Action" /> + <.input field={@form[:class]} type="text" label="Class" /> + <.input field={@form[:item]} type="text" label="Item" /> + <:actions> + <.button phx-disable-with="Saving...">Save Message + + +
+ """ + end + + @impl true + def update(%{message: message} = assigns, socket) do + {:ok, + socket + |> assign(assigns) + |> assign_new(:form, fn -> + to_form(Core.change_message(message)) + end)} + end + + @impl true + def handle_event("validate", %{"message" => message_params}, socket) do + changeset = Core.change_message(socket.assigns.message, message_params) + {:noreply, assign(socket, form: to_form(changeset, action: :validate))} + end + + def handle_event("save", %{"message" => message_params}, socket) do + save_message(socket, socket.assigns.action, message_params) + end + + defp save_message(socket, :edit, message_params) do + case Core.update_message(socket.assigns.message, message_params) do + {:ok, message} -> + notify_parent({:saved, message}) + + {:noreply, + socket + |> put_flash(:info, "Message updated successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} + end + end + + defp save_message(socket, :new, message_params) do + case Core.create_message(message_params) do + {:ok, message} -> + notify_parent({:saved, message}) + + {:noreply, + socket + |> put_flash(:info, "Message created successfully") + |> push_patch(to: socket.assigns.patch)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} + end + end + + defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) +end diff --git a/lib/scopes_web/live/message_live/index.ex b/lib/scopes_web/live/message_live/index.ex new file mode 100644 index 0000000..ede0569 --- /dev/null +++ b/lib/scopes_web/live/message_live/index.ex @@ -0,0 +1,47 @@ +defmodule ScopesWeb.MessageLive.Index do + use ScopesWeb, :live_view + + alias Scopes.Core + alias Scopes.Core.Message + + @impl true + def mount(_params, _session, socket) do + {:ok, stream(socket, :messages, Core.list_messages())} + end + + @impl true + def handle_params(params, _url, socket) do + {:noreply, apply_action(socket, socket.assigns.live_action, params)} + end + + defp apply_action(socket, :edit, %{"id" => id}) do + socket + |> assign(:page_title, "Edit Message") + |> assign(:message, Core.get_message!(id)) + end + + defp apply_action(socket, :new, _params) do + socket + |> assign(:page_title, "New Message") + |> assign(:message, %Message{}) + end + + defp apply_action(socket, :index, _params) do + socket + |> assign(:page_title, "Listing Messages") + |> assign(:message, nil) + end + + @impl true + def handle_info({ScopesWeb.MessageLive.FormComponent, {:saved, message}}, socket) do + {:noreply, stream_insert(socket, :messages, message)} + end + + @impl true + def handle_event("delete", %{"id" => id}, socket) do + message = Core.get_message!(id) + {:ok, _} = Core.delete_message(message) + + {:noreply, stream_delete(socket, :messages, message)} + end +end diff --git a/lib/scopes_web/live/message_live/index.html.heex b/lib/scopes_web/live/message_live/index.html.heex new file mode 100644 index 0000000..52ca8c4 --- /dev/null +++ b/lib/scopes_web/live/message_live/index.html.heex @@ -0,0 +1,45 @@ +<.header> + Listing Messages + <:actions> + <.link patch={~p"/messages/new"}> + <.button>New Message + + + + +<.table + id="messages" + rows={@streams.messages} + row_click={fn {_id, message} -> JS.navigate(~p"/messages/#{message}") end} +> + <:col :let={{_id, message}} label="Domain">{message.domain} + <:col :let={{_id, message}} label="Action">{message.action} + <:col :let={{_id, message}} label="Class">{message.class} + <:col :let={{_id, message}} label="Item">{message.item} + <:col :let={{_id, message}} label="Data">{message.data} + <:action :let={{_id, message}}> +
+ <.link navigate={~p"/messages/#{message}"}>Show +
+ <.link patch={~p"/messages/#{message}/edit"}>Edit + + <:action :let={{id, message}}> + <.link + phx-click={JS.push("delete", value: %{id: message.id}) |> hide("##{id}")} + data-confirm="Are you sure?" + > + Delete + + + + +<.modal :if={@live_action in [:new, :edit]} id="message-modal" show on_cancel={JS.patch(~p"/messages")}> + <.live_component + module={ScopesWeb.MessageLive.FormComponent} + id={@message.id || :new} + title={@page_title} + action={@live_action} + message={@message} + patch={~p"/messages"} + /> + diff --git a/lib/scopes_web/live/message_live/show.ex b/lib/scopes_web/live/message_live/show.ex new file mode 100644 index 0000000..e119917 --- /dev/null +++ b/lib/scopes_web/live/message_live/show.ex @@ -0,0 +1,21 @@ +defmodule ScopesWeb.MessageLive.Show do + use ScopesWeb, :live_view + + alias Scopes.Core + + @impl true + def mount(_params, _session, socket) do + {:ok, socket} + end + + @impl true + def handle_params(%{"id" => id}, _, socket) do + {:noreply, + socket + |> assign(:page_title, page_title(socket.assigns.live_action)) + |> assign(:message, Core.get_message!(id))} + end + + defp page_title(:show), do: "Show Message" + defp page_title(:edit), do: "Edit Message" +end diff --git a/lib/scopes_web/live/message_live/show.html.heex b/lib/scopes_web/live/message_live/show.html.heex new file mode 100644 index 0000000..6c27dd3 --- /dev/null +++ b/lib/scopes_web/live/message_live/show.html.heex @@ -0,0 +1,30 @@ +<.header> + Message {@message.id} + <:subtitle>This is a message record from your database. + <:actions> + <.link patch={~p"/messages/#{@message}/show/edit"} phx-click={JS.push_focus()}> + <.button>Edit message + + + + +<.list> + <:item title="Domain">{@message.domain} + <:item title="Action">{@message.action} + <:item title="Class">{@message.class} + <:item title="Item">{@message.item} + <:item title="Data">{@message.data} + + +<.back navigate={~p"/messages"}>Back to messages + +<.modal :if={@live_action == :edit} id="message-modal" show on_cancel={JS.patch(~p"/messages/#{@message}")}> + <.live_component + module={ScopesWeb.MessageLive.FormComponent} + id={@message.id} + title={@page_title} + action={@live_action} + message={@message} + patch={~p"/messages/#{@message}"} + /> + diff --git a/lib/scopes_web/router.ex b/lib/scopes_web/router.ex index 14d2e02..31d4e8f 100644 --- a/lib/scopes_web/router.ex +++ b/lib/scopes_web/router.ex @@ -72,6 +72,13 @@ defmodule ScopesWeb.Router do live "/users/settings", UserSettingsLive, :edit live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email live "/bingo", Bingo + + live "/messages", MessageLive.Index, :index + live "/messages/new", MessageLive.Index, :new + live "/messages/:id/edit", MessageLive.Index, :edit + live "/messages/:id", MessageLive.Show, :show + live "/messages/:id/show/edit", MessageLive.Show, :edit + end end diff --git a/priv/repo/migrations/20250614165203_create_messages.exs b/priv/repo/migrations/20250614165203_create_messages.exs new file mode 100644 index 0000000..663d012 --- /dev/null +++ b/priv/repo/migrations/20250614165203_create_messages.exs @@ -0,0 +1,15 @@ +defmodule Scopes.Repo.Migrations.CreateMessages do + use Ecto.Migration + + def change do + create table(:messages) do + add :domain, :string + add :action, :string + add :class, :string + add :item, :string + add :data, :map + + timestamps(type: :utc_datetime) + end + end +end diff --git a/test/scopes/core_test.exs b/test/scopes/core_test.exs new file mode 100644 index 0000000..3b20f07 --- /dev/null +++ b/test/scopes/core_test.exs @@ -0,0 +1,67 @@ +defmodule Scopes.CoreTest do + use Scopes.DataCase + + alias Scopes.Core + + describe "messages" do + alias Scopes.Core.Message + + import Scopes.CoreFixtures + + @invalid_attrs %{data: nil, domain: nil, item: nil, action: nil, class: nil} + + test "list_messages/0 returns all messages" do + message = message_fixture() + assert Core.list_messages() == [message] + end + + test "get_message!/1 returns the message with given id" do + message = message_fixture() + assert Core.get_message!(message.id) == message + end + + test "create_message/1 with valid data creates a message" do + valid_attrs = %{data: %{}, domain: "some domain", item: "some item", action: "some action", class: "some class"} + + assert {:ok, %Message{} = message} = Core.create_message(valid_attrs) + assert message.data == %{} + assert message.domain == "some domain" + assert message.item == "some item" + assert message.action == "some action" + assert message.class == "some class" + end + + test "create_message/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Core.create_message(@invalid_attrs) + end + + test "update_message/2 with valid data updates the message" do + message = message_fixture() + update_attrs = %{data: %{}, domain: "some updated domain", item: "some updated item", action: "some updated action", class: "some updated class"} + + assert {:ok, %Message{} = message} = Core.update_message(message, update_attrs) + assert message.data == %{} + assert message.domain == "some updated domain" + assert message.item == "some updated item" + assert message.action == "some updated action" + assert message.class == "some updated class" + end + + test "update_message/2 with invalid data returns error changeset" do + message = message_fixture() + assert {:error, %Ecto.Changeset{}} = Core.update_message(message, @invalid_attrs) + assert message == Core.get_message!(message.id) + end + + test "delete_message/1 deletes the message" do + message = message_fixture() + assert {:ok, %Message{}} = Core.delete_message(message) + assert_raise Ecto.NoResultsError, fn -> Core.get_message!(message.id) end + end + + test "change_message/1 returns a message changeset" do + message = message_fixture() + assert %Ecto.Changeset{} = Core.change_message(message) + end + end +end diff --git a/test/scopes_web/live/message_live_test.exs b/test/scopes_web/live/message_live_test.exs new file mode 100644 index 0000000..7bf58fc --- /dev/null +++ b/test/scopes_web/live/message_live_test.exs @@ -0,0 +1,113 @@ +defmodule ScopesWeb.MessageLiveTest do + use ScopesWeb.ConnCase + + import Phoenix.LiveViewTest + import Scopes.CoreFixtures + + @create_attrs %{data: %{}, domain: "some domain", item: "some item", action: "some action", class: "some class"} + @update_attrs %{data: %{}, domain: "some updated domain", item: "some updated item", action: "some updated action", class: "some updated class"} + @invalid_attrs %{data: nil, domain: nil, item: nil, action: nil, class: nil} + + defp create_message(_) do + message = message_fixture() + %{message: message} + end + + describe "Index" do + setup [:create_message] + + test "lists all messages", %{conn: conn, message: message} do + {:ok, _index_live, html} = live(conn, ~p"/messages") + + assert html =~ "Listing Messages" + assert html =~ message.domain + end + + test "saves new message", %{conn: conn} do + {:ok, index_live, _html} = live(conn, ~p"/messages") + + assert index_live |> element("a", "New Message") |> render_click() =~ + "New Message" + + assert_patch(index_live, ~p"/messages/new") + + assert index_live + |> form("#message-form", message: @invalid_attrs) + |> render_change() =~ "can't be blank" + + assert index_live + |> form("#message-form", message: @create_attrs) + |> render_submit() + + assert_patch(index_live, ~p"/messages") + + html = render(index_live) + assert html =~ "Message created successfully" + assert html =~ "some domain" + end + + test "updates message in listing", %{conn: conn, message: message} do + {:ok, index_live, _html} = live(conn, ~p"/messages") + + assert index_live |> element("#messages-#{message.id} a", "Edit") |> render_click() =~ + "Edit Message" + + assert_patch(index_live, ~p"/messages/#{message}/edit") + + assert index_live + |> form("#message-form", message: @invalid_attrs) + |> render_change() =~ "can't be blank" + + assert index_live + |> form("#message-form", message: @update_attrs) + |> render_submit() + + assert_patch(index_live, ~p"/messages") + + html = render(index_live) + assert html =~ "Message updated successfully" + assert html =~ "some updated domain" + end + + test "deletes message in listing", %{conn: conn, message: message} do + {:ok, index_live, _html} = live(conn, ~p"/messages") + + assert index_live |> element("#messages-#{message.id} a", "Delete") |> render_click() + refute has_element?(index_live, "#messages-#{message.id}") + end + end + + describe "Show" do + setup [:create_message] + + test "displays message", %{conn: conn, message: message} do + {:ok, _show_live, html} = live(conn, ~p"/messages/#{message}") + + assert html =~ "Show Message" + assert html =~ message.domain + end + + test "updates message within modal", %{conn: conn, message: message} do + {:ok, show_live, _html} = live(conn, ~p"/messages/#{message}") + + assert show_live |> element("a", "Edit") |> render_click() =~ + "Edit Message" + + assert_patch(show_live, ~p"/messages/#{message}/show/edit") + + assert show_live + |> form("#message-form", message: @invalid_attrs) + |> render_change() =~ "can't be blank" + + assert show_live + |> form("#message-form", message: @update_attrs) + |> render_submit() + + assert_patch(show_live, ~p"/messages/#{message}") + + html = render(show_live) + assert html =~ "Message updated successfully" + assert html =~ "some updated domain" + end + end +end diff --git a/test/support/fixtures/core_fixtures.ex b/test/support/fixtures/core_fixtures.ex new file mode 100644 index 0000000..dffc5b8 --- /dev/null +++ b/test/support/fixtures/core_fixtures.ex @@ -0,0 +1,24 @@ +defmodule Scopes.CoreFixtures do + @moduledoc """ + This module defines test helpers for creating + entities via the `Scopes.Core` context. + """ + + @doc """ + Generate a message. + """ + def message_fixture(attrs \\ %{}) do + {:ok, message} = + attrs + |> Enum.into(%{ + action: "some action", + class: "some class", + data: %{}, + domain: "some domain", + item: "some item" + }) + |> Scopes.Core.create_message() + + message + end +end