Powered by AppSignal & Oban Pro

=============================================================================

guides/todo_app_integration.livemd

=============================================================================

ASH FORM BUILDER - TODO APP INTEGRATION GUIDE

=============================================================================

Complete step-by-step guide: From mix.exs to LiveView CRUD

=============================================================================

=============================================================================

STEP 1: ADD DEPENDENCIES (mix.exs)

=============================================================================

defmodule TodoApp.MixProject do use Mix.Project

def project do [ app: :todo_app, version: "0.1.0", elixir: "~> 1.17", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, aliases: aliases(), deps: deps() ] end

def application do [ mod: {TodoApp.Application, []}, extra_applications: [:logger, :runtime_tools] ] end

defp elixirc_paths(:test), do: ["lib", "test/support"] defp elixirc_paths(_), do: ["lib"]

defp deps do [ # Core Ash Framework {:ash, "> 3.0"}, {:ash_phoenix, "> 2.0"}, {:ash_postgres, "~> 2.0"},

  # Phoenix
  {:phoenix, "~> 1.7.14"},
  {:phoenix_html, "~> 4.0"},
  {:phoenix_live_reload, "~> 1.2", only: :dev},
  {:phoenix_live_view, "~> 1.0.0"},
  {:floki, ">= 0.30.0", only: :test},
  {:phoenix_live_reload, "~> 1.2", only: :dev},
  
  # ASH FORM BUILDER ⭐
  {:ash_form_builder, path: "../ash_form_builder"},  # Local path
  # OR from git: {:ash_form_builder, git: "https://github.com/nagieeb0/ash_form_builder.git"},
  # OR from hex (when published): {:ash_form_builder, "~> 0.1.0"}
  
  # UI Components (Optional - for MishkaTheme)
  {:mishka_chelekom, "~> 0.0.8"},
  
  # Database
  {:ecto_sql, "~> 3.10"},
  {:postgrex, ">= 0.0.0"},
  
  # Other
  {:telemetry_metrics, "~> 1.0"},
  {:telemetry_poller, "~> 1.0"},
  {:gettext, "~> 0.20"},
  {:jason, "~> 1.2"},
  {:dns_cluster, "~> 0.1.1"},
  {:bandit, "~> 1.5"}
]

end

defp aliases do [ setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"], "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.reset": ["ecto.drop", "ecto.setup"], test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"] ] end end

=============================================================================

STEP 2: CONFIGURATION (config/config.exs)

=============================================================================

import Config

Configure AshFormBuilder theme

config :ash_form_builder, :theme, AshFormBuilder.Theme.MishkaTheme

OR use default: config :ash_form_builder, :theme, AshFormBuilder.Themes.Default

Configure Ash

config :ash, :include_embedded_source_by_default?, true

Configure your endpoint

config :todo_app, TodoAppWeb.Endpoint, url: [host: "localhost"], adapter: Bandit.PhoenixAdapter, render_errors: [ formats: [html: TodoAppWeb.ErrorHTML, json: TodoAppWeb.ErrorJSON], layout: false ], pubsub_server: TodoApp.PubSub, live_view: [signing_salt: "your-signing-salt"]

Configure your Repo

config :todo_app, TodoApp.Repo, database: Path.expand("../todo_app_dev.db", Path.dirname(ENV.file)), stacktrace: true, show_sensitive_data_on_connection_error: true, pool_size: 10, pool: Ecto.Adapters.SQL.Sandbox

Configure Ash domains

config :todo_app, ash_domains: [TodoApp.Todos]

=============================================================================

STEP 3: ASH DOMAIN (lib/todo_app/todos.ex)

=============================================================================

defmodule TodoApp.Todos do @moduledoc """ Todos domain - manages tasks, categories, and tags.

Code Interfaces

This domain generates form helper functions via define :form_to_*:

  • TodoApp.Todos.Task.Form.for_create/1
  • TodoApp.Todos.Task.Form.for_update/2

These helpers integrate seamlessly with AshFormBuilder. """

use Ash.Domain

resources do # Task resource with form code interfaces resource TodoApp.Todos.Task do # Standard CRUD define :list_tasks, action: :read define :get_task, action: :read, get_by: [:id] define :destroy_task, action: :destroy

  # ⭐ Form Code Interfaces - generates Form helpers
  define :form_to_create_task, action: :create
  define :form_to_update_task, action: :update
end

