# Plugin Development

> #### Experimental {: .warning}
>
> The Plugin API is `@experimental`. Breaking changes may occur in minor versions until
> this notice is removed. Pin your `phoenix_filament` dependency to a specific version
> when building plugins.

Plugins let you extend a PhoenixFilament panel with custom navigation, live routes,
dashboard widgets, and lifecycle hooks — using the exact same API that PhoenixFilament
uses internally.

## Quick Start

```elixir
defmodule MyApp.AnalyticsPlugin do
  use PhoenixFilament.Plugin

  @impl true
  def register(_panel, _opts) do
    %{
      nav_items: [
        nav_item("Analytics",
          path: "/analytics",
          icon: "hero-chart-bar",
          nav_group: "Reports")
      ],
      routes: [
        route("/analytics", MyAppWeb.AnalyticsLive, :index)
      ]
    }
  end
end
```

Register it in your panel:

```elixir
defmodule MyAppWeb.Admin do
  use PhoenixFilament.Panel, path: "/admin"

  plugins do
    plugin MyApp.AnalyticsPlugin
  end
end
```

## `use PhoenixFilament.Plugin`

Using this macro:

1. Adds `@behaviour PhoenixFilament.Plugin` to your module
2. Imports `nav_item/2` and `route/3` helper functions

## Callbacks

### `register/2` (required)

Called at compile time when the panel resolves its plugin list. Returns a map describing
what the plugin contributes to the panel.

```elixir
@impl true
def register(panel_module, opts) do
  %{
    nav_items: [...],
    routes: [...],
    widgets: [...],
    hooks: [...]
  }
end
```

Arguments:

- `panel_module` — the Panel module that is registering this plugin
- `opts` — the keyword list passed to `plugin MyPlugin, key: value`

All keys in the returned map are optional. Omit any key you do not need.

#### `:nav_items`

Navigation entries added to the panel sidebar. Build them with `nav_item/2`:

```elixir
nav_item("Analytics",
  path: "/analytics",
  icon: "hero-chart-bar",
  nav_group: "Reports")
```

`nav_item/2` options:

| Key | Type | Description |
|-----|------|-------------|
| `path:` | string | URL path (relative to panel root) |
| `icon:` | string | Heroicon name |
| `nav_group:` | string | Sidebar group heading |

#### `:routes`

Live routes added to the panel's `live_session`. Build them with `route/3`:

```elixir
route("/analytics", MyAppWeb.AnalyticsLive, :index)
route("/analytics/:id", MyAppWeb.AnalyticsLive, :show)
```

`route/3` arguments:

1. Path string (relative to the panel's `scope` path)
2. LiveView module
3. Live action atom

All routes registered via plugins automatically inherit the panel's `on_mount` hooks,
session name, and layout.

#### `:widgets`

Dashboard widgets contributed by the plugin:

```elixir
widgets: [
  %{module: MyApp.AnalyticsWidget, sort: 5, column_span: 6}
]
```

Widget map keys:

| Key | Default | Description |
|-----|---------|-------------|
| `module` | required | LiveComponent module |
| `sort` | `0` | Dashboard rendering order (ascending) |
| `column_span` | `12` | Grid column span (1–12) |

The widget module must implement one of the widget behaviours:
`PhoenixFilament.Widget.StatsOverview`, `PhoenixFilament.Widget.Chart`,
`PhoenixFilament.Widget.Table`, or `PhoenixFilament.Widget.Custom`.

#### `:hooks`

Lifecycle hooks called at various points in the panel LiveView:

```elixir
hooks: [
  {:handle_event, &MyApp.AnalyticsPlugin.handle_event/3},
  {:handle_info,  &MyApp.AnalyticsPlugin.handle_info/2},
  {:handle_params, &MyApp.AnalyticsPlugin.handle_params/3},
  {:after_render,  &MyApp.AnalyticsPlugin.after_render/1}
]
```

Hook function signatures:

```elixir
# handle_event: called before the panel's handle_event
def handle_event(event, params, socket), do: {:cont, socket}

# handle_info: called before the panel's handle_info
def handle_info(message, socket), do: {:cont, socket}

# handle_params: called before the panel's handle_params
def handle_params(params, uri, socket), do: {:cont, socket}

# after_render: called after each render
def after_render(socket), do: socket
```

Return `{:cont, socket}` to allow the default handler to proceed, or `{:halt, socket}`
to stop further processing.

### `boot/1` (optional)

Called at runtime on each panel LiveView `mount`. Receives the socket after the panel's
own `on_mount` hooks have run. Returns the modified socket.

```elixir
@impl true
def boot(socket) do
  user_id = socket.assigns.current_user.id

  # Subscribe to a PubSub topic
  Phoenix.PubSub.subscribe(MyApp.PubSub, "analytics:#{user_id}")

  # Add assigns available throughout the panel session
  Phoenix.Component.assign(socket, :analytics_enabled, true)
end
```

`boot/1` cannot halt the mount — authentication is the Panel's responsibility.

## Plugin Options

Pass configuration to your plugin from the panel:

```elixir
plugins do
  plugin MyApp.AnalyticsPlugin,
    nav_group: "Reports",
    show_realtime: true
end
```

Access options in `register/2`:

```elixir
def register(_panel, opts) do
  group = opts[:nav_group] || "Analytics"
  show_realtime = opts[:show_realtime] || false

  %{
    nav_items: [
      nav_item("Analytics", path: "/analytics", nav_group: group)
    ] ++ if(show_realtime, do: [nav_item("Live", path: "/analytics/live", nav_group: group)], else: [])
  }
end
```

## Complete Plugin Example

```elixir
defmodule MyApp.AuditLogPlugin do
  use PhoenixFilament.Plugin

  @impl true
  def register(_panel, opts) do
    group = opts[:nav_group] || "System"

    %{
      nav_items: [
        nav_item("Audit Log",
          path: "/audit",
          icon: "hero-clipboard-document-list",
          nav_group: group)
      ],
      routes: [
        route("/audit", MyAppWeb.AuditLive, :index),
        route("/audit/:id", MyAppWeb.AuditLive, :show)
      ],
      widgets: [
        %{module: MyApp.RecentAuditWidget, sort: 10, column_span: 12}
      ],
      hooks: [
        {:handle_info, &__MODULE__.handle_info/2}
      ]
    }
  end

  @impl true
  def boot(socket) do
    if Map.has_key?(socket.assigns, :current_user) do
      Phoenix.PubSub.subscribe(MyApp.PubSub, "audit_log")
    end
    socket
  end

  def handle_info({:audit_event, event}, socket) do
    # Update live audit count in sidebar badge, etc.
    updated = update_in(socket.assigns[:audit_count] || 0, &(&1 + 1))
    {:cont, Phoenix.Component.assign(socket, :audit_count, updated)}
  end

  def handle_info(_msg, socket), do: {:cont, socket}
end
```

## Testing Your Plugin

Use ExUnit with `use ExUnit.Case`:

```elixir
defmodule MyApp.AuditLogPluginTest do
  use ExUnit.Case, async: true

  describe "register/2" do
    test "returns nav_items and routes" do
      result = MyApp.AuditLogPlugin.register(MyAppWeb.Admin, [])

      assert [nav_item] = result.nav_items
      assert nav_item.label == "Audit Log"
      assert nav_item.path == "/audit"

      assert [route1, route2] = result.routes
      assert route1.path == "/audit"
      assert route1.live_action == :index
    end

    test "respects nav_group option" do
      result = MyApp.AuditLogPlugin.register(MyAppWeb.Admin, nav_group: "Security")
      [nav_item] = result.nav_items
      assert nav_item.nav_group == "Security"
    end
  end

  describe "boot/1" do
    test "assigns analytics_enabled" do
      socket = %Phoenix.LiveView.Socket{assigns: %{current_user: %{id: 1}}}
      result = MyApp.AuditLogPlugin.boot(socket)
      # boot modifies the socket — assert your expected changes
      assert result != nil
    end
  end
end
```

## Stability Contract

| Version | Status |
|---------|--------|
| v0.1.x | `@experimental` — breaking changes possible in minor versions |
| v0.2+ | Stabilize based on community feedback |
| v1.0 | Stable, semver-protected |

Pin your dependency to a patch version while the API is experimental:

```elixir
{:phoenix_filament, "~> 0.1.0"}
```
