lib/kaffy/resource_form.ex

defmodule Kaffy.ResourceForm do
  use Phoenix.HTML

  def form_label_string({field, options}), do: Map.get(options, :label, field)
  def form_label_string(field) when is_atom(field), do: field

  def form_label(form, field) do
    label_text = form_label_string(field)
    label(form, label_text)
  end

  def form_help_text({_field, options}), do: Map.get(options, :help_text, nil)
  def form_help_text(field) when is_atom(field), do: nil

  def bare_form_field(resource, form, {field, options}) do
    options = options || %{}
    type = Map.get(options, :type, Kaffy.ResourceSchema.field_type(resource[:schema], field))
    permission = Map.get(options, :permission, :write)
    choices = Map.get(options, :choices)

    cond do
      !is_nil(choices) ->
        select(form, field, choices, class: "custom-select")

      permission == :read ->
        content_tag(
          :div,
          label(form, field, Kaffy.ResourceSchema.kaffy_field_value(resource[:schema], field))
        )

      true ->
        build_html_input(resource[:schema], form, {field, options}, type, [])
    end
  end

  def form_field(changeset, form, field, opts \\ [])

  def form_field(changeset, form, {field, options}, opts) do
    options = options || %{}

    type =
      Map.get(options, :type, Kaffy.ResourceSchema.field_type(changeset.data.__struct__, field))

    opts =
      if type == :textarea do
        rows = Map.get(options, :rows, 5)
        Keyword.put(opts, :rows, rows)
      else
        opts
      end

    permission =
      case is_nil(changeset.data.id) do
        true -> Map.get(options, :create, :editable)
        false -> Map.get(options, :update, :editable)
      end

    choices = Map.get(options, :choices)

    cond do
      !is_nil(choices) ->
        select(form, field, choices, class: "custom-select")

      true ->
        build_html_input(
          changeset.data,
          form,
          {field, options},
          type,
          opts,
          permission == :readonly
        )
    end
  end

  # def form_field(changeset, form, {field, options}, opts) do
  #   type = Kaffy.ResourceSchema.field_type(changeset.data.__struct__, field)
  #   build_html_input(changeset.data, form, {field, options}, type, opts)
  # end

  defp build_html_input(schema, form, {field, options}, type, opts, readonly \\ false) do
    data = schema
    {conn, opts} = Keyword.pop(opts, :conn)
    opts = Keyword.put(opts, :readonly, readonly)
    schema = schema.__struct__

    case type do
      {:embed, _} ->
        embed = Kaffy.ResourceSchema.embed_struct(schema, field)
        embed_fields = Kaffy.ResourceSchema.fields(embed)
        embed_changeset = Ecto.Changeset.change(Map.get(data, field) || embed.__struct__)

        inputs_for(form, field, fn fp ->
          [
            {:safe, ~s(<div class="card ml-3" style="padding:15px;">)},
            Enum.reduce(embed_fields, [], fn f, all ->
              content_tag :div, class: "form-group" do
                [
                  [
                    form_label(fp, f),
                    form_field(embed_changeset, fp, {f, options}, class: "form-control")
                  ]
                  | all
                ]
              end
            end),
            {:safe, "</div>"}
          ]
        end)

      :id ->
        case Kaffy.ResourceSchema.primary_key(schema) == [field] do
          true -> text_input(form, field, opts)
          false -> text_or_assoc(conn, schema, form, field, opts)
        end

      :binary_id ->
        case Kaffy.ResourceSchema.primary_key(schema) == [field] do
          true -> text_input(form, field, opts)
          false -> text_or_assoc(conn, schema, form, field, opts)
        end

      :string ->
        text_input(form, field, opts)

      :richtext ->
        opts = Keyword.put(opts, :class, "kaffy-editor")
        textarea(form, field, opts)

      :textarea ->
        textarea(form, field, opts)

      :integer ->
        number_input(form, field, opts)

      :float ->
        text_input(form, field, opts)

      :decimal ->
        text_input(form, field, opts)

      t when t in [:boolean, :boolean_checkbox] ->
        checkbox_opts = Keyword.put(opts, :class, "custom-control-input")
        label_opts = Keyword.put(opts, :class, "custom-control-label")

        [
          {:safe, ~s(<div class="custom-control custom-checkbox">)},
          checkbox(form, field, checkbox_opts),
          label(form, field, form_label_string({field, options}), label_opts),
          {:safe, "</div>"}
        ]

      :boolean_switch ->
        checkbox_opts = Keyword.put(opts, :class, "custom-control-input")
        label_opts = Keyword.put(opts, :class, "custom-control-label")

        [
          {:safe, ~s(<div class="custom-control custom-switch">)},
          checkbox(form, field, checkbox_opts),
          label(form, field, form_label_string({field, options}), label_opts),
          {:safe, "</div>"}
        ]

      :map ->
        value = Map.get(data, field, "")

        value =
          cond do
            is_map(value) -> Kaffy.Utils.json().encode!(value, escape: :html_safe, pretty: true)
            true -> value
          end

        textarea(form, field, [value: value, rows: 4, placeholder: "JSON Content"] ++ opts)

      {:parameterized, Ecto.Enum, %{values: values}} ->
        values = Enum.map(values, &to_string/1)
        value = Map.get(data, field, nil)

        select(form, field, values, [value: value] ++ opts)

      {:array, {:parameterized, Ecto.Enum, %{values: values}}} ->
        values = Enum.map(values, &to_string/1)
        value = Map.get(data, field, nil)

        multiple_select(form, field, values, [value: value] ++ opts)

      {:array, _} ->
        case !is_nil(options[:values_fn]) && is_function(options[:values_fn], 2) do
          true ->
            values = options[:values_fn].(data, conn)
            value = Map.get(data, field, nil)
            multiple_select(form, field, values, [value: value] ++ opts)

          false ->
            value =
              data
              |> Map.get(field, "")
              |> Kaffy.Utils.json().encode!(escape: :html_safe, pretty: true)

            textarea(form, field, [value: value, rows: 4, placeholder: "JSON Content"] ++ opts)
        end

      :file ->
        file_input(form, field, opts)

      :select ->
        select(form, field, opts)

      :date ->
        flatpickr_date(form, field, opts)

      :time ->
        flatpickr_time(form, field, opts)

      :naive_datetime ->
        flatpickr_datetime(form, field, opts)

      :naive_datetime_usec ->
        flatpickr_datetime_usec(form, field, opts)

      :utc_datetime ->
        flatpickr_datetime(form, field, opts)

      :utc_datetime_usec ->
        flatpickr_datetime_usec(form, field, opts)

      _ ->
        text_input(form, field, opts)
    end
  end

  defp flatpickr_time(form, field, opts) do
    flatpickr_generic(form, field, opts, "Select Date...", "flatpickr-wrap-time", "🕒")
  end

  defp flatpickr_date(form, field, opts) do
    flatpickr_generic(form, field, opts, "Select Date...", "flatpickr-wrap-date", "🗓️")
  end

  defp flatpickr_datetime(form, field, opts) do
    flatpickr_generic(form, field, opts, "Select Datetime...", "flatpickr-wrap-datetime")
  end

  defp flatpickr_datetime_usec(form, field, opts) do
    flatpickr_generic(form, field, opts, "Select Datetime...", "flatpickr-wrap-datetime-usec")
  end

  defp flatpickr_generic(form, field, opts, placeholder, fp_class, icon \\ "📅") do
    opts = Keyword.put(opts, :class, "flatpickr-input")
    opts = Keyword.put(opts, :class, "form-control")
    opts = Keyword.put(opts, :id, "inlineFormInputGroup")
    opts = Keyword.put(opts, :placeholder, placeholder)
    opts = Keyword.put(opts, :"data-input", "")

    [
      {:safe, ~s(
            <div class="input-group mb-2 flatpickr #{fp_class}">
              <div class="input-group-prepend">
                <div class="input-group-text" data-clear>❌</div>
              </div>
              <div class="input-group-prepend">
                <div class="input-group-text" data-toggle>#{icon}</div>
              </div>
          )},
      text_input(form, field, opts),
      {:safe, "</div>"}
    ]
  end

  defp text_or_assoc(conn, schema, form, field, opts) do
    actual_assoc =
      Enum.filter(Kaffy.ResourceSchema.associations(schema), fn a ->
        Kaffy.ResourceSchema.association(schema, a).owner_key == field
      end)
      |> Enum.at(0)

    field_no_id =
      case actual_assoc do
        nil -> field
        _ -> Kaffy.ResourceSchema.association(schema, actual_assoc).field
      end

    case field_no_id in Kaffy.ResourceSchema.associations(schema) do
      true ->
        assoc = Kaffy.ResourceSchema.association_schema(schema, field_no_id)
        option_count = Kaffy.ResourceQuery.cached_total_count(assoc, true, assoc)

        case option_count > 100 do
          true ->
            target_context = Kaffy.Utils.get_context_for_schema(conn, assoc)
            target_resource = Kaffy.Utils.get_schema_key(conn, target_context, assoc)

            content_tag :div, class: "input-group" do
              [
                number_input(form, field,
                  class: "form-control",
                  id: field,
                  aria_describedby: field
                ),
                content_tag :div, class: "input-group-append" do
                  content_tag :span, class: "input-group-text", id: field do
                    link(content_tag(:i, "", class: "fas fa-search"),
                      to:
                        Kaffy.Utils.router().kaffy_resource_path(
                          conn,
                          :index,
                          target_context,
                          target_resource,
                          c: conn.params["context"],
                          r: conn.params["resource"],
                          pick: field
                        ),
                      id: "pick-raw-resource"
                    )
                  end
                end
              ]
            end

          false ->
            options = Kaffy.Utils.repo().all(assoc)

            fields = Kaffy.ResourceSchema.fields(assoc)

            string_fields =
              Enum.filter(fields, fn {_f, options} ->
                options.type == :string or
                  (Kaffy.Utils.is_module(options.type) and
                     Kernel.function_exported?(options.type, :type, 0) and
                     options.type.type == :string)
              end)

            popular_strings =
              string_fields
              |> Enum.filter(fn {f, _} -> f in [:name, :title] end)
              |> Enum.at(0)

            string_field =
              case is_nil(popular_strings) do
                true -> (Enum.at(string_fields, 0) || {:id}) |> elem(0)
                false -> elem(popular_strings, 0)
              end

            select(
              form,
              field,
              Enum.map(options, fn o -> {Map.get(o, string_field, "Resource ##{o.id}"), o.id} end),
              class: "custom-select"
            )
        end

      false ->
        number_input(form, field, opts)
    end
  end

  def get_field_error(changeset, field) do
    changeset
    |> build_error_messages()
    |> Map.get(field)
    |> case do
      nil ->
        {nil, ""}

      # # In case of field is a embedded schema
      %{} ->
        {nil, ""}

      messages ->
        error_msg =
          Kaffy.ResourceAdmin.humanize_term(field) <> " " <> Enum.join(messages, ", ") <> "!"

        {error_msg, "is-invalid"}
    end
  end

  defp build_error_messages(changeset) do
    Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
      Enum.reduce(opts, msg, fn {key, value}, acc ->
        String.replace(acc, "%{#{key}}", build_changeset_value(value))
      end)
    end)
  end

  defp build_changeset_value(value) when is_tuple(value),
    do: value |> Tuple.to_list() |> Enum.join(", ")

  defp build_changeset_value(value) when is_list(value),
    do: value |> Enum.join(", ")

  defp build_changeset_value(value), do: to_string(value)

  def kaffy_input(conn, changeset, form, field, options) do
    ft = Kaffy.ResourceSchema.field_type(changeset.data.__struct__, field)

    case Kaffy.Utils.is_module(ft) && Keyword.has_key?(ft.__info__(:functions), :render_form) do
      true ->
        ft.render_form(conn, changeset, form, field, options)

      false ->
        {error_msg, error_class} = get_field_error(changeset, field)
        help_text = form_help_text({field, options})

        content_tag :div, class: "form-group #{error_class}" do
          label_tag = if ft != :boolean, do: form_label(form, {field, options}), else: ""

          field_tag =
            form_field(changeset, form, {field, options},
              class: "form-control #{error_class}",
              conn: conn
            )

          field_feeback = [
            content_tag :div, class: "invalid-feedback" do
              error_msg
            end,
            content_tag :p, class: "help_text" do
              help_text
            end
          ]

          [label_tag, field_tag, field_feeback]
        end
    end
  end
end