# Category resource
resource TodoApp.Todos.Category do
  define :list_categories, action: :read
  define :search_categories, action: :read
  define :form_to_create_category, action: :create
end

# Tag resource (creatable on-the-fly)
resource TodoApp.Todos.Tag do
  define :list_tags, action: :read
  define :search_tags, action: :read
  define :form_to_create_tag, action: :create
end

end end

=============================================================================

STEP 4: ASH RESOURCES

=============================================================================

─────────────────────────────────────────────────────────────────────────────

4.1 TASK RESOURCE (Main Todo Item)

─────────────────────────────────────────────────────────────────────────────

defmodule TodoApp.Todos.Task do @moduledoc """ Task resource - represents a single todo item.

Features:

  • Title, description, due date
  • Priority and status enums
  • Many-to-many with Categories
  • Many-to-many with Tags (creatable!) """

use Ash.Resource, domain: TodoApp.Todos, data_layer: AshPostgres.DataLayer, extensions: [AshFormBuilder] # ⭐ Required for AshFormBuilder

postgres do table "tasks" repo TodoApp.Repo end

───────────────────────────────────────────────────────────────────────────

Attributes

───────────────────────────────────────────────────────────────────────────

attributes do uuid_primary_key :id

attribute :title, :string do
  allow_nil? false
  constraints min_length: 1, max_length: 200
end

attribute :description, :text do
  allow_nil? true
end

attribute :completed, :boolean do
  default false
end

attribute :priority, :atom do
  constraints one_of: [:low, :medium, :high, :urgent]
  default :medium
end

attribute :status, :atom do
  constraints one_of: [:pending, :in_progress, :done]
  default :pending
end

attribute :due_date, :date do
  allow_nil? true
end

timestamps()

end

───────────────────────────────────────────────────────────────────────────

Relationships

───────────────────────────────────────────────────────────────────────────

relationships do # Many-to-many with Categories (select from existing) many_to_many :categories, TodoApp.Todos.Category do through TodoApp.Todos.TaskCategory source_attribute_on_join_resource :task_id destination_attribute_on_join_resource :category_id end

# Many-to-many with Tags (creatable on-the-fly!)
many_to_many :tags, TodoApp.Todos.Tag do
  through TodoApp.Todos.TaskTag
  source_attribute_on_join_resource :task_id
  destination_attribute_on_join_resource :tag_id
end

end

───────────────────────────────────────────────────────────────────────────

Actions

───────────────────────────────────────────────────────────────────────────

actions do defaults [:read]

create :create do
  accept [:title, :description, :completed, :priority, :status, :due_date]
  # Manage relationships
  manage_relationship :categories, :categories, type: :append_and_remove
  manage_relationship :tags, :tags, type: :append_and_remove
end

update :update do
  accept [:title, :description, :completed, :priority, :status, :due_date]
  manage_relationship :categories, :categories, type: :append_and_remove
  manage_relationship :tags, :tags, type: :append_and_remove
end

destroy :destroy do
  primary? true
  require_atomic? false
end

end

───────────────────────────────────────────────────────────────────────────

Validations

───────────────────────────────────────────────────────────────────────────

validations do validate present([:title]) validate string_length(:title, min: 1, max: 200) validate expression(:due_date, fn task, _ -> if task.due_date && Date.compare(task.due_date, Date.utc_today()) == :lt do {:error, "due date cannot be in the past"} else :ok end end) end

───────────────────────────────────────────────────────────────────────────

Policies

───────────────────────────────────────────────────────────────────────────

policies do policy action_type(:create) do authorize_if actor_present() end

policy action_type(:update) do
  authorize_if actor_present()
end

policy action_type(:destroy) do
  authorize_if actor_present()
end

policy action_type(:read) do
  authorize_if always()
end

end

===========================================================================

ASH FORM BUILDER DSL - Form Configuration

===========================================================================

form do action :create submit_label "Create Task" wrapper_class "space-y-6"

# ────────────────────────────────────────────────────────────────────────
# Standard Fields
# ────────────────────────────────────────────────────────────────────────

field :title do
  label "Task Title"
  placeholder "e.g., Complete project documentation"
  required true
  hint "Keep it concise but descriptive"
end

field :description do
  label "Description"
  type :textarea
  placeholder "Add any additional details..."
  rows 4
  hint "Optional: Add more context about this task"
end

field :priority do
  label "Priority"
  type :select
  options [
    {"Low", :low},
    {"Medium", :medium},
    {"High", :high},
    {"Urgent", :urgent}
  ]
  hint "How important is this task?"
