Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor filter modules and make them reusable #113

Merged
merged 18 commits into from
Mar 20, 2024
Merged
160 changes: 92 additions & 68 deletions lib/backpex/filters/boolean.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ defmodule Backpex.Filters.Boolean do
> In addition it will add a `render` and `render_form` function in order to display the corresponding filter.
> It will also implement the `Backpex.Filter.query` function to define a boolean query.
"""
use BackpexWeb, :filter

@doc """
The list of options for the select filter.
Expand All @@ -47,88 +48,111 @@ defmodule Backpex.Filters.Boolean do
defmacro __using__(_opts) do
quote do
use BackpexWeb, :filter
use Backpex.Filter

alias Backpex.Filters.Boolean, as: BooleanFilter

@behaviour Backpex.Filters.Boolean

@impl Backpex.Filter
def query(query, _attribute, []), do: query

def query(query, attribute, value) do
Enum.reduce(value, nil, fn
v, nil ->
Map.get(predicates(), v)

v, p ->
dynamic(^p or ^Map.get(predicates(), v))
end)
|> maybe_query(query)
BooleanFilter.query(query, options(), attribute, value)
end

defp maybe_query(nil, query), do: query
defp maybe_query(predicates, query), do: where(query, ^predicates)

@impl Backpex.Filter
def render(var!(assigns)) do
var!(assigns) =
var!(assigns)
|> assign(:label, option_value_to_label(options(), var!(assigns).value))

~H"""
<%= @label %>
"""
def render(assigns) do
assigns = assign(assigns, :options, options())
BooleanFilter.render(assigns)
end

defp option_value_to_label(options, values) do
Enum.map(values, fn key -> find_option_label(options, key) end)
|> Enum.intersperse(", ")
@impl Backpex.Filter
def render_form(assigns) do
assigns = assign(assigns, :options, options())
BooleanFilter.render_form(assigns)
end

defp find_option_label(options, key) do
Enum.find_value(options, fn option ->
if option.key == key, do: option.label
end) || ""
end
defoverridable query: 3, render: 1, render_form: 1
end
end

@impl Backpex.Filter
def render_form(var!(assigns) = assigns) do
checked = if is_nil(assigns.value), do: [], else: assigns.value
options = Enum.map(options(), fn %{label: l, key: k} -> {l, k} end)

var!(assigns) =
var!(assigns)
|> assign(:checked, checked)
|> assign(:options, options)

~H"""
<div class="mt-2 flex flex-col space-y-2">
<%= Phoenix.HTML.Form.hidden_input(@form, @field, name: Phoenix.HTML.Form.input_name(@form, @field), value: "") %>
<%= for {label, key} <- @options do %>
<label class="flex cursor-pointer items-center gap-x-2">
<%= Phoenix.HTML.Form.checkbox(
@form,
@field,
name: Phoenix.HTML.Form.input_name(@form, @field) <> "[]",
class: "checkbox checkbox-sm checkbox-primary",
checked: to_string(key) in @checked,
checked_value: key,
unchecked_value: "",
hidden_input: false
) %>
<span class="label-text">
<%= label %>
</span>
</label>
<% end %>
</div>
"""
end
attr :value, :any, required: true
attr :options, :list, required: true

defp predicates do
Enum.map(options(), fn %{predicate: p, key: k} -> {k, p} end)
|> Enum.into(%{})
end
def render(assigns) do
assigns = assign(assigns, :label, option_value_to_label(assigns.options, assigns.value))

defoverridable query: 3
end
~H"""
<%= @label %>
"""
end

attr :form, :any, required: true
attr :field, :atom, required: true
attr :value, :any, required: true
attr :options, :list, required: true

def render_form(assigns) do
checked = if is_nil(assigns.value), do: [], else: assigns.value
options = Enum.map(assigns.options, fn %{label: l, key: k} -> {l, k} end)

assigns =
assigns
|> assign(:checked, checked)
|> assign(:options, options)

