lib/live_admin/components/resource/form.ex

defmodule LiveAdmin.Components.Container.Form do
  use Phoenix.LiveComponent
  use Phoenix.HTML

  import Phoenix.LiveView.Helpers
  import LiveAdmin.ErrorHelpers
  import LiveAdmin, only: [associated_resource: 3, get_config: 3]
  import LiveAdmin.Components.Container, only: [route_with_params: 2]

  alias __MODULE__.{ArrayInput, SearchSelect}
  alias LiveAdmin.{Resource, SessionStore}

  @supported_primitive_types [
    :string,
    :boolean,
    :date,
    :integer,
    :naive_datetime,
    :utc_datetime,
    :id,
    :float
  ]

  @impl true
  def update(assigns = %{record: record, config: config}, socket) do
    socket =
      socket
      |> assign(assigns)
      |> assign(:enabled, false)
      |> assign(:changeset, Resource.change(record, config))

    {:ok, socket}
  end

  @impl true
  def update(assigns = %{config: config}, socket) do
    socket =
      socket
      |> assign(assigns)
      |> assign(:enabled, false)
      |> assign(:changeset, Resource.change(assigns.resource, config))

    {:ok, socket}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.form
        let={f}
        for={@changeset}
        as="params"
        phx_change="validate"
        phx_submit={@action}
        phx_target={@myself}
        class="resource__form"
      >
        <%= for {field, type, opts} <- Resource.fields(@resource, @config) do %>
          <.field
            field={field}
            type={type}
            form={f}
            immutable={Keyword.get(opts, :immutable, false)}
            resource={@resource}
            resources={@resources}
            form_ref={@myself}
            session={SessionStore.lookup(@session_id)}
          />
        <% end %>
        <div class="form__actions">
          <%= submit("Save",
            class: "resource__action#{if !@enabled, do: "--disabled", else: "--btn"}",
            disabled: !@enabled
          ) %>
        </div>
      </.form>
    </div>
    """
  end

  @impl true
  def handle_event(
        "validate",
        %{"field" => field, "value" => value},
        socket = %{assigns: %{changeset: changeset, config: config, session_id: session_id}}
      ) do
    changeset =
      validate(
        changeset,
        Map.put(changeset.changes, String.to_existing_atom(field), value),
        config,
        session_id
      )

    {:noreply,
     assign(socket,
       changeset: changeset,
       enabled: enabled?(changeset, socket.assigns.action, config)
     )}
  end

  @impl true
  def handle_event(
        "validate",
        %{"params" => params},
        socket = %{assigns: %{changeset: changeset, config: config, session_id: session_id}}
      ) do
    changeset = validate(changeset, params, config, session_id)

    {:noreply,
     assign(socket,
       changeset: changeset,
       enabled: enabled?(changeset, socket.assigns.action, config)
     )}
  end

  @impl true
  def handle_event(
        "create",
        %{"params" => params},
        %{assigns: %{resource: resource, key: key, config: config, session_id: session_id}} =
          socket
      ) do
    socket =
      case Resource.create(resource, config, params, SessionStore.lookup(session_id)) do
        {:ok, _} -> push_redirect(socket, to: route_with_params(socket, [:list, key]))
        {:error, changeset} -> assign(socket, changeset: changeset)
      end

    {:noreply, socket}
  end

  @impl true
  def handle_event(
        "update",
        %{"params" => params},
        %{
          assigns: %{
            key: key,
            config: config,
            session_id: session_id,
            record: record
          }
        } = socket
      ) do
    socket =
      case Resource.update(
             record,
             config,
             params,
             SessionStore.lookup(session_id)
           ) do
        {:ok, _} -> push_redirect(socket, to: route_with_params(socket, [:list, key]))
        {:error, changeset} -> assign(socket, changeset: changeset)
      end

    {:noreply, socket}
  end

  defp field(assigns = %{type: type}) when type in @supported_primitive_types,
    do: field_group(assigns)

  defp field(assigns = %{type: {_, Ecto.Enum, _}}), do: field_group(assigns)

  defp field(assigns = %{type: {:array, :string}}), do: field_group(assigns)

  defp field(assigns = %{type: {:array, {_, Ecto.Enum, _}}}), do: field_group(assigns)

  defp field(assigns = %{type: {_, Ecto.Embedded, _}}) do
    ~H"""
    <div>
      <h2 class="embed__title"><%= @field %></h2>
      <div class="embed__group">
        <%= unless @immutable do %>
          <%= for fp <- inputs_for(@form, @field) do %>
            <%= for {field, type, _} <- fields_for_embed(@type) do %>
              <.field
                field={field}
                type={type}
                form={fp}
                immutable={false}
                resource={@resource}
                resources={@resources}
                form_ref={@form_ref}
                session={@session}
              />
            <% end %>
          <% end %>
        <% else %>
          <pre><%= @form |> input_value(@field) |> inspect() %></pre>
        <% end %>
      </div>
    </div>
    """
  end

  defp field(assigns) do
    ~H"""
    <div class="field__group--disabled">
      <%= label(@form, @field, class: "field__label") %>
      <%= textarea(@form, @field, disabled: true, value: @form |> input_value(@field) |> inspect()) %>
    </div>
    """
  end

  defp field_group(assigns) do
    ~H"""
    <div class={"field__group#{if @immutable, do: "--disabled"}"}>
      <%= label(@form, @field, class: "field__label") %>
      <.input
        form={@form}
        type={@type}
        field={@field}
        disabled={@immutable}
        resource={@resource}
        resources={@resources}
        form_ref={@form_ref}
        session={@session}
      />
      <%= error_tag(@form, @field) %>
    </div>
    """
  end

  defp input(assigns = %{type: :id}) do
    assigns.resource
    |> associated_resource(assigns.field, assigns.resources)
    |> case do
      nil ->
        ~H"""
        <%= textarea(@form, @field, rows: 1, class: "field__text", disabled: @disabled) %>
        """

      {_, {resource, config}} ->
        ~H"""
        <%= unless @form.data |> Ecto.primary_key() |> Keyword.keys() |> Enum.member?(@field) do %>
          <.live_component
            module={SearchSelect}
            id={assigns.field}
            form={@form}
            field={@field}
            disabled={@disabled}
            resource={resource}
            config={config}
            form_ref={@form_ref}
            session={@session}
            handle_select="validate"
          />
        <% else %>
          <div class="form__number">
            <%= number_input(@form, @field, class: "field__number", disabled: @disabled) %>
          </div>
        <% end %>
        """
    end
  end

  defp input(assigns = %{type: {:array, :string}}) do
    ~H"""
    <.live_component
      module={ArrayInput}
      id={assigns.field}
      form={@form}
      field={@field}
      disabled={@disabled}
      form_ref={@form_ref}
    />
    """
  end

  defp input(assigns = %{type: :string}) do
    ~H"""
    <%= textarea(@form, @field, rows: 1, class: "field__text", disabled: @disabled) %>
    """
  end

  defp input(assigns = %{type: :boolean}) do
    ~H"""
    <div class="form__checkbox">
      <%= checkbox(@form, @field, class: "field__checkbox", disabled: @disabled) %>
    </div>
    """
  end

  defp input(assigns = %{type: :date}) do
    ~H"""
    <%= date_input(@form, @field, class: "field__date", disabled: @disabled) %>
    """
  end

  defp input(assigns = %{type: number}) when number in [:integer, :id] do
    ~H"""
    <div class="form__number">
      <%= number_input(@form, @field, class: "field__number", disabled: @disabled) %>
    </div>
    """
  end

  defp input(assigns = %{type: :float}) do
    ~H"""
    <div class="form__number">
      <%= number_input(@form, @field, class: "field__number", disabled: @disabled, step: "any") %>
    </div>
    """
  end

  defp input(assigns = %{type: type}) when type in [:naive_datetime, :utc_datetime] do
    ~H"""
    <div class="form__time">
      <%= datetime_local_input(@form, @field, class: "field__time", disabled: @disabled) %>
    </div>
    """
  end

  defp input(assigns = %{type: {_, Ecto.Enum, %{mappings: mappings}}}) do
    ~H"""
    <%= select(@form, @field, [nil | Keyword.keys(mappings)],
      disabled: @disabled,
      class: "field__select"
    ) %>
    """
  end

  defp input(assigns = %{type: {:array, {_, Ecto.Enum, %{mappings: mappings}}}}) do
    ~H"""
    <div class="field__checkbox--group">
      <%= hidden_input(@form, @field, name: input_name(@form, @field) <> "[]", value: nil) %>
      <%= for option <- Keyword.keys(mappings) do %>
        <%= checkbox(@form, @field,
          name: input_name(@form, @field) <> "[]",
          checked_value: option,
          value: @form |> input_value(@field) |> Kernel.||([]) |> Enum.find(&(&1 == option)),
          unchecked_value: "",
          hidden_input: false,
          disabled: @disabled,
          id: input_id(@form, @field) <> to_string(option)
        ) %>
        <label for={input_id(@form, @field) <> to_string(option)}>
          <%= option %>
        </label>
      <% end %>
    </div>
    """
  end

  defp fields_for_embed({_, _, %{related: schema}}), do: Resource.fields(schema, %{})

  defp validate(changeset, params, config, session_id) do
    changeset.data
    |> Resource.change(config, params)
    |> Resource.validate(config, SessionStore.lookup(session_id))
  end

  def enabled?(changeset, action, config) do
    get_config(config, :"#{action}_with", true) && Enum.empty?(changeset.errors)
  end
end