end

field :status do
  label "Status"
  type :select
  options [
    {"Pending", :pending},
    {"In Progress", :in_progress},
    {"Done", :done}
  ]
end

field :due_date do
  label "Due Date"
  type :date
  hint "When should this be completed?"
end

field :completed do
  label "Completed"
  type :checkbox
  hint "Mark as complete"
end

# ────────────────────────────────────────────────────────────────────────
# Many-to-Many: Categories (NON-CREATABLE)
# ────────────────────────────────────────────────────────────────────────
# Users can only select from existing categories

field :categories do
  type :multiselect_combobox
  label "Categories"
  placeholder "Search categories..."
  required false

  opts [
    search_event: "search_categories",
    debounce: 300,
    label_key: :name,
    value_key: :id,
    hint: "Organize your task into categories"
  ]
end

# ────────────────────────────────────────────────────────────────────────
# Many-to-Many: Tags (CREATABLE!) ⭐
# ────────────────────────────────────────────────────────────────────────
# Users can create new tags on-the-fly

field :tags do
  type :multiselect_combobox
  label "Tags"
  placeholder "Search or create tags..."
  required false

  opts [
    # ★ Enable creatable functionality
    creatable: true,
    create_action: :create,
    create_label: "Create \"",
    
    search_event: "search_tags",
    debounce: 300,
    label_key: :name,
    value_key: :id,
    hint: "Add tags or create new ones instantly"
  ]
end

end end

─────────────────────────────────────────────────────────────────────────────

4.2 CATEGORY RESOURCE

─────────────────────────────────────────────────────────────────────────────

defmodule TodoApp.Todos.Category do @moduledoc "Category for organizing tasks (e.g., Work, Personal, Shopping)"

use Ash.Resource, domain: TodoApp.Todos, data_layer: AshPostgres.DataLayer

postgres do table "categories" repo TodoApp.Repo end

attributes do uuid_primary_key :id

attribute :name, :string do
  allow_nil? false
  unique true
end

attribute :color, :string do
  default "blue"
  constraints one_of: ["red", "blue", "green", "yellow", "purple", "orange"]
end

attribute :icon, :string do
  allow_nil? true
end

timestamps()

end

relationships do many_to_many :tasks, TodoApp.Todos.Task do through TodoApp.Todos.TaskCategory source_attribute_on_join_resource :category_id destination_attribute_on_join_resource :task_id end end

actions do defaults [:read, :destroy]

create :create do
  accept [:name, :color, :icon]
end

update :update do
  accept [:name, :color, :icon]
end

end

validations do validate present([:name]) end end

─────────────────────────────────────────────────────────────────────────────

4.3 TAG RESOURCE (Creatable On-the-Fly)

─────────────────────────────────────────────────────────────────────────────

defmodule TodoApp.Todos.Tag do @moduledoc "Tag for labeling tasks (e.g., #urgent, #waiting, #5min)"

use Ash.Resource, domain: TodoApp.Todos, data_layer: AshPostgres.DataLayer

postgres do table "tags" repo TodoApp.Repo end

attributes do uuid_primary_key :id

attribute :name, :string do
  allow_nil? false
  unique true
end

timestamps()

end

relationships do many_to_many :tasks, TodoApp.Todos.Task do through TodoApp.Todos.TaskTag source_attribute_on_join_resource :tag_id destination_attribute_on_join_resource :task_id end end

actions do defaults [:read, :destroy]

create :create do
  accept [:name]
end

update :update do
  accept [:name]
end

end

validations do validate present([:name]) validate string_length(:name, min: 1, max: 50) end end

─────────────────────────────────────────────────────────────────────────────

4.4 JOIN RESOURCES

─────────────────────────────────────────────────────────────────────────────

defmodule TodoApp.Todos.TaskCategory do use Ash.Resource, domain: TodoApp.Todos, data_layer: AshPostgres.DataLayer

postgres do table "tasks_categories" repo TodoApp.Repo end

attributes do uuid_primary_key :id attribute :task_id, :uuid, allow_nil?: false attribute :category_id, :uuid, allow_nil?: false end

relationships do belongs_to :task, TodoApp.Todos.Task belongs_to :category, TodoApp.Todos.Category end end

defmodule TodoApp.Todos.TaskTag do use Ash.Resource, domain: TodoApp.Todos, data_layer: AshPostgres.DataLayer

