Skip to content

Commit

Permalink
Merge pull request #661 from naymspace/feature/field-validations
Browse files Browse the repository at this point in the history
introduce option validation for all backpex fields
  • Loading branch information
pehbehbeh authored Dec 4, 2024
2 parents 281d672 + fa00a8f commit 09ff3d7
Show file tree
Hide file tree
Showing 26 changed files with 798 additions and 264 deletions.
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,
display_field: :name,
prompt: "Choose product..."
}
Expand Down
1 change: 0 additions & 1 deletion demo/lib/demo_web/live/user_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ defmodule DemoWeb.UserLive do
label: "Avatar",
upload_key: :avatar,
accept: ~w(.jpg .jpeg .png),
max_entries: 1,
max_file_size: 512_000,
put_upload_change: &put_upload_change/6,
consume_upload: &consume_upload/4,
Expand Down
1 change: 0 additions & 1 deletion demo/lib/demo_web/resource_actions/upload.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ defmodule DemoWeb.ResourceActions.Upload do
label: "Upload",
upload_key: :upload,
accept: ~w(.jpg .jpeg .png),
max_entries: 1,
put_upload_change: &put_upload_change/6,
consume_upload: &consume_upload/4,
remove_uploads: &remove_uploads/3,
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
32 changes: 32 additions & 0 deletions guides/upgrading/v0.9.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,38 @@ 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
```

## Removed string support on `throttle` field options

The fields that allow the `throttle` options previously supported a string value (e.g. `"500"`).
Please change it to an integer value (e.g. `500`).

## Resource Action and Item Action `init_change/1` is renamed

The term `init_change` was confusing because the result is being used as the base schema / item for the changeset function. Therefore we renamed the function to `base_schema/1` for both Item Actions and Resource Actions.
Expand Down
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
147 changes: 142 additions & 5 deletions lib/backpex/field.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,121 @@
# credo:disable-for-this-file Credo.Check.Refactor.CyclomaticComplexity
defmodule Backpex.Field do
@moduledoc ~S'''
@config_schema [
module: [
doc: "The field module.",
type: :atom,
required: true
],
label: [
doc: "The field label.",
type: :string,
required: true
],
default: [
doc: """
A function to assign default values to fields. Also see the [field defaults](/guides/fields/defaults.md) guide.
""",
type: {:fun, 1}
],
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.",
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: {:or, [{:in, [:top, :center, :bottom]}, {:fun, 1}]}
],
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 for all views except index. Receives the assigns and has to return a boolean.",
type: {:fun, 1}
],
can?: [
doc:
"Function to change the visibility of a field for all views. 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: {:or, [:boolean, {:fun, 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}
],
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 +127,7 @@ defmodule Backpex.Field do
}
]
end
'''
"""
import Phoenix.Component, only: [assign: 3]

@doc """
Expand Down Expand Up @@ -97,13 +209,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
51 changes: 41 additions & 10 deletions lib/backpex/fields/belongs_to.ex
Original file line number Diff line number Diff line change
@@ -1,15 +1,48 @@
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: {:or, [:string, {:fun, 1}]}
],
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 `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 +59,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

0 comments on commit 09ff3d7

Please sign in to comment.