lib/live_admin/components/container.ex

defmodule LiveAdmin.Components.Container do
  use Phoenix.LiveView
  use PhoenixHTMLHelpers

  import LiveAdmin,
    only: [
      resource_title: 2,
      route_with_params: 1,
      route_with_params: 2,
      trans: 1
    ]

  import LiveAdmin.Components
  import LiveAdmin.View, only: [get_function_keys: 3]

  alias LiveAdmin.Resource
  alias Phoenix.LiveView.JS

  @impl true
  def mount(_params, _session, socket) do
    socket =
      assign(socket, loading: !connected?(socket))

    if connected?(socket), do: Process.send_after(self(), :clear_flash, 2000)

    {:ok, socket}
  end

  @impl true
  def handle_info(:clear_flash, socket) do
    {:noreply, clear_flash(socket)}
  end

  @impl true
  def handle_params(
        params = %{"record_id" => id},
        uri,
        socket = %{assigns: %{live_action: action, loading: false}}
      )
      when action in [:edit, :view] do
    socket =
      socket
      |> assign_resource_info(uri)
      |> assign_mod()
      |> assign_repo()
      |> assign_prefix(params)

    record =
      Resource.find(
        id,
        socket.assigns.resource,
        socket.assigns[:prefix],
        socket.assigns.repo
      )

    socket = assign(socket, record: record)

    {:noreply, socket}
  end

  @impl true
  def handle_params(params, uri, socket = %{assigns: %{live_action: :list, loading: false}}) do
    socket =
      socket
      |> assign(search: params["s"])
      |> assign_resource_info(uri)
      |> assign_pagination_params(params)
      |> assign_mod()
      |> assign_repo()
      |> assign_prefix(params)

    {:noreply, socket}
  end

  @impl true
  def handle_params(params, uri, socket = %{assigns: %{live_action: :new}}),
    do:
      {:noreply,
       socket
       |> assign_resource_info(uri)
       |> assign_mod()
       |> assign_repo()
       |> assign_prefix(params)}

  def handle_params(_, _, socket), do: {:noreply, socket}

  @impl true
  def handle_event("set_locale", %{"locale" => locale}, socket) do
    new_session = Map.put(socket.assigns.session, :locale, locale)

    LiveAdmin.session_store().persist!(new_session)

    {:noreply, assign(socket, :session, new_session)}
  end

  def render(assigns = %{loading: true}), do: ~H""

  @impl true
  def render(assigns) do
    ~H"""
    <div class="resource__banner">
      <h1 class="resource__title">
        <%= resource_title(@resource, @config) %>
      </h1>

      <div class="resource__actions">
        <div>
          <.link
            navigate={route_with_params(assigns, params: [prefix: @prefix])}
            class="resource__action--btn"
          >
            <%= trans("List") %>
          </.link>
          <%= if LiveAdmin.fetch_config(@resource, :create_with, @config) != false do %>
            <.link
              navigate={route_with_params(assigns, segments: ["new"], params: [prefix: @prefix])}
              class="resource__action--btn"
            >
              <%= trans("New") %>
            </.link>
          <% else %>
            <button class="resource__action--disabled" disabled="disabled">
              <%= trans("New") %>
            </button>
          <% end %>
          <.dropdown
            :let={task}
            label={trans("Run task")}
            items={get_function_keys(@resource, @config, :tasks)}
            disabled={Enum.empty?(get_function_keys(@resource, @config, :tasks))}
          >
            <.task_control task={task} session={@session} resource={@resource} />
          </.dropdown>
          <%= if Enum.any?(@prefix_options) do %>
            <.dropdown
              :let={prefix}
              id="prefix-select"
              label={@prefix || trans("Set prefix")}
              items={[""] ++ Enum.filter(@prefix_options, &(to_string(&1) != @prefix))}
            >
              <.link navigate={route_with_params(assigns, params: [prefix: prefix])}>
                <%= if prefix == "", do: trans("clear"), else: prefix %>
              </.link>
            </.dropdown>
          <% end %>
          <%= if LiveAdmin.use_i18n? do %>
            <.dropdown
              :let={locale}
              id="locale-select"
              label={@session.locale || "Set locale"}
              items={
                Enum.filter(
                  LiveAdmin.gettext_backend().locales(),
                  &(to_string(&1) != @session.locale)
                )
              }
            >
              <button
                class="resource__action--link"
                phx-click={JS.push("set_locale", value: %{locale: locale}, page_loading: true)}
              >
                <%= locale %>
              </button>
            </.dropdown>
          <% end %>
        </div>
      </div>
    </div>

    <%= render("#{@live_action}.html", assigns) %>
    """
  end

  def render("list.html", assigns) do
    ~H"""
    <.live_component
      module={@mod}
      id="list"
      key={@key}
      resource={@resource}
      page={@page}
      sort_attr={@sort_attr}
      sort_dir={@sort_dir}
      search={@search}
      prefix={@prefix}
      session={@session}
      base_path={@base_path}
      resources={@resources}
      repo={@repo}
      config={@config}
    />
    """
  end

  def render("new.html", assigns) do
    ~H"""
    <.live_component
      module={@mod}
      id="form"
      action="create"
      session={@session}
      key={@key}
      resources={@resources}
      resource={@resource}
      prefix={@prefix}
      base_path={@base_path}
      repo={@repo}
      config={@config}
    />
    """
  end

  def render("edit.html", assigns) do
    ~H"""
    <.live_component
      module={@mod}
      id="form"
      action="update"
      session={@session}
      key={@key}
      record={@record}
      resources={@resources}
      resource={@resource}
      prefix={@prefix}
      repo={@repo}
      base_path={@base_path}
      config={@config}
    />
    """
  end

  def render("view.html", assigns) do
    ~H"""
    <.live_component
      module={@mod}
      id="view"
      record={@record}
      resource={@resource}
      resources={@resources}
      session={@session}
      key={@key}
      base_path={@base_path}
      prefix={@prefix}
      repo={@repo}
      config={@config}
    />
    """
  end

  defp assign_prefix(socket, %{"prefix" => ""}) do
    socket
    |> assign_and_presist_prefix(nil)
    |> push_redirect(to: route_with_params(socket.assigns))
  end

  defp assign_prefix(socket, %{"prefix" => prefix}) do
    socket.assigns.prefix_options
    |> Enum.find(fn option -> to_string(option) == prefix end)
    |> case do
      nil ->
        push_redirect(socket, to: route_with_params(socket.assigns, params: [prefix: ""]))

      prefix ->
        assign_and_presist_prefix(socket, prefix)
    end
  end

  defp assign_prefix(socket = %{assigns: %{session: session}}, _) do
    case session.prefix do
      nil ->
        assign_and_presist_prefix(socket, nil)

      prefix ->
        push_patch(socket, to: route_with_params(socket.assigns, params: [prefix: prefix]))
    end
  end

  defp assign_and_presist_prefix(socket, prefix) do
    new_session = Map.put(socket.assigns.session, :prefix, prefix)

    LiveAdmin.session_store().persist!(new_session)

    assign(socket, prefix: prefix, session: new_session)
  end

  defp assign_resource_info(socket, uri) do
    %URI{host: host, path: path} = URI.parse(uri)

    %{resource: {key, mod}} = Phoenix.Router.route_info(socket.router, "GET", path, host)

    assign(socket, key: key, resource: mod)
  end

  defp assign_mod(socket = %{assigns: %{resource: resource, live_action: action, config: config}}) do
    mod =
      resource
      |> LiveAdmin.fetch_config(:components, config)
      |> Keyword.fetch!(action)

    assign(socket, :mod, mod)
  end

  defp assign_repo(socket = %{assigns: %{resource: resource, config: config}}) do
    repo = LiveAdmin.fetch_config(resource, :ecto_repo, config)

    prefix_options =
      if function_exported?(repo, :prefixes, 0) do
        repo.prefixes()
      else
        []
      end

    assign(socket, repo: repo, prefix_options: prefix_options)
  end

  defp assign_pagination_params(socket, params) do
    params =
      Map.new(params, fn
        {"sort-attr", val} -> {"sort_attr", val}
        {"sort-dir", val} -> {"sort_dir", val}
        pair -> pair
      end)

    types =
      %{
        page: :integer,
        sort_attr:
          Ecto.ParameterizedType.init(Ecto.Enum,
            values:
              socket.assigns.resource
              |> LiveAdmin.Resource.fields(socket.assigns.config)
              |> Enum.map(fn {field, _, _} -> field end)
          ),
        sort_dir: Ecto.ParameterizedType.init(Ecto.Enum, values: [:asc, :desc])
      }

    defaults = %{
      page: 1,
      sort_attr: LiveAdmin.primary_key!(socket.assigns.resource),
      sort_dir: :asc
    }

    params =
      {defaults, types}
      |> Ecto.Changeset.cast(params, Map.keys(types))
      |> Ecto.Changeset.apply_action!(:update)

    assign(socket, params)
  end

  defp task_control(assigns) do
    {name, _, _, arity, docs} =
      LiveAdmin.fetch_function(assigns.resource, assigns.session, :tasks, assigns.task)

    extra_arg_count = arity - 1

    assigns =
      assign(assigns,
        extra_arg_count: extra_arg_count,
        function_docs: docs,
        modalize: extra_arg_count > 0 or Enum.any?(docs),
        title: name |> to_string() |> humanize()
      )

    ~H"""
    <button
      class="resource__action--link"
      phx-click={
        if @modalize,
          do:
            JS.show(
              to: "##{@task}-task-modal",
              transition: {"ease-in duration-300", "opacity-0", "opacity-100"}
            ),
          else: JS.push("task", value: %{"name" => @task}, page_loading: true, target: "#list")
      }
      ,
      data-confirm={if @modalize, do: nil, else: "Are you sure?"}
    >
      <%= @task |> to_string() |> humanize() %>
    </button>
    <%= if @modalize do %>
      <.modal id={"#{@task}-task-modal"}>
        <span class="modal__title"><%= @title %></span>
        <%= for {_lang, doc} <- @function_docs do %>
          <span class="docs"><%= doc %></span>
        <% end %>
        <.form for={Phoenix.Component.to_form(%{})} phx-submit="task" phx-target="#list">
          <input type="hidden" name="name" value={@task} />
          <%= if @extra_arg_count > 0 do %>
            <b>Arguments</b>
            <%= for num <- 1..@extra_arg_count do %>
              <div>
                <label><%= num %></label>
                <input type="text" name="args[]" />
              </div>
            <% end %>
          <% end %>
          <input type="submit" value="Execute" />
        </.form>
      </.modal>
    <% end %>
    """
  end
end