postgres do table "tasks_tags" repo TodoApp.Repo end

attributes do uuid_primary_key :id attribute :task_id, :uuid, allow_nil?: false attribute :tag_id, :uuid, allow_nil?: false end

relationships do belongs_to :task, TodoApp.Todos.Task belongs_to :tag, TodoApp.Todos.Tag end end

=============================================================================

STEP 5: PHOENIX LIVEVIEW - TASK FORM

=============================================================================

defmodule TodoAppWeb.TaskLive.Form do @moduledoc """ LiveView for creating and updating tasks.

Features:

  • Zero manual AshPhoenix.Form calls
  • Automatic form generation via AshFormBuilder
  • Searchable combobox for categories
  • Creatable combobox for tags
  • Real-time validation """

use TodoAppWeb, :live_view

alias TodoApp.Todos alias TodoApp.Todos.Task

───────────────────────────────────────────────────────────────────────────

MOUNT - Initialize Form

───────────────────────────────────────────────────────────────────────────

@impl true def mount(%{"id" => id} = _params, _session, socket) do # EDIT MODE: Update existing task task = Todos.get_task!(id, load: [:categories, :tags], actor: socket.assigns.current_user)

form = Task.Form.for_update(task, actor: socket.assigns.current_user)

{:ok,
 socket
 |> assign(:page_title, "Edit Task")
 |> assign(:form, form)
 |> assign(:task, task)
 |> assign(:mode, :edit)
 |> assign(:category_options, load_options(task.categories))
 |> assign(:tag_options, load_options(task.tags))}

end

def mount(_params, _session, socket) do # CREATE MODE: New task form = Task.Form.for_create(actor: socket.assigns.current_user)

{:ok,
 socket
 |> assign(:page_title, "New Task")
 |> assign(:form, form)
 |> assign(:task, nil)
 |> assign(:mode, :create)
 |> assign(:category_options, [])
 |> assign(:tag_options, [])}

end

───────────────────────────────────────────────────────────────────────────

RENDER - Form UI

───────────────────────────────────────────────────────────────────────────

@impl true def render(assigns) do ~H"""

<%= @page_title %>

  <div class="bg-white rounded-lg shadow-md p-6">
    <%!--
      AshFormBuilder.FormComponent:
      - Renders all fields from the form DSL
      - Uses configured theme (MishkaTheme)
      - Handles validation errors
      - Manages combobox search & create events
    --%>
    <.live_component
      module={AshFormBuilder.FormComponent}
      id="task-form"
      resource={Task}
      form={@form}
    />
  </div>

  <div class="mt-6 flex justify-between">
    <.link
      href={~p"/tasks"}
      class="text-gray-600 hover:text-gray-900"
    >
      ← Back to Tasks
    </.link>
  </div>
</div>
"""

end

───────────────────────────────────────────────────────────────────────────

SEARCH HANDLERS - Combobox

───────────────────────────────────────────────────────────────────────────

@impl true def handle_event("search_categories", %{"query" => query}, socket) do categories = TodoApp.Todos.Category |> Ash.Query.filter(contains(name: ^query)) |> Todos.read!(actor: socket.assigns.current_user)

options = Enum.map(categories, &{&1.name, &1.id})

{:noreply, push_event(socket, "update_combobox_options", %{
  field: "categories",
  options: options
})}

end

@impl true def handle_event("search_tags", %{"query" => query}, socket) do # For creatable combobox - search existing tags # Users can still create new ones via the create button tags = TodoApp.Todos.Tag |> Ash.Query.filter(contains(name: ^query)) |> Todos.read!(actor: socket.assigns.current_user)

options = Enum.map(tags, &{&1.name, &1.id})

{:noreply, push_event(socket, "update_combobox_options", %{
  field: "tags",
  options: options
})}

end

───────────────────────────────────────────────────────────────────────────

SUCCESS HANDLER - Form Submission

───────────────────────────────────────────────────────────────────────────

@impl true def handle_info({:form_submitted, Task, task}, socket) do message = case socket.assigns.mode do :create -> "Task created successfully! 🎉" :update -> "Task updated successfully! ✅" end

{:noreply,
 socket
 |> put_flash(:info, message)
 |> push_navigate(to: ~p"/tasks/#{task.id}")}

end

───────────────────────────────────────────────────────────────────────────

PRIVATE HELPERS

───────────────────────────────────────────────────────────────────────────

