# Graph App Recipes

These recipes show common graph-app patterns. They are intentionally app-level
patterns: LivePanels owns runtime graph behavior, and your app owns product
meaning.

## Add A Node And Select It

```elixir
def handle_event("add-step", _params, socket) do
  id = "step-#{System.unique_integer([:positive])}"

  socket =
    socket
    |> LivePanels.Canvas.Item.add!(:step,
      id: id,
      x: 120,
      y: 80,
      w: 280,
      h: 160,
      props: %{title: "New step"}
    )
    |> LivePanels.Canvas.Selection.select!(id)

  {:noreply, socket}
end
```

## Add A Node Between Two Nodes

```elixir
def insert_between(socket, from_id, to_id) do
  new_id = "step-#{System.unique_integer([:positive])}"

  socket =
    LivePanels.Canvas.transaction!(socket, fn tx ->
      tx
      |> LivePanels.Canvas.Item.add!(:step, id: new_id, x: 360, y: 120)
      |> LivePanels.Graph.connect!(from: {from_id, "out"}, to: {new_id, "in"})
      |> LivePanels.Graph.connect!(from: {new_id, "out"}, to: {to_id, "in"})
      |> LivePanels.Canvas.Selection.select!(new_id)
    end)

  socket
end
```

If an existing connector should be removed as part of the insert, disconnect it in
the same transaction.

## Prevent Cycles In An App Event

Use `LivePanels.Graph.Query.creates_cycle?/3` before submitting a graph command:

```elixir
def handle_event("connect", %{"from" => from_id, "to" => to_id}, socket) do
  state = LivePanels.Canvas.Read.state(socket)

  if LivePanels.Graph.Query.creates_cycle?(state, from_id, to_id) do
    {:noreply, put_flash(socket, :error, "That connection would create a cycle.")}
  else
    socket =
      LivePanels.Graph.connect!(socket,
        from: {from_id, "out"},
        to: {to_id, "in"}
      )

    {:noreply, socket}
  end
end
```

Use application policy when the rule must apply to every command source. Use an application
event check when the rule belongs to one product workflow.

## Show Incoming And Outgoing Counts

```elixir
defp assign_graph_counts(socket) do
  state = LivePanels.Canvas.Read.state(socket)
  by_item = LivePanels.Graph.Query.connectors_by_item(state)

  assign(socket, :connector_counts, by_item)
end
```

Render:

```heex
<span class="node-count">
  {length(Map.get(@connector_counts, context.item.id, []))}
</span>
```

## Select Downstream Nodes

```elixir
def handle_event("select-downstream", %{"id" => item_id}, socket) do
  state = LivePanels.Canvas.Read.state(socket)

  selection =
    LivePanels.Graph.Query.navigation(state, {:item, item_id}, :outgoing,
      mode: :all
    ).selection

  {:noreply, LivePanels.Canvas.Selection.select!(socket, selection)}
end
```

Use this pattern for "show dependencies", "select consumers", or "trace
lineage" controls.

## Fit A Selected Subgraph

```elixir
def handle_event("focus-selection", _params, socket) do
  {:noreply, LivePanels.Graph.fit(socket, :selection)}
end
```

This changes the current actor's viewport. It does not move nodes for other
participants.

## Toggle Connector Route

```elixir
def handle_event("graph-route", %{"route" => route}, socket) do
  route =
    case route do
      "bezier" -> :bezier
      "orthogonal" -> :orthogonal
      "straight" -> :straight
      _ -> :bezier
    end

  {:noreply, assign(socket, :connector_route, route)}
end
```

Store shared or persistent route choice on connector items:

```elixir
socket =
  LivePanels.Graph.edit!(socket, :all,
    route: route,
    direction: :horizontal
  )
```

Route choice can stay in application UI state only when it is a local display
preference. Store it on connector items when it should persist, stream, or be
shared.

## Render A Graph Inspector

```elixir
defp assign_graph_inspector(socket) do
  state = LivePanels.Canvas.Read.state(socket)
  selection = LivePanels.Canvas.Read.selection(socket)
  selected_item_ids = LivePanels.Canvas.Selection.selected_item_ids(selection)

  selected_connectors =
    Enum.filter(LivePanels.Graph.connectors(state), fn connector ->
      MapSet.member?(selected_item_ids, connector.id)
    end)

  assign(socket, selected_connectors: selected_connectors)
end
```

Render connector controls:

```heex
<aside :if={@selected_connectors != []} class="graph-inspector">
  <section :for={connector <- @selected_connectors}>
    <p>{connector.id}</p>
    <button {disconnect_attrs(connector)}>Remove connector</button>
  </section>
</aside>
```

## Mirror Canvas Connectors To Domain Records

For domain-backed graphs, keep the domain write explicit:

```elixir
def handle_event("connect-dependency", %{"from" => from_id, "to" => to_id}, socket) do
  with {:ok, dependency} <- MyApp.Dependencies.create(from_id, to_id) do
    socket =
      LivePanels.Graph.connect!(socket,
        from: {dependency.from_id, "out"},
        to: {dependency.to_id, "in"}
      )

    {:noreply, socket}
  else
    {:error, changeset} ->
      {:noreply, assign(socket, :dependency_error, changeset)}
  end
end
```

Do not hide the domain write inside LivePanels. LivePanels should only own the
canvas mutation.

## Restore A Domain-Backed Graph

  Build source-authored items and connector commands from domain records:

```elixir
def mount(%{"workflow_id" => workflow_id}, _session, socket) do
  workflow = MyApp.Workflows.get!(workflow_id)

    socket =
      socket
      |> assign(:workflow, workflow)
      |> assign(:graph_items, graph_items(workflow))
      |> assign(:graph_connectors, graph_connectors(workflow))

  {:ok, socket}
end
```

If you also use LivePanels layout persistence, make sure domain records exist
before the layout restores. Stable item ids are the bridge between app data and
canvas state.

## Debug A Missing Connector

Check these in order:

- both endpoint items exist;
- both endpoint ports are declared on the item attrs;
- directions are output-to-input;
- port types are compatible;
- target `max_connections` is not exceeded;
- the command was not rejected by application policy;
- the graph extension is configured on the canvas;
- `canvas_runtime/1` receives current `@livepanels.items`;
- route presentation on the connector item is valid.

Use `LivePanels.Graph.connect/2`, `disconnect/2`, or
`reconnect/3` when server code needs the rejection reason immediately.
Browser graph gestures also record their result on
`LivePanels.Canvas.Read.last_command/1`.
