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

introduce option validation for all backpex fields #661

Draft
wants to merge 10 commits into
base: develop
Choose a base branch
from
1 change: 0 additions & 1 deletion demo/lib/demo_web/live/short_link_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ defmodule DemoWeb.ShortLinkLive do
product: %{
module: Backpex.Fields.BelongsTo,
label: "Product",
source: Demo.Product,
Flo0807 marked this conversation as resolved.
Show resolved Hide resolved
display_field: :name,
prompt: "Choose product..."
}
Expand Down
2 changes: 1 addition & 1 deletion guides/fields/custom-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ When creating your own custom field, you can use the `field` macro from the `Bac
The simplest version of a custom field would look like this:

```elixir
use BackpexWeb, :field
use Backpex.Field

@impl Backpex.Field
def render_value(assigns) do
Expand Down
27 changes: 27 additions & 0 deletions guides/upgrading/v0.9.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,30 @@ end

Although the change is relatively small, if you are using public functions of the `Backpex.LiveResource` directly,
check the updated function definitions in the module documentation.

## Refactor custom fields

In case you built your own custom fields: We changed the way how to use the `Backpex.Field`.

Before:

```elixir
use BackpexWeb, :field
```

After:

```elixir
use Backpex.Field
```

In case your field has field-specific configuration options, you need to provide those when using `Backpex.Field`:

```elixir
@config_schema [
# see https://hexdocs.pm/nimble_options/NimbleOptions.html
# or any other core backpex field for examples...
]

use Backpex.Field, config_schema: @config_schema
```
2 changes: 1 addition & 1 deletion lib/backpex/adapters/ecto.ex
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ defmodule Backpex.Adapters.Ecto do
end

defp record_query(id, schema, item_query, live_resource) do
fields = live_resource.fields()
fields = live_resource.validated_fields()
schema_name = name_by_schema(schema)
primary_key = live_resource.config(:primary_key)
primary_type = schema.__schema__(:type, primary_key)
Expand Down
135 changes: 130 additions & 5 deletions lib/backpex/field.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,109 @@
# credo:disable-for-this-file Credo.Check.Refactor.CyclomaticComplexity
defmodule Backpex.Field do
@moduledoc ~S'''
@config_schema [
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default (function of arity 1) is missing. See https://github.com/naymspace/backpex/blob/0.8.2/lib/backpex/live_resource.ex#L546

Also, can we add debounce and throttle to the global field options?

module: [
doc: "The field module.",
type: :atom,
required: true
],
label: [
doc: "The field label.",
type: :string,
required: true
],
render: [
doc: "A function to overwrite the template used . It should take `assigns` and return a HEEX template.",
type: {:fun, 1}
],
render_form: [
doc: "A function to overwrite the template used in forms. It should take `assigns` and return a HEEX template.",
type: {:fun, 1}
],
custom_alias: [
doc: "A custom alias for the field.",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Flo0807 Feel free to suggest further information here (and for other options).

type: :atom
],
align: [
doc: "Align the fields of a resource in the index view.",
type: {:in, [:left, :center, :right]}
],
align_label: [
doc: "Align the labels of the fields in the edit view.",
type: {:in, [:top, :center, :bottom]}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also allow a function of arity 1 for align_label

],
searchable: [
doc: "Define wether this field should be searchable on the index view.",
type: :boolean
],
orderable: [
doc: "Define wether this field should be orderable on the index view.",
type: :boolean
],
visible: [
doc: "Function to change the visibility of a field. Receives the assigns and has to return a boolean.",
type: {:fun, 1}
],
panel: [
doc: "Group field into panel. Also see the [panels](/guides/authorization/panels.md) guide.",
type: :atom
],
index_editable: [
doc: """
Define wether this field should be editable on the index view. Also see the
[index edit](/guides/authorization/index-edit.md) guide.
""",
type: :boolean
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

index_editable can also be a function of arity 1

],
index_column_class: [
doc: """
Add additional class(es) to the index column.
In case of a function it takes the `assigns` and should return a string.
""",
type: {:or, [:string, {:fun, 1}]}
],
select: [
doc: """
Define a dynamic select query expression for this field.

### Example

full_name: %{
module: Backpex.Fields.Text,
label: "Full Name",
select: dynamic([user: u], fragment("concat(?, ' ', ?)", u.first_name, u.last_name)),
}
""",
type: {:struct, Ecto.Query.DynamicExpr}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get the following warning when generating the docs:

Generating docs...
     warning: documentation references module "Ecto.Query.DynamicExpr" but it is hidden
     │
 134 │   @callback render_index_form(assigns :: map()) :: %Phoenix.LiveView.Rendered{}
     │   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     │
     └─ lib/backpex/field.ex:134: Backpex.Field (module)

But maybe thats not a problem?

],
only: [
doc: "Define the only views where this field should be visible.",
type: {:list, {:in, [:new, :edit, :show, :index, :resource_action]}}
],
except: [
doc: "Define the views where this field should not be visible.",
type: {:list, {:in, [:new, :edit, :show, :index, :resource_action]}}
],
translate_error: [
doc: """
Function to customize error messages for a field. The function receives the error tuple and must return a tuple
with the message and metadata.
""",
type: {:fun, 1}
]
]

@moduledoc """
Behaviour implemented by all fields.

A field defines how a column is rendered on index, show and edit views. In the resource configuration file you can configure a list of fields. You may create your own field by implementing this behaviour. A field has to be a [LiveComponent](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html).
A field defines how a column is rendered on index, show and edit views. In the resource configuration file you can
configure a list of fields. You may create your own field by implementing this behaviour. A field has to be a
[LiveComponent](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html).

### Options

These are general field options which can be used on every field. Check the field modules for field-specific options.

#{NimbleOptions.docs(@config_schema)}

### Example

Expand All @@ -15,7 +115,7 @@ defmodule Backpex.Field do
}
]
end
'''
"""
import Phoenix.Component, only: [assign: 3]

@doc """
Expand Down Expand Up @@ -97,13 +197,38 @@ defmodule Backpex.Field do

@optional_callbacks render_form_readonly: 1, render_index_form: 1

@doc """
Returns the default config schema.
"""
def default_config_schema, do: @config_schema

@doc """
Defines `Backpex.Field` behaviour and provides default implementations.
"""
defmacro __using__(_) do
quote do
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
@config_schema opts[:config_schema] || []

@before_compile Backpex.Field
@behaviour Backpex.Field

use BackpexWeb, :field

def validate_config!({name, options} = _field, live_resource) do
field_options = Keyword.new(options)

case NimbleOptions.validate(field_options, Backpex.Field.default_config_schema() ++ @config_schema) do
{:ok, validated_options} ->
validated_options

{:error, error} ->
raise """
Configuration error for field "#{name}" in "#{live_resource}".

#{error.message}
"""
end
end
end
end

Expand Down
43 changes: 33 additions & 10 deletions lib/backpex/fields/belongs_to.ex
Original file line number Diff line number Diff line change
@@ -1,15 +1,40 @@
defmodule Backpex.Fields.BelongsTo do
@config_schema [
display_field: [
doc: "The field of the relation to be used for searching, ordering and displaying values.",
type: :atom,
required: true
],
display_field_form: [
doc: "Field to be used to display form values.",
type: :atom
],
live_resource: [
doc: "The live resource of the association. Used to generate links navigating to the association.",
type: :atom
],
options_query: [
doc: """
Manipulates the list of available options in the select.

Defaults to `fn (query, _field) -> query end` which returns all entries.
""",
type: {:fun, 2}
],
prompt: [
doc: "The text to be displayed when no option is selected or function that receives the assigns.",
type: :string
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prompt can also be a function

]
]

@moduledoc """
A field for handling a `belongs_to` relation.

## Options
## Field-specific options

* `:display_field` - The field of the relation to be used for searching, ordering and displaying values.
* `:display_field_form` - Optional field to be used to display form values.
* `:live_resource` - The live resource of the association. Used to generate links navigating to the association.
* `:options_query` - Manipulates the list of available options in the select.
Defaults to `fn (query, _field) -> query end` which returns all entries.
* `:prompt` - The text to be displayed when no option is selected or function that receives the assigns.
See `Backpex.Field` for general field options.

#{NimbleOptions.docs(@config_schema)}

## Example

Expand All @@ -26,10 +51,8 @@ defmodule Backpex.Fields.BelongsTo do
]
end
"""
use BackpexWeb, :field

use Backpex.Field, config_schema: @config_schema
import Ecto.Query

alias Backpex.Router

@impl Phoenix.LiveComponent
Expand Down
20 changes: 16 additions & 4 deletions lib/backpex/fields/boolean.ex
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
defmodule Backpex.Fields.Boolean do
@config_schema [
debounce: [
doc: "Timeout value (in milliseconds), \"blur\" or function that receives the assigns.",
type: {:or, [:pos_integer, :string, {:fun, 1}]}
],
throttle: [
doc: "Timeout value (in milliseconds) or function that receives the assigns.",
type: {:or, [:pos_integer, {:fun, 1}]}
]
]

@moduledoc """
A field for handling a boolean value.

## Options
## Field-specific options

See `Backpex.Field` for general field options.

* `:debounce` - Optional integer timeout value (in milliseconds), "blur" or function that receives the assigns.
* `:throttle` - Optional integer timeout value (in milliseconds) or function that receives the assigns.
#{NimbleOptions.docs(@config_schema)}
"""
use BackpexWeb, :field
use Backpex.Field, config_schema: @config_schema

@impl Backpex.Field
def render_value(assigns) do
Expand Down
22 changes: 16 additions & 6 deletions lib/backpex/fields/currency.ex
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
defmodule Backpex.Fields.Currency do
@config_schema [
debounce: [
doc: "Timeout value (in milliseconds), \"blur\" or function that receives the assigns.",
type: {:or, [:pos_integer, :string, {:fun, 1}]}
],
throttle: [
doc: "Timeout value (in milliseconds) or function that receives the assigns.",
type: {:or, [:pos_integer, {:fun, 1}]}
]
]

@moduledoc """
A field for handling a currency value.

## Options
## Field-specific options

See `Backpex.Field` for general field options.

* `:debounce` - Optional integer timeout value (in milliseconds), "blur" or function that receives the assigns.
* `:throttle` - Optional integer timeout value (in milliseconds) or function that receives the assigns.
#{NimbleOptions.docs(@config_schema)}

## Schema

Expand Down Expand Up @@ -34,10 +46,8 @@ defmodule Backpex.Fields.Currency do
]
end
"""
use BackpexWeb, :field

use Backpex.Field, config_schema: @config_schema
import Ecto.Query

alias Backpex.Ecto.Amount.Type

@impl Backpex.Field
Expand Down
Loading