defp load_options(records) do Enum.map(records, &{&1.name, &1.id}) end end

=============================================================================

STEP 6: PHOENIX LIVEVIEW - TASK INDEX (List View)

=============================================================================

defmodule TodoAppWeb.TaskLive.Index do @moduledoc "Lists all tasks with create button"

use TodoAppWeb, :live_view

alias TodoApp.Todos alias TodoApp.Todos.Task

@impl true def mount(_params, _session, socket) do {:ok, stream(socket, :tasks, Todos.list_tasks())} end

@impl true def render(assigns) do ~H"""

Tasks

<.link href={~p"/tasks/new"} class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700" > + New Task </.link>

  <div class="bg-white rounded-lg shadow overflow-hidden">
    <table class="min-w-full divide-y divide-gray-200">
      <thead class="bg-gray-50">
        <tr>
          <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Title</th>
          <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Priority</th>
          <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
          <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Due Date</th>
          <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
        </tr>
      </thead>
      <tbody class="bg-white divide-y divide-gray-200" id="tasks" phx-update="stream">
        <tr :for={{id, task} <- @streams.tasks} class="hover:bg-gray-50">
          <td class="px-6 py-4 whitespace-nowrap">
            <div class="text-sm font-medium text-gray-900"><%= task.title %></div>
          </td>
          <td class="px-6 py-4 whitespace-nowrap">
            <span class={priority_class(task.priority)}>
              <%= String.capitalize(to_string(task.priority)) %>
            </span>
          </td>
          <td class="px-6 py-4 whitespace-nowrap">
            <span class={status_class(task.status)}>
              <%= String.capitalize(to_string(task.status)) %>
            </span>
          </td>
          <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
            <%= if task.due_date, do: task.due_date, else: "-" %>
          </td>
          <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
            <.link href={~p"/tasks/#{task.id}"} class="text-blue-600 hover:text-blue-900 mr-3">
              View
            </.link>
            <.link href={~p"/tasks/#{task.id}/edit"} class="text-green-600 hover:text-green-900">
              Edit
            </.link>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</div>
"""

end

defp priority_class(:urgent), do: "px-2 py-1 text-xs rounded-full bg-red-100 text-red-800" defp priority_class(:high), do: "px-2 py-1 text-xs rounded-full bg-orange-100 text-orange-800" defp priority_class(:medium), do: "px-2 py-1 text-xs rounded-full bg-yellow-100 text-yellow-800" defp priority_class(:low), do: "px-2 py-1 text-xs rounded-full bg-green-100 text-green-800"

defp status_class(:done), do: "px-2 py-1 text-xs rounded-full bg-green-100 text-green-800" defp status_class(:in_progress), do: "px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-800" defp status_class(:pending), do: "px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-800" end

=============================================================================

STEP 7: PHOENIX LIVEVIEW - TASK SHOW (View Single Task)

=============================================================================

defmodule TodoAppWeb.TaskLive.Show do @moduledoc "Displays a single task with details"

use TodoAppWeb, :live_view

alias TodoApp.Todos

@impl true def mount(%{"id" => id}, _session, socket) do task = Todos.get_task!(id, load: [:categories, :tags])

{:ok,
 socket
 |> assign(:page_title, task.title)
 |> assign(:task, task)}

end

@impl true def render(assigns) do ~H"""

<%= @task.title %>

<.link href={~p"/tasks/#{@task.id}/edit"} class="bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700" > Edit </.link>

    <div class="space-y-4">
      <div>
        <h3 class="text-sm font-medium text-gray-500">Description</h3>
        <p class="mt-1 text-gray-900"><%= @task.description || "No description" %></p>
      </div>

      <div class="grid grid-cols-2 gap-4">
        <div>
          <h3 class="text-sm font-medium text-gray-500">Priority</h3>
          <p class="mt-1"><%= String.capitalize(to_string(@task.priority)) %></p>
        </div>

        <div>
          <h3 class="text-sm font-medium text-gray-500">Status</h3>
          <p class="mt-1"><%= String.capitalize(to_string(@task.status)) %></p>
        </div>

        <div>
          <h3 class="text-sm font-medium text-gray-500">Due Date</h3>
          <p class="mt-1"><%= if @task.due_date, do: @task.due_date, else: "Not set" %></p>
        </div>

        <div>
          <h3 class="text-sm font-medium text-gray-500">Completed</h3>
          <p class="mt-1"><%= if @task.completed, do: "Yes ✅", else: "No" %></p>
        </div>
      </div>

      <div :if={length(@task.categories) > 0}>
        <h3 class="text-sm font-medium text-gray-500">Categories</h3>
        <div class="mt-2 flex flex-wrap gap-2">
          <span
            :for={category <- @task.categories}
            class="px-3 py-1 text-sm rounded-full bg-blue-100 text-blue-800"
          >
            <%= category.name %>
          </span>
        </div>
      </div>

      <div :if={length(@task.tags) > 0}>
        <h3 class="text-sm font-medium text-gray-500">Tags</h3>
        <div class="mt-2 flex flex-wrap gap-2">
          <span
            :for={tag <- @task.tags}
            class="px-3 py-1 text-sm rounded-full bg-purple-100 text-purple-800"
          >
            #<%= tag.name %>
          </span>
        </div>
      </div>
    </div>
  </div>

  <div class="mt-6">
    <.link href={~p"/tasks"} class="text-gray-600 hover:text-gray-900">
      ← Back to Tasks
    </.link>
  </div>
</div>
"""

end end

=============================================================================

STEP 8: ROUTES (lib/todo_app_web/router.ex)

=============================================================================

defmodule TodoAppWeb.Router do use TodoAppWeb, :router

pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_live_flash plug :put_root_layout, html: {TodoAppWeb.Layouts, :root} plug :protect_from_forgery plug :put_secure_browser_headers end

scope "/", TodoAppWeb do pipe_through :browser

# Task routes
live "/tasks", TaskLive.Index, :index
live "/tasks/new", TaskLive.Form, :new
live "/tasks/:id", TaskLive.Show, :show
live "/tasks/:id/edit", TaskLive.Form, :edit

end end

=============================================================================

STEP 9: RUNNING THE APP

=============================================================================

1. Get dependencies:

$ mix deps.get

2. Create and migrate database:

$ mix ecto.setup

3. Start the server:

$ mix phx.server

4. Visit: http://localhost:4000/tasks

=============================================================================

STEP 10: TESTING

=============================================================================

test/todo_app_web/live/task_live_test.exs

defmodule TodoAppWeb.TaskLiveTest do use TodoAppWeb.ConnCase

import Phoenix.LiveViewTest import TodoApp.TodosFixtures

alias TodoApp.Todos

describe "Index" do test "lists all tasks", %{conn: conn} do {:ok, _index_live, html} = live(conn, ~p"/tasks")

  assert html =~ "Tasks"
  assert html =~ "New Task"
end

end

describe "Create Task" do test "renders form", %{conn: conn} do {:ok, _index_live, html} = live(conn, ~p"/tasks/new")

  assert html =~ "New Task"
  assert html =~ "Task Title"
end

test "creates task and redirects", %{conn: conn} do
  {:ok, live, _html} = live(conn, ~p"/tasks/new")

  assert form(live, "#task-form", task: %{
    title: "Test Task",
    description: "Test description",
    priority: "high"
  }) |> render_submit()
end

test "creates tag on-the-fly via creatable combobox", %{conn: conn} do
  {:ok, live, _html} = live(conn, ~p"/tasks/new")

  # Simulate creating a new tag
  {:noreply, _updated_socket} =
    AshFormBuilder.FormComponent.handle_event(
      "create_combobox_item",
      %{
        "field" => "tags",
        "resource" => "Elixir.TodoApp.Todos.Tag",
        "action" => "create",
        "creatable_value" => "Create \"urgent\""
      },
      live.socket
    )

  # Verify tag was created
  assert %TodoApp.Todos.Tag{name: "urgent"} =
           Ash.read_one!(TodoApp.Todos.Tag, name: "urgent")
end

end

describe "Edit Task" do setup [:create_task]

test "renders edit form", %{conn: conn, task: task} do
  {:ok, _index_live, html} = live(conn, ~p"/tasks/#{task.id}/edit")

  assert html =~ "Edit Task"
  assert html =~ task.title
end

test "updates task and redirects", %{conn: conn, task: task} do
  {:ok, live, _html} = live(conn, ~p"/tasks/#{task.id}/edit")

  assert form(live, "#task-form", task: %{
    title: "Updated Task"
  }) |> render_submit()
end

defp create_task(_) do
  task = task_fixture()
  %{task: task}
end

end end

=============================================================================

END OF TODO APP GUIDE

=============================================================================