lib/live_admin/router.ex

defmodule LiveAdmin.Router do
  import Phoenix.Component, only: [assign: 2]

  @doc """
  Defines a group of LiveAdmin resources that share a common path prefix, and optionally, configuration.

  ## Arguments

  * `path` - Defines a scope to be added to the router under which the resources will be grouped in a single live session
  * `opts` - Configuration for this LiveAdmin scope. In addition to global options:
    - `title` (binary) - Title to display in nav bar
    - `on_mount` (tuple) - Module function when routes are mounted that will receive and should return session
  """
  defmacro live_admin(path, opts \\ [], do: context) do
    import Phoenix.LiveView.Router, only: [live: 4, live_session: 3]

    quote do
      current_path =
        __MODULE__
        |> Module.get_attribute(:phoenix_top_scopes)
        |> Map.fetch!(:path)

      @base_path Path.join(["/", current_path, unquote(path)])

      scope unquote(path), alias: false, as: false do
        live_session :"live_admin_#{@base_path}",
          session: {unquote(__MODULE__), :build_session, [@base_path, unquote(opts)]},
          root_layout: {LiveAdmin.View, :layout},
          layout: {LiveAdmin.View, :app},
          on_mount: {unquote(__MODULE__), :assign_options} do
          live("/", LiveAdmin.Components.Home, :"home_#{@base_path}", as: :"home_#{@base_path}")

          live("/session", LiveAdmin.Components.Session, :"session_#{@base_path}",
            as: :"session_#{@base_path}"
          )

          unquote(context)
        end
      end
    end
  end

  @doc """
  Defines a resource to be included in a LiveAdmin UI.

  For each configured resource at path `/foo`, the following routes will be added:

  * `/foo` - List view
  * `/foo/new` - New record form
  * `/foo/:id/edit` - Update record form
  """
  defmacro admin_resource(path, resource_mod) do
    import Phoenix.LiveView.Router, only: [live: 4]

    quote bind_quoted: [path: path, resource_mod: resource_mod] do
      full_path = Path.join(@base_path, path)

      live(path, LiveAdmin.Components.Container, :list,
        as: :"list_#{full_path}",
        metadata: %{base_path: @base_path, resource: {path, resource_mod}}
      )

      live("#{path}/new", LiveAdmin.Components.Container, :new,
        as: :"new_#{full_path}",
        metadata: %{base_path: @base_path, resource: {path, resource_mod}}
      )

      live("#{path}/:record_id", LiveAdmin.Components.Container, :view,
        as: :"view_#{full_path}",
        metadata: %{base_path: @base_path, resource: {path, resource_mod}}
      )

      live("#{path}/edit/:record_id", LiveAdmin.Components.Container, :edit,
        as: :"edit_#{full_path}",
        metadata: %{base_path: @base_path, resource: {path, resource_mod}}
      )
    end
  end

  def build_session(conn, base_path, opts) do
    opts_schema =
      LiveAdmin.base_configs_schema() ++
        [title: [type: :string, default: "LiveAdmin"], on_mount: [type: {:tuple, [:atom, :atom]}]]

    default_components =
      Keyword.merge(
        [
          nav: LiveAdmin.Components.Nav,
          home: LiveAdmin.Components.Home.Content,
          session: LiveAdmin.Components.Session.Content,
          new: LiveAdmin.Components.Container.Form,
          edit: LiveAdmin.Components.Container.Form,
          list: LiveAdmin.Components.Container.Index,
          view: LiveAdmin.Components.Container.View
        ],
        Application.get_env(:live_admin, :components, [])
      )

    opts =
      opts
      |> NimbleOptions.validate!(opts_schema)
      |> Keyword.put(
        :components,
        Keyword.merge(default_components, Keyword.get(opts, :components, []))
      )
      |> Keyword.put_new(:ecto_repo, Application.get_env(:live_admin, :ecto_repo))
      |> Keyword.put_new(:render_with, Application.get_env(:live_admin, :render_with))
      |> Keyword.put_new(:delete_with, Application.get_env(:live_admin, :delete_with))
      |> Keyword.put_new(:create_with, Application.get_env(:live_admin, :create_with))
      |> Keyword.put_new(:list_with, Application.get_env(:live_admin, :list_with))
      |> Keyword.put_new(:update_with, Application.get_env(:live_admin, :update_with))
      |> Keyword.put_new(:label_with, Application.get_env(:live_admin, :label_with))
      |> Keyword.put_new(:title_with, Application.get_env(:live_admin, :title_with))
      |> Keyword.put_new(:validate_with, Application.get_env(:live_admin, :validate_with))
      |> Keyword.put_new(:hidden_fields, Application.get_env(:live_admin, :hidden_fields, []))
      |> Keyword.put_new(
        :immutable_fields,
        Application.get_env(:live_admin, :immutable_fields, [])
      )
      |> Keyword.put_new(:actions, Application.get_env(:live_admin, :actions, []))
      |> Keyword.put_new(:tasks, Application.get_env(:live_admin, :tasks, []))

    %{
      "session_id" => LiveAdmin.session_store().init!(conn),
      "base_path" => base_path,
      "opts" => opts
    }
  end

  def on_mount(
        :assign_options,
        _params,
        %{
          "base_path" => base_path,
          "session_id" => session_id,
          "opts" => opts
        },
        socket
      ) do
    session = LiveAdmin.session_store().load!(session_id)

    Gettext.put_locale(LiveAdmin.gettext_backend(), session.locale)

    socket =
      assign(socket,
        session: session,
        base_path: base_path,
        resources: LiveAdmin.resources(socket.router, base_path),
        config: opts
      )

    socket =
      case Keyword.get(opts, :on_mount) do
        {m, f} -> apply(m, f, [socket])
        _ -> socket
      end

    {:cont, socket}
  end
end