~H"""
<div class="mt-2 flex flex-col space-y-2">
<%= Phoenix.HTML.Form.hidden_input(@form, @field, name: Phoenix.HTML.Form.input_name(@form, @field), value: "") %>
<%= for {label, key} <- @options do %>
<label class="flex cursor-pointer items-center gap-x-2">
<%= Phoenix.HTML.Form.checkbox(
@form,
@field,
name: Phoenix.HTML.Form.input_name(@form, @field) <> "[]",
class: "checkbox checkbox-sm checkbox-primary",
checked: to_string(key) in @checked,
checked_value: key,
unchecked_value: "",
hidden_input: false
) %>
<span class="label-text">
<%= label %>
</span>
</label>
<% end %>
</div>
"""
end

def query(query, _options, _attribute, []), do: query

def query(query, options, _attribute, value) do
Enum.reduce(value, nil, fn
v, nil ->
Map.get(predicates(options), v)

v, p ->
dynamic(^p or ^Map.get(predicates(options), v))
end)
|> maybe_query(query)
end

def maybe_query(nil, query), do: query
def maybe_query(predicates, query), do: where(query, ^predicates)

def option_value_to_label(options, values) do
Enum.map(values, fn key -> find_option_label(options, key) end)
|> Enum.intersperse(", ")
end

def find_option_label(options, key) do
Enum.find_value(options, fn option ->
if option.key == key, do: option.label
end) || ""
end

def predicates(options) do
Enum.map(options, fn %{predicate: p, key: k} -> {k, p} end)
|> Enum.into(%{})
end
end
178 changes: 100 additions & 78 deletions lib/backpex/filters/multi_select.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ defmodule Backpex.Filters.MultiSelect do
> When you `use Backpex.Filters.MultiSelect`, the `Backpex.Filters.MultiSelect` module will set `@behavior Backpex.Filters.Select`.
> In addition it will add a `render` and `render_form` function in order to display the corresponding filter.
"""
use BackpexWeb, :filter

@doc """
The list of options for the multi select filter.
Expand All @@ -35,99 +36,120 @@ defmodule Backpex.Filters.MultiSelect do
defmacro __using__(_opts) do
quote do
use BackpexWeb, :filter
use Backpex.Filter

alias Backpex.Filters.MultiSelect, as: MultiSelectFilter

@behaviour Backpex.Filters.Select

@impl Backpex.Filter
def query(query, _attribute, []), do: query

def query(query, attribute, value) do
Enum.reduce(value, nil, fn
v, nil ->
dynamic([x], field(x, ^attribute) == ^v)
defdelegate query(query, attribute, value), to: MultiSelectFilter

v, p ->
dynamic([x], ^p or field(x, ^attribute) == ^v)
end)
|> maybe_query(query)
@impl Backpex.Filter
def render(assigns) do
assigns = assign(assigns, :options, options())
MultiSelectFilter.render(assigns)
end

defp maybe_query(nil, query), do: query
defp maybe_query(predicates, query), do: where(query, ^predicates)

@impl Backpex.Filter
def render(var!(assigns)) do
var!(assigns) =
var!(assigns)
|> assign(:label, option_value_to_label(options(), var!(assigns).value))

~H"""
<%= @label %>
"""
end
def render_form(assigns) do
assigns =
assigns
|> assign(:options, options())
|> assign(:prompt, prompt())

defp option_value_to_label(options, values) do
Enum.map(values, fn key -> find_option_label(options, key) end)
|> Enum.intersperse(", ")
MultiSelectFilter.render_form(assigns)
end

defp find_option_label(options, key) do
Enum.find_value(options, fn {l, k} ->
if k == key, do: l
end) || ""
end
defoverridable query: 3, render: 1, render_form: 1
end
end

@impl Backpex.Filter
def render_form(var!(assigns) = assigns) do
checked = if is_nil(assigns.value), do: [], else: assigns.value
attr :value, :any, required: true
attr :options, :list, required: true

var!(assigns) =
var!(assigns)
|> assign(:checked, checked)
|> assign(:options, options())
|> assign(:prompt, prompt())
def render(assigns) do
assigns = assign(assigns, :label, option_value_to_label(assigns.options, assigns.value))

~H"""
<div class="mt-2" x-data="{ open: false }">
<div tabindex="0" @click="open = !open" role="button" class="select select-sm select-bordered w-full">
<%= if @checked == [] do %>
<%= @prompt %>
<% else %>
<%= "#{Enum.count(@checked)} #{Backpex.translate("selected")}" %>
<% end %>
</div>
<ul
tabindex="0"
class="dropdown-content z-[1] menu bg-base-100 rounded-box min-w-60 max-h-96 w-max overflow-y-auto p-2 shadow"
x-show="open"
@click.outside="open = false"
>
<div class="space-y-2">
<%= Phoenix.HTML.Form.hidden_input(@form, @field, name: Phoenix.HTML.Form.input_name(@form, @field), value: "") %>
<%= for {label, key} <- @options do %>
<label class="flex cursor-pointer items-center gap-x-2">
<%= Phoenix.HTML.Form.checkbox(
@form,
@field,
name: Phoenix.HTML.Form.input_name(@form, @field) <> "[]",
class: "checkbox checkbox-sm checkbox-primary",
checked: to_string(key) in @checked,
checked_value: key,
unchecked_value: "",
hidden_input: false
) %>
<span class="label-text">
<%= label %>
</span>
</label>
<% end %>
</div>
</ul>
~H"""
<%= @label %>
"""
end

attr :form, :any, required: true
attr :field, :atom, required: true
attr :value, :any, required: true
attr :options, :list, required: true
attr :prompt, :string, required: true

def render_form(assigns) do
checked = if is_nil(assigns.value), do: [], else: assigns.value
assigns = assign(assigns, :checked, checked)

~H"""
<div class="mt-2" x-data="{ open: false }">
<div tabindex="0" @click="open = !open" role="button" class="select select-sm select-bordered w-full">
<%= if @checked == [] do %>
<%= @prompt %>
<% else %>
<%= "#{Enum.count(@checked)} #{Backpex.translate("selected")}" %>
<% end %>
</div>
<ul
tabindex="0"
class="dropdown-content z-[1] menu bg-base-100 rounded-box min-w-60 max-h-96 w-max overflow-y-auto p-2 shadow"
x-show="open"
@click.outside="open = false"
>
<div class="space-y-2">
<%= Phoenix.HTML.Form.hidden_input(@form, @field, name: Phoenix.HTML.Form.input_name(@form, @field), value: "") %>
<%= for {label, key} <- @options do %>
<label class="flex cursor-pointer items-center gap-x-2">
<%= Phoenix.HTML.Form.checkbox(
@form,
@field,
name: Phoenix.HTML.Form.input_name(@form, @field) <> "[]",
class: "checkbox checkbox-sm checkbox-primary",
checked: to_string(key) in @checked,
checked_value: key,
unchecked_value: "",
hidden_input: false
) %>
<span class="label-text">
<%= label %>
</span>
</label>
<% end %>
</div>
"""
end
</ul>
</div>
"""
end

defoverridable query: 3
end
def query(query, _attribute, []), do: query

def query(query, attribute, value) do
Enum.reduce(value, nil, fn
v, nil ->
dynamic([x], field(x, ^attribute) == ^v)

v, p ->
dynamic([x], ^p or field(x, ^attribute) == ^v)
end)
|> maybe_query(query)
end

def maybe_query(nil, query), do: query
def maybe_query(predicates, query), do: where(query, ^predicates)

def option_value_to_label(options, values) do
Enum.map(values, fn key -> find_option_label(options, key) end)
|> Enum.intersperse(", ")
end

def find_option_label(options, key) do
Enum.find_value(options, fn {l, k} ->
if k == key, do: l
end) || ""
end
end
Loading