` itself.
### Limitations
- **No cell-level options yet.** A future `cell_options` API will let you customize individual `` elements via the field DSL. For now, `row_options` only affects the row container.
- **No grid or kanban analog.** `self.grid_view` is the future home for grid-card options; kanban gets its own when the time comes.
- **Turbo Stream re-renders.** When a row is broadcast-updated via Turbo Stream, the `view:` local resolves based on the original render context. Verify in your specs if you depend on `view` branching.
---
# Grid view
Some resources are best displayed in a grid view. We can do that with Avo using a `cover_url`, a `title`, and a `body`.
## Enable grid view
To enable grid view for a resource, you need to configure the `grid_view` class attribute on the resource. That will add the grid view to the view switcher on the view.
```ruby{2-13}
class Avo::Resources::Post < Avo::BaseResource
self.grid_view = {
card: -> do
{
cover_url:
if record.cover_photo.attached?
main_app.url_for(record.cover_photo.url)
end,
title: record.name,
body: record.truncated_body
}
end
}
end
```
## Options
Next, you should configure a few things for the grid card.
What should be used as the title of the card.
```ruby
self.grid_view = {
card: -> do
{
title: record.title
}
end
}
```
What should be used as the body of the card. You can use this field to display a description of the record.
```ruby
self.grid_view = {
card: -> do
{
body: record.truncated_body
}
end
}
```
What should be used as the cover URL of the card.
```ruby
self.grid_view = {
card: -> do
{
cover_url: record.image.attached? ? main_app.url_for(record.image.variant(resize_to_fill: [300, 300])) : nil
}
end
}
```
If `nil` is given, a default placeholder image will be used.
Optionally you may add a badge to give more context to the card or make it stand out.
See [below](#grid-item-badge) a list of options you can configure for the badge.
## Make grid the default view
To make the grid the default way of viewing a resource **Index**, we have to use the `default_view_type` class attribute.
```ruby{2}
class Avo::Resources::Post < Avo::BaseResource
self.default_view_type = :grid
end
```
## Custom style
You may want to customize the card a little bit. That's possible using the `html` option.
```ruby{13-37}
class Avo::Resources::Post < Avo::BaseResource
self.grid_view = {
card: -> do
{
cover_url:
if record.cover_photo.attached?
main_app.url_for(record.cover_photo.url)
end,
title: record.name,
body: record.truncated_body
}
end,
html: -> do
{
title: {
index: {
wrapper: {
classes: "bg-blue-50 rounded-md p-2"
}
}
},
body: {
index: {
wrapper: {
classes: "bg-gray-50 rounded-md p-1"
}
}
},
cover: {
index: {
wrapper: {
classes: "blur-sm"
}
}
}
}
end
}
end
```
## Grid Item Badge
You can display and customize a badge on top of your grid items. Badges are useful for showing status indicators, labels, or other visual cues that help users quickly identify important information about each item.
### Complete Example
```ruby
# Dynamic badge based on record status
self.grid_view = {
card: -> do
{
cover_url: record.image.attached? ? main_app.url_for(record.image.variant(resize_to_fill: [300, 300])) : nil,
title: record.title,
body: simple_format(record.description),
badge: {
label: record.new? ? "New" : "Updated",
color: record.new? ? "green" : "orange",
style: record.new? ? "solid" : "subtle",
title: record.new? ? "New product available" : "Recently updated",
icon: record.new? ? "heroicons/outline/arrow-trending-up" : "heroicons/outline/arrow-path"
}
}
end
}
```
### Options
The visible text displayed on the badge. This is the primary content that users will see on your grid items.
```ruby
self.grid_view = {
card: -> do
{
badge: { label: "New" }
}
end
}
```
Sets the badge color. Accepts a static value or a proc for dynamic coloring based on the record.
#### Available colors
**Base colors:** `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`
**Semantic colors:** `neutral`, `success`, `danger`, `warning`, `info`
```ruby
self.grid_view = {
card: -> do
{
badge: {
label: "New",
color: "green"
}
}
end
}
```
Controls the badge appearance style.
#### Available styles
- `subtle` - Light background with colored text (default)
- `solid` - Solid colored background with white text
```ruby
self.grid_view = {
card: -> do
{
badge: {
label: "New",
color: "green",
style: "solid"
}
}
end
}
```
The tooltip text that appears when users hover over the badge. Useful for providing additional context or detailed information.
```ruby
self.grid_view = {
card: -> do
{
badge: {
label: "New",
title: "New product available"
}
}
end
}
```
Adds an icon to the badge.
```ruby
self.grid_view = {
card: -> do
{
badge: {
label: "New",
color: "green",
icon: "tabler/outline/trending-up"
}
}
end
}
```
---
# Map view
Some resources that contain geospatial data can benefit from being displayed on a map. For
resources to be displayed to the map view they require a `coordinates` field, but that's customizable.
## Enable map view
To enable map view for a resource, you need to add the `map_view` class attribute to a resource. That will add the view switcher to the view.
```ruby
class Avo::Resources::City < Avo::BaseResource
# ...
self.map_view = {
mapkick_options: {
controls: true
},
record_marker: -> {
{
latitude: record.coordinates.first,
longitude: record.coordinates.last,
tooltip: record.name
}
},
table: {
visible: true,
layout: :right
}
}
end
```
:::warning
You need to add the `mapkick-rb` (not `mapkick`) gem to your `Gemfile` and have the `MAPBOX_ACCESS_TOKEN` environment variable with a valid [Mapbox](https://account.mapbox.com/auth/signup/) key.
:::
The options you pass here are forwarded to the [`mapkick` gem](https://github.com/ankane/mapkick).
This block is being applied to all the records present in the current query to fetch the coordinates of off the record.
You may use this block to fetch the coordinates from other places (API calls, cache queries, etc.) rather than the database.
This block has to return a hash compatible with the [`PointMap` items](https://github.com/ankane/mapkick#point-map). Has to have `latitude` and `longitude` and optionally `tooltip`, `label`, or `color`.
This is the configuration for the adjacent table. You can set the visibility to `true` or `false`, and set the position of the table `:top`, `:right`, `:bottom`, or `:left`.
Available since version
Allow to define extra markers. The `extra_markers` block is executed in the `ExecutionContext` and should return an array of hashes.
For each extra marker, you can specify a label, tooltip, and color.
```ruby
self.map_view = {
# ...
extra_markers: -> do
[
{
latitude: 37.780411,
longitude: -25.497047,
label: "AΓ§ores",
tooltip: "SΓ£o Miguel",
color: "#0F0"
}
]
end,
# ...
}
```
## Make it the default view
To make the map view the default way of viewing a resource on , we have to use the `default_view_type` class attribute.
```ruby{7}
class Avo::Resources::City < Avo::BaseResource
self.default_view_type = :map
end
```
---
# Resource controllers
In order to benefit from Rails' amazing REST architecture, Avo generates a controller alongside every resource.
Generally speaking you don't need to touch those controllers. Everything just works out of the box with configurations added to the resource file.
However, sometimes you might need more granular control about what is happening in the controller actions or their callbacks. In that scenario you may take over and override that behavior.
## Request-Response lifecycle
Each interaction with the CRUD UI results in a request - response cycle. That cycle passes through the `BaseController`. Each auto-generated controller for your resource inherits from `ResourcesController`, which inherits from `BaseController`.
```ruby
class Avo::CoursesController < Avo::ResourcesController
end
```
In order to make your controllers more flexible, there are several overridable methods similar to how [devise](https://github.com/heartcombo/devise#controller-filters-and-helpers:~:text=You%20can%20also%20override%20after_sign_in_path_for%20and%20after_sign_out_path_for%20to%20customize%20your%20redirect%20hooks) overrides `after_sign_in_path_for` and `after_sign_out_path_for`.
## Create methods
For the `create` method, you can modify the `after_create_path`, the messages, and the actions both on success or failure.
Overriding this method, you can tell Avo what path to follow after a record was created with success.
```ruby
def after_create_path
"/avo/resources/users"
end
```
Override this method to create a custom response when a record was created with success.
```ruby
def create_success_action
respond_to do |format|
format.html { redirect_to after_create_path, notice: create_success_message}
end
end
```
Override this method to create a custom response when a record failed to be created.
```ruby
def create_fail_action
respond_to do |format|
flash.now[:error] = create_fail_message
format.html { render :new, status: :unprocessable_entity }
end
end
```
Override this method to change the message the user receives when a record was created with success.
```ruby
def create_success_message
"#{@resource.name} #{t("avo.was_successfully_created")}."
end
```
Override this method to change the message the user receives when a record failed to be created.
```ruby
def create_fail_message
t "avo.you_missed_something_check_form"
end
```
## Update methods
For the `update` method, you can modify the `after_update_path`, the messages, and the actions both on success or failure.
Overriding this method, you can tell Avo what path to follow after a record was updated with success.
```ruby
def after_update_path
"/avo/resources/users"
end
```
Override this method to create a custom response when a record was updated with success.
```ruby
def update_success_action
respond_to do |format|
format.html { redirect_to after_update_path, notice: update_success_message }
end
end
```
Override this method to create a custom response when a record failed to be updated.
```ruby
def update_fail_action
respond_to do |format|
flash.now[:error] = update_fail_message
format.html { render :edit, status: :unprocessable_entity }
end
end
```
Override this method to change the message the user receives when a record was updated with success.
```ruby
def update_success_message
"#{@resource.name} #{t("avo.was_successfully_updated")}."
end
```
Override this method to change the message the user receives when a record failed to be updated.
```ruby
def update_fail_message
t "avo.you_missed_something_check_form"
end
```
## Destroy methods
For the `destroy` method, you can modify the `after_destroy_path`, the messages, and the actions both on success or failure.
Overriding this method, you can tell Avo what path to follow after a record was destroyed with success.
```ruby
def after_update_path
"/avo/resources/users"
end
```
Override this method to create a custom response when a record was destroyed with success.
```ruby
def destroy_success_action
respond_to do |format|
format.html { redirect_to after_destroy_path, notice: destroy_success_message }
end
end
```
Override this method to create a custom response when a record failed to be destroyed.
```ruby
def destroy_fail_action
respond_to do |format|
format.html { redirect_back fallback_location: params[:referrer] || resources_path(resource: @resource, turbo_frame: params[:turbo_frame], view_type: params[:view_type]), error: destroy_fail_message }
end
end
```
Override this method to change the message the user receives when a record was destroyed with success.
```ruby
def destroy_success_message
t("avo.resource_destroyed", attachment_class: @attachment_class)
end
```
Override this method to change the message the user receives when a record failed to be destroyed.
```ruby
def destroy_fail_message
@errors.present? ? @errors.join(". ") : t("avo.failed")
end
```
---
# Breadcrumbs
Avo has a pretty advanced breadcrumbs system.
It will use the resource avatar or initials to display the breadcrumb and context.
!Breadcrumbs
It has minimal configuration options but you will have the oportunity to interact with it in a few places like the `avo.rb` config file or in the controller actions when you want to add breadcrumbs to a custom tool.
## API
## `add_breadcrumb`
This method is used to add a breadcrumb to the stack.
It takes the following arguments:
### Options
Sets the title of the breadcrumb.
```ruby
add_breadcrumb title: "Home"
add_breadcrumb title: "Details"
```
| Option | Value |
| --------------- | ------- |
| Required | `true` |
| Default value | `nil` |
| Possible values | Strings |
This sets the link of the breadcrumb.
```ruby
add_breadcrumb title: "Posts", path: avo.posts_path
add_breadcrumb title: "Custom tool", path: avo.custom_tool_path
```
:::warning
You're most probably linking to an internal Avo page, so you need to prefix the path using the `avo` dot as per Rails' engine rules. See Rails engines and path helpers for a full guide.
:::
| Option | Value |
| --------------- | ------- |
| Required | `false` |
| Default value | `nil` |
| Possible values | Strings |
Sets the icon of the breadcrumb.
```ruby
add_breadcrumb title: "Home", icon: "heroicons/outline/home"
add_breadcrumb title: "Details", icon: "heroicons/outline/information-circle"
```
| Option | Value |
| --------------- | ---------------------------------------------- |
| Required | `false` |
| Default value | `nil` |
| Possible values | Icon strings (e.g. `"heroicons/outline/home"`) |
Sets the initials displayed in the breadcrumb avatar when no icon is present. Useful for resource records where you want to show abbreviated identifiers (e.g. "JD" for a user named John Doe).
```ruby
add_breadcrumb title: "John Doe", initials: "JD", path: user_path(@user)
add_breadcrumb title: "Post #123", initials: "P123", path: post_path(@post)
```
| Option | Value |
| --------------- | ------- |
| Required | `false` |
| Default value | `nil` |
| Possible values | Strings |
It returns the breadcrumb object.
```ruby
add_breadcrumb title: "Home", path: root_path
```
## Breadcrumbs for custom pages
You can add breadcrumbs to custom pages in the controller action.
```ruby{3}
class Avo::ToolsController < Avo::ApplicationController
def custom_tool
add_breadcrumb title: "Custom tool", path: avo.custom_tool_path
end
end
```
---
# Fields
Fields are the backbone of a `Resource`.
Through fields you tell Avo what to fetch from the database and how to display it on the , , and views.
Fields can also be used in `Actions` to gather user input before running the action.
Avo ships with various simple fields like `text`, `textarea`, `number`, `password`, `boolean`, `select`, and more complex ones like `markdown`, `key_value`, `trix`, `tags`, and `code`.
## Declaring fields
You add fields to a resource through the `fields` method using the `field DATABASE_COLUMN, as: FIELD_TYPE, **FIELD_OPTIONS` notation.
```ruby
def fields
field :name, as: :text
end
```
The `name` property is the column in the database where Avo looks for information or a property on your model.
That will add a few fields in your new Avo app.
On the and views, we'll get a new text column of that record's database value.
Finally, on the and views, we will get a text input field that will display & update the `name` field on that model.
### Specific methods for each view
The `fields` method in your resource is invoked whenever non-specific view methods are present. To specify fields for each view or a group of views, you can use the following methods:
`index` view -> `index_fields`
`show` view -> `show_fields`
`edit` / `update` views -> `edit_fields`
`new` / `create` views -> `new_fields`
You can also register fields for a specific group of views as follows:
`index` / `show` views -> `display_fields`
`edit` / `update` / `new` / `create` views -> `form_fields`
When specific view fields are defined, they take precedence over view group fields. If neither specific view fields nor view group fields are defined, the fields will be retrieved from the `fields` method.
The below example use two custom helpers methods to organize the fields through `display_fields` and `form_fields`
:::code-group
```ruby [display_fields]
def display_fields
base_fields
tool_fields
end
```
```ruby [form_fields]
def form_fields
base_fields
tool_fields
tool Avo::ResourceTools::CityEditor, only_on: :forms
end
```
```ruby [tool_fields (helper method)]
# Notice that even if those fields are hidden on the form, we still include them on `form_fields`.
# This is because we want to be able to edit them using the tool.
# When submitting the form, we need this fields declared on the resource in order to know how to process them and fill the record.
def tool_fields
with_options hide_on: :forms do
field :name, as: :text, help: "The name of your city", filterable: true
field :population, as: :number, filterable: true
field :is_capital, as: :boolean, filterable: true
field :features, as: :key_value
field :image_url, as: :external_image
field :tiny_description, as: :markdown
field :status, as: :badge, enum: ::City.statuses
end
end
```
```ruby [base_fields (helper method)]
def base_fields
field :id, as: :id
field :coordinates, as: :location, stored_as: [:latitude, :longitude]
field :city_center_area,
as: :area,
geometry: :polygon,
mapkick_options: {
style: "mapbox://styles/mapbox/satellite-v9",
controls: true
},
datapoint_options: {
label: "Paris City Center",
tooltip: "Bonjour mes amis!",
color: "#009099"
}
field :description,
as: :trix,
attachment_key: :description_file,
visible: -> { resource.params[:show_native_fields].blank? }
field :metadata,
as: :code,
format_using: -> {
if view.edit?
JSON.generate(value)
else
value
end
},
update_using: -> do
ActiveSupport::JSON.decode(value)
end
field :created_at, as: :date_time, filterable: true
end
```
:::
:::warning In some scenarios fields require presence even if not visible
In certain situations, fields must be present in your resource configuration, even if they are hidden from view. Consider the following example where `tool_fields` are included within `form_fields` despite being wrapped in a `with_options hide_on: :forms do ... end` block.
For instance, when using `tool Avo::ResourceTools::CityEditor, only_on: :forms`, it will render the `features` field, which is of type `key_value`. When the form is submitted, Avo relies on the presence of the `features` field to determine its type and properly parse the submitted value.
If you omit the declaration of `field :features, as: :key_value, hide_on: :forms`, Avo will be unable to update that specific database column.
:::
## Field conventions
When we declare a field, we pinpoint the specific database row for that field. Usually, that's a snake case value.
Each field has a label. Avo will convert the snake case name to a humanized version.
In the following example, the `is_available` field will render the label as *Is available*.
```ruby
field :is_available, as: :boolean
```
:::info
If having the fields stacked one on top of another is not the right layout, try the resource-sidebar.
:::
### A more complex example
```ruby
class Avo::Resources::User < Avo::BaseResource
def fields
field :id, as: :id
field :first_name, as: :text
field :last_name, as: :text
field :email, as: :text
field :active, as: :boolean
field :cv, as: :file
field :is_admin?, as: :boolean
end
end
```
The `fields` method is already hydrated with the `current_user`, `params`, `request`, `view_context`, and `context` variables so you can use them to conditionally show/hide fields
```ruby
class Avo::Resources::User < Avo::BaseResource
def fields
field :id, as: :id
field :first_name, as: :text
field :last_name, as: :text
field :email, as: :text
field :is_admin?, as: :boolean
field :active, as: :boolean
if current_user.is_admin?
field :cv, as: :file
end
end
end
```
## Field Types
---
# Field options
Avo fields are dynamic and can be configured using field options.
There are quite a few **common field options** described on this page that will work with most fields (but some might not support them), and some **custom field options** that only some fields respond to that are described on each field page.
### Common field option example
```ruby
# disabled will disable the field on the `Edit` view
field :name, as: :text, disabled: true
field :status, as: :select, disabled: true
```
### Custom field option example
```ruby
# options will set the dropdown options for a select field
field :status, as: :select, options: %w[first second third]
```
## Change field name
To customize the label, you can use the `name` property to pick a different label.
```ruby
field :is_available, as: :boolean, name: "Availability"
```
## Showing / Hiding fields on different views
There will be cases where you want to show fields on different views conditionally. For example, you may want to display a field in the and views and hide it on the and views.
For scenarios like that, you may use the visibility helpers `hide_on`, `show_on`, `only_on`, and `except_on` methods. Available options for these methods are: `:new`, `:edit`, `:index`, `:show`, `:forms` (both `:new` and `:edit`) and `:all` (only for `hide_on` and `show_on`).
Version 3 introduces the `:display` option that is the opposite of `:forms`, referring to both, `:index` and `:show`
Be aware that a few fields are designed to override those options (ex: the `id` field is hidden in and ).
```ruby
field :body, as: :text, hide_on: [:index, :show]
```
Please read the detailed views page for more info.
## Field Visibility
You might want to restrict some fields to be accessible only if a specific condition applies. For example, hide fields if the user is not an admin.
You can use the `visible` block to do that. It can be a `boolean` or a lambda.
Inside the lambda, we have access to the `context` object and the current `resource`. The `resource` has the current `record` object, too (`resource.record`).
```ruby
field :is_featured, as: :boolean, visible: -> { context[:user].is_admin? } # show field based on the context object
field :is_featured, as: :boolean, visible: -> { resource.name.include? 'user' } # show field based on the resource name
field :is_featured, as: :boolean, visible: -> { resource.record.published_at.present? } # show field based on a record attribute
```
:::warning
On form submissions, the `visible` block is evaluated in the `create` and `update` controller actions. That's why you have to check if the `resource.record` object is present before trying to use it.
:::
```ruby
# `resource.record` is nil when submitting the form on resource creation
field :name, as: :text, visible -> { resource.record.enabled? }
# Do this instead
field :name, as: :text, visible -> { resource.record&.enabled? }
```
## Computed Fields
You might need to show a field with a value you don't have in a database row. In that case, you may compute the value using a block that receives the `record` (the actual database record), the `resource` (the configured Avo resource), and the current `view`. With that information, you can compute what to show on the field in the and views.
```ruby
field 'Has posts', as: :boolean do
record.posts.present?
rescue
false
end
```
:::info
Computed fields are displayed only on the and views.
:::
This example will display a boolean field with the value computed from your custom block.
## Fields Formatter
Sometimes you will want to process the database value before showing it to the user.
There are several ways to format fields.
### Common formatting options
In all cases, you have access to a bunch of variables inside this block, all the defaults that `Avo::ExecutionContext` provides plus `value`, `record`, `resource`, `view` and `field`.
Format the field value using a block.
:::info
Notice that this block will have effect on **all** views.
:::
Formatting affects copyable value, when using `format_using` with `copyable`, the formatted value is what gets copied to the clipboard, not the original database value. For example, using `format_using: -> { value.truncate(20) }` will copy the truncated text (including the `...`). If you need to display a truncated value while copying the full value, consider using CSS truncation via the `html` option instead of `format_using`.
```ruby
field :is_writer, as: :text, format_using: -> {
if view.form?
value
else
value.present? ? 'π' : 'π'
end
}
```
This example snippet will make the `:is_writer` field generate `π` or `π` emojis instead of `1` or `0` values on display views and the values `1` or `0` on form views.
Another example:
```ruby
field :company_url,
as: :text,
format_using: -> {
if view.new? || view.edit?
value
else
link_to(value, value, target: "_blank")
end
} do
main_app.companies_url(record)
end
```
Avo provides helper methods to format fields based on the current view.
- `format_index_using` - Format the field value **only** for the **index** view
- `format_show_using` - Format the field value **only** for the **show** view
- `format_edit_using` - Format the field value **only** for the **edit** view
- `format_new_using` - Format the field value **only** for the **new** view
- `format_form_using` - Format the field value **only** for the **forms** view (**new** and **edit**)
- `format_display_using` - Format the field value **only** for the **display** view (**index** and **show**)
This methods are available for all fields.
Example on how to format a field in the index and show views:
```ruby
field :is_writer, format_display_using: -> { value.present? ? 'π' : 'π' }
```
## Formatting with Rails helpers
You can also format using Rails helpers like `number_to_currency` (note that `view_context` is used to access the helper):
```ruby
field :price, as: :number, format_using: -> { view_context.number_to_currency(value) }
```
## Parse value before update
When it's necessary to parse information before storing it in the database, the `update_using` option proves to be useful. Inside the block you can access the raw `value` from the form, and the returned value will be saved in the database.
```ruby
field :metadata,
as: :code,
update_using: -> do
ActiveSupport::JSON.decode(value)
end
```
## Sortable fields
One of the most common operations with database records is sorting the records by one of your fields. For that, Avo makes it easy using the `sortable` option.
Add it to any field to make that column sortable in the view.
```ruby
field :name, as: :text, sortable: true
```
**Related:**
- Add an index on the `created_at` column
## Custom sortable block
When using computed fields or `belongs_to` associations, you can't set `sortable: true` to that field because Avo doesn't know what to sort by. However, you can use a block to specify how the records should be sorted in those scenarios.
```ruby{4-7}
class Avo::Resources::User < Avo::BaseResource
field :is_writer,
as: :text,
sortable: -> {
# Order by something else completely, just to make a test case that clearly and reliably does what we want.
query.order(id: direction)
},
hide_on: :edit do
record.posts.to_a.size > 0 ? "yes" : "no"
end
end
```
The block receives the `query` and the `direction` in which the sorting should be made and must return back a `query`.
In the example of a `Post` that `has_many` `Comment`s, you might want to order the posts by which one received a comment the latest.
You can do that using this query.
::: code-group
```ruby{5} [app/avo/resources/post.rb]
class Avo::Resources::Post < Avo::BaseResource
field :last_commented_at,
as: :date,
sortable: -> {
query.includes(:comments).order("comments.created_at #{direction}")
}
end
```
```ruby{4-6} [app/models/post.rb]
class Post < ApplicationRecord
has_many :comments
def last_commented_at
comments.last&.created_at
end
end
```
:::
## Placeholder
Some fields support the `placeholder` option, which will be passed to the inputs on and views when they are empty.
```ruby
field :name, as: :text, placeholder: 'John Doe'
```
## Required
To indicate that a field is mandatory, you can utilize the `required` option, which adds an asterisk to the field as a visual cue.
Avo automatically examines each field to determine if the associated attribute requires a mandatory presence. If it does, Avo appends the asterisk to signify its mandatory status. It's important to note that this option is purely cosmetic and does not incorporate any validation logic into your model. You will need to manually include the validation logic yourself, such as (`validates :name, presence: true`).
```ruby
field :name, as: :text, required: true
```
You may use a block as well. It will be executed in the `Avo::ExecutionContext` and you will have access to the `view`, `record`, `params`, `context`, `view_context`, and `current_user`.
```ruby
field :name, as: :text, required: -> { view == :new } # make the field required only on the new view and not on edit
```
## Disabled
When you need to prevent the user from editing a field, the `disabled` option will render it as `disabled` on and views and the value will not be passed to that record in the database. This prevents a bad actor to go into the DOM, enable that field, update it, and then submit it, updating the record.
```ruby
field :name, as: :text, disabled: true
```
### Disabled as a block
You may use a block as well. It will be executed in the `Avo::ExecutionContext` and you will have access to the `view`, `record`, `params`, `context`, `view_context`, and `current_user`.
```ruby
field :id, as: :number, disabled: -> { view == :edit } # make the field disabled only on the new edit view
```
## Readonly
When you need to prevent the user from editing a field, the `readonly` option will render it as `disabled` on and views. This does not, however, prevent the user from enabling the field in the DOM and send an arbitrary value to the database.
```ruby
field :name, as: :text, readonly: true
```
## Default Value
When you need to give a default value to one of your fields on the view, you may use the `default` block, which takes either a fixed value or a block.
```ruby
# using a value
field :name, as: :text, default: 'John'
# using a callback function
field :level, as: :select, options: { 'Beginner': :beginner, 'Advanced': :advanced }, default: -> { Time.now.hour < 12 ? 'advanced' : 'beginner' }
```
Sometimes you will need some extra text to explain better what the field is used for. You can achieve that by using the `help` method.
The value can be either text or HTML and is displayed only on the view (use [`label_help`](#label_help) option to display it on all views).
```ruby
# using the text value
field :custom_css, as: :code, theme: 'dracula', language: 'css', help: "This enables you to edit the user's custom styles."
# using HTML value
field :password, as: :password, help: 'You may verify the password strength here .'
```
## Help text
The `label_help` option allows you to add a help text below the label of a field on every view.
```ruby
# using the text value
field :custom_css, as: :code, theme: 'dracula', language: 'css', label_help: "This enables you to edit the user's custom styles."
# using HTML value
field :password, as: :password, label_help: 'You may verify the password strength here .'
```
## Nullable
When a user uses the **Save** button, Avo stores the value for each field in the database. However, there are cases where you may prefer to explicitly instruct Avo to store a `NULL` value in the database row when the field is empty. You do that by using the `nullable` option, which converts `nil` and empty values to `NULL`.
You may also define which values should be interpreted as `NULL` using the `null_values` method.
```ruby
# using default options
field :updated_status, as: :status, failed_when: [:closed, :rejected, :failed], loading_when: [:loading, :running, :waiting], nullable: true
# using custom null values
field :body, as: :textarea, nullable: true, null_values: ['0', '', 'null', 'nil', nil]
```
## Link to record
Sometimes, on the view, you may want a field in the table to be a link to that resource so that you don't have to scroll to the right to click on the icon. You can use `link_to_record` to change a table cell to be a link to that record.
```ruby
# for id field
field :id, as: :id, link_to_record: true
# for text field
field :name, as: :text, link_to_record: true
# for gravatar field
field :email, as: :gravatar, link_to_record: true
```
You can add this property on `id`, `text`, and `gravatar` fields.
Optionally you can enable the global config `id_links_to_resource`. More on that on the id links to resource docs page.
**Related:**
- ID links to resource
- Resource controls on the left side
## Align text on Index view
It's customary on tables to align numbers to the right. You can do that using the `html` option.
```ruby{2}
class Avo::Resources::Project < Avo::BaseResource
field :users_required, as: :number, html: {index: {wrapper: {classes: "text-right"}}}
end
```
## Stacked layout
For some fields, it might make more sense to use all of the horizontal area to display it. You can do that by changing the layout of the field wrapper using the `stacked` option.
```ruby
field :meta, as: :key_value, stacked: true
```
#### `inline` layout (default)
#### `stacked` layout
## Global `stacked` layout
You may also set all the fields to follow the `stacked` layout by changing the `field_wrapper_layout` initializer option from `:inline` (default) to `:stacked`.
```ruby
Avo.configure do |config|
config.field_wrapper_layout = :stacked
end
```
Now, all fields will have the stacked layout throughout your app.
## Field options
The field's `components` option allows you to customize the view components used for rendering the field in all, `index`, `show` and `edit` views. This provides you with a high degree of flexibility.
### Ejecting the field components
To start customizing the field components, you can eject one or multiple field components using the `avo:eject` command. Ejecting a field component generates the necessary files for customization. Here's how you can use the `avo:eject` command:
#### Ejecting All Components for a Field
`$ rails g avo:eject --field-components FIELD_TYPE --scope admin`
Replace `FIELD_TYPE` with the desired field type. For instance, to eject components for a Text field, use:
`$ rails g avo:eject --field-components text --scope admin`
This command will generate the files for all the index, edit and show components of the Text field, for each field type the amount of components may vary.
For more advanced usage check the eject documentation.
:::warning Scope
If you don't pass a `--scope` when ejecting a field view component, the ejected component will override the default components all over the project.
Check eject documentation for more details.
:::
### Customizing field components using `components` option
Here's some examples of how to use the `components` option in a field definition:
::: code-group
```ruby [Hash]
field :description,
as: :text,
components: {
index_component: Avo::Fields::Admin::TextField::IndexComponent,
show_component: Avo::Fields::Admin::TextField::ShowComponent,
edit_component: "Avo::Fields::Admin::TextField::EditComponent"
}
```
```ruby [Block]
field :description,
as: :text,
components: -> do
{
show_component: Avo::Fields::Admin::TextField::ShowComponent,
edit_component: "Avo::Fields::Admin::TextField::EditComponent"
}
end
```
:::
The components block it's executed using `Avo::ExecutionContent` and gives access to a bunch of variables as: `resource`, `record`, `view`, `params` and more.
`_component` is the key used to render the field's ``'s component, replace `` with one of the views in order to customize a component per each view.
:::warning Initializer
It's important to keep the initializer on your custom components as the original field view component initializer.
:::
### Attach HTML attributes
Using the `html` option you can attach `style`, `classes`, and `data` attributes. The `style` attribute adds the `style` tag to your element, `classes` adds the `class` tag, and the `data` attribute the `data` tag to the element you choose.
You may find more detailed information about the HTML attributes here.
The `summarizable` option allows you to generate a visual summary of a column's data distribution. This feature provides a quick and intuitive overview of your dataset by displaying a chart within the table header.
You can enable `summarizable` for a column like this:
```ruby
def fields
field :status, as: :select, summarizable: true
field :status, as: :badge, summarizable: true
end
```
### How It Works
When `summarizable` is enabled, a chart icon will appear in the table header for that column.
Clicking on the icon will display a summary chart based on the data in that column.
The chart provides a visual representation of data distribution, making it easier to analyze trends.
Allows to specify the target attribute on the model for each field. By default the target attribute is the field's id.
Usage example:
```ruby
field :status, as: :select, options: [:one, :two, :three], only_on: :forms
field :secondary_field_for_status,
as: :badge,
for_attribute: :status,
options: {info: :one, :success: :two, warning: :three},
except_on: :forms,
help: "Secondary field for status using the for_attribute option"
```
This handy option enables you to send arbitrary information to the field. It's especially useful when you're building your own custom fields or you are using [custom components](#components) for the built-in fields.
Usage example:
```ruby{4,9-11}
# meta as a hash
field :status,
as: :custom_status,
meta: {foo: :bar}
# meta as a block
field :status,
as: :badge,
meta: -> do
record.statuses.map(&:id)
end
```
Within your field template you can now access the `@field.meta` attribute.
```erb{2}
<%= field_wrapper **field_wrapper_args do %>
<% if @field.meta[:foo] %>
<%= @resource.record.foo_value %>
<% else %>
<%= @field.value %>
<% end %>
<% end %>
```
The `copyable` option enables users to copy the field's value to their clipboard. When set to `true`, a clipboard icon appears when hovering over the field value, allowing easy copying. This feature can be particularly useful for fields such as unique identifiers, URLs, or other text-based content that users may frequently need to copy.
```ruby
field :name, as: :text, copyable: true
```
The `copyable` option is available for text-based fields such as `:text`, `:textarea`, and others that render text values.
The `react_on` option enables dynamic reactivity for a field when changes occur elsewhere in the form. When a specified field changes, the current field is re-evaluated, and the `@record` object is refreshed with the latest form values.
:::tip
To retrieve the original value of a field before it was changed, use the [`*_was`](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-2A_was) methods.
```ruby
# Current from form
@record.country
"USA"
# Initial value
@record.country_was
"Spain"
```
:::
#### Possible values
You can configure the field to react to:
- A single field: `:field_one`
- Multiple fields: `[:field_one, :field_two]`
- All fields in the form: `:all`
#### Example
In the example below, the `city` field is set to react whenever the `country` select field is changed. This ensures that the available city options are always relevant to the selected country.
```ruby{11}
# app/avo/resources/course.rb
class Avo::Resources::Course < Avo::BaseResource
def fields
field :country,
as: :select,
options: Course.countries,
include_blank: "No country"
field :city,
as: :select,
react_on: :country,
options: -> { Course.cities.dig(@record.country&.to_sym) || [""] }
end
end
```
---
# Field Discovery
`discover_columns` and `discover_associations` automatically detect and configure fields for your Avo resources based on your model's database structure.
```rb{6-7}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
# ...
def fields
discover_columns
discover_associations
end
end
```
VIDEO
## Options
Specify which fields should be discovered, excluding all others.
```rb{6-7}
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
# ...
def fields
discover_columns only: [:title, :body, :published_at]
discover_associations only: [:author, :comments]
end
end
```
##### Default value
`nil`
#### Possible values
Array of symbols representing column or association names
Specify which fields should be excluded from discovery.
```rb{6-7}
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
# ...
def fields
discover_columns except: [:metadata, :internal_notes]
discover_associations except: [:audit_logs]
end
end
```
##### Default value
`nil`
#### Possible values
Array of symbols representing column or association names
Override how specific column names are mapped to field types globally.
```rb{5-8}
# config/initializers/avo.rb
Avo.configure do |config|
# ...
config.column_names_mapping = {
published_at: { field: :date_time, timezone: 'UTC' },
role: { field: :select, enum: -> { User.roles } }
}
end
```
##### Default value
`{}`
#### Possible values
Hash mapping column names to field configurations
Override how database column types are mapped to field types globally.
```rb{5-8}
# config/initializers/avo.rb
Avo.configure do |config|
# ...
config.column_types_mapping = {
jsonb: { field: :code, language: 'json' },
decimal: { field: :number, decimals: 2 }
}
end
```
##### Default value
`{}`
#### Possible values
Hash mapping database column types to field configurations
## Examples
### Basic Discovery
```rb{6-7}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
# ...
def fields
discover_columns
discover_associations
end
end
```
### Custom Field Options
This will add the provided options to every discovered field or association. This is particularly useful when having duplicative configurations across many fields.
```rb{6-7}
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
# ...
def fields
discover_columns help: "Automatically discovered fields"
discover_associations searchable: false
end
end
```
### Combining Manual and Discovered Fields
```rb{6,8-9,11}
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
# ...
def fields
field :custom_field, as: :text
discover_columns except: [:custom_field]
discover_associations
field :another_custom_field, as: :boolean
end
end
```
## Automatic Type Mapping
Field discovery maps database column types to Avo field types automatically.
e.g.
- `string` β `:text`
- `integer` β `:number`
- `float` β `:number`
- `datetime` β `:datetime`
- `boolean` β `:boolean`
- `json/jsonb` β `:code`
The full, up-to-date list can be found [here](https://github.com/avo-hq/avo/blob/main/lib/avo/mappings.rb)
## Association Discovery
The following associations are automatically configured:
- `belongs_to` β `:belongs_to`
- `has_one` β `:has_one`
- `has_many` β `:has_many`
- `has_one_attached` β `:file`
- `has_many_attached` β `:files`
- `has_rich_text` β `:trix`
- `acts-as-taggable-on :tags` β `:tags`
The full, up-to-date list can be found [here](https://github.com/avo-hq/avo/blob/main/lib/avo/mappings.rb)
---
# Resource Header
The resource header is a key component that, by default, is displayed at the top of your resource pages. It provides a consistent area showing the resource's title, description, profile photo, discreet information and controls.
The `header` DSL allows you to control where the header appears within your resource's field layout. By default, if you don't explicitly declare a `header`, Avo automatically generates one and places it at the top of the page.
```ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
self.title = :name
self.description = "Users of the application"
def fields
header # Explicitly place the header
card do
field :id, as: :id
field :email, as: :text
end
end
end
```
## Automatic Header Generation
If you don't explicitly define a `header` in your `fields` method, Avo will automatically create one and insert it as the first item on the page. This ensures that every resource page has a consistent header with the title, description, and controls.
```ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
self.title = :name
def fields
# No explicit header - Avo will automatically add one at the top
card do
field :id, as: :id
field :email, as: :text
end
end
end
```
## Positioning the Header
One of the key benefits of the `header` DSL is the ability to position it anywhere within your fields layout. This is particularly useful when you want to display some content before the main header.
```ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
self.title = :name
def fields
# Display a card before the header
card do
field :status, as: :badge
end
header # Header appears after the card
card do
field :id, as: :id
field :email, as: :text
end
end
end
```
---
# Resource panels
Panels are the backbone of Avo's display infrastructure. Most of the information that's on display is wrapped inside a panel. They help maintain a consistent design throughout Avo's pages. They are also available as a view component `Avo::PanelComponent` for custom tools, and you can make your own pages using it.
When using the fields DSL for resources, all fields declared in the root will be grouped into a "main" panel, but you can add your panels.
```ruby
class Avo::Resources::User < Avo::BaseResource
def fields
field :id, as: :id, link_to_record: true
field :email, as: :text, name: "User Email", required: true
panel name: "User information", description: "Some information about this user" do
field :first_name, as: :text, required: true, placeholder: "John"
field :last_name, as: :text, required: true, placeholder: "Doe"
field :active, as: :boolean, name: "Is active", show_on: :show
end
end
end
```
You can customize the panel `name` and panel `description`.
## What is the Main Panel?
The Main Panel is the primary container for fields in a resource. It typically includes the resource's title, action buttons, and fields that are part of the resource's core data. You can think of it as the central hub for managing and displaying the resource's information.
The Main Panel is automatically created by Avo based on your resource's field definitions. However, you can also customize it to meet your specific requirements.
## How does Avo compute panels?
By default Avo's field organization occurs behind the scenes, leveraging multiple panels to simplify the onboarding process and reduce complexity when granular customization is not needed.
When retrieving the fields, the first step involves categorizing them based on whether or not they have their own panel. Fields without their own panels are referred to as "standalone" fields. Notably, most association fields, such as `field :users, as: :has_many`, automatically have their dedicated panels.
During the Avo's grouping process, we ensure that the fields maintain the order in which they were declared.
Once the groups are established, we check whether the main panel has been explicitly declared within the resource. If it has been declared, this step is skipped. However, if no main panel declaration exists, we compute a main panel and assign the first group of standalone fields to it. This ensures that the field arrangement aligns with your resource's structure and maintains the desired order.
## Computed panels vs Manual customization
Let's focus on the `fields` method for the next examples. In these examples, we demonstrate how to achieve the same field organization using both computed panels and manual customization. Each example have the code that makes Avo compute the panels and also have an example on how to intentionally declare the panels in order to achieve the same result.
:::code-group
```ruby [Computed]
def fields
field :id, as: :id
field :name, as: :text
field :user, as: :belongs_to
field :type, as: :text
end
```
```ruby [Customized]
def fields
main_panel do
field :id, as: :id
field :name, as: :text
field :user, as: :belongs_to
field :type, as: :text
end
end
```
:::
On this example Avo figured out that a main panel was not declared and it computes one with all standalone fields.
Now let's add some field that is not standalone between `name` and `user` fields.
:::code-group
```ruby{5} [Computed]
def fields
field :id, as: :id
field :name, as: :text
field :reviews, as: :has_many
field :user, as: :belongs_to
field :type, as: :text
end
```
```ruby [Customized]
def fields
main_panel do
field :id, as: :id
field :name, as: :text
end
field :reviews, as: :has_many
panel do
field :user, as: :belongs_to
field :type, as: :text
end
end
```
:::
Since the field that has it owns panel was inserted between a bunch of standalone fields Avo will compute a main panel for the first batch of standalone fields (`id` and `name`) and will compute a simple panel for the remaining groups of standalone fields (`user` and `type`)
With these rules on mind we have the ability to keep the resource simple and also to fully customize it, for example, if we want to switch the computed main panel with the computed panel we can declare them in the desired order.
```ruby
def fields
panel do
field :user, as: :belongs_to
field :type, as: :text
end
field :reviews, as: :has_many
main_panel do
field :id, as: :id
field :name, as: :text
end
end
```
By using the `main_panel` and `panel` method, you can manually customize the organization of fields within your resource, allowing for greater flexibility and control.
## Index view fields
By default, only the fields declared in the root and the fields declared inside `main_panel` will be visible on the `Index` view.
```ruby{4-8}
class Avo::Resources::User < Avo::BaseResource
def fields
# Only these fields will be visible on the `Index` view
field :id, as: :id, link_to_record: true
field :email, as: :text, name: "User Email", required: true
field :name, as: :text, only_on: :index do
"#{record.first_name} #{record.last_name}"
end
# These fields will be hidden on the `Index` view
panel name: "User information", description: "Some information about this user" do
field :first_name, as: :text, required: true, placeholder: "John"
field :last_name, as: :text, required: true, placeholder: "Doe"
field :active, as: :boolean, name: "Is active", show_on: :show
end
end
end
```
The `visible` option allows you to dynamically control the visibility of a panel and all its children based on certain conditions.
This option is particularly useful when you need to show or hide entire sections of your resource at once without having to do it for each field.
Example:
```ruby
panel name: "User information", visible: -> { resource.record.enabled? } do
field :first_name, as: :text
field :last_name, as: :text
end
```
---
# Resource Sidebar
By default, all declared fields are going to be stacked vertically in the main area. But there are some fields with information that needs to be displayed in a smaller area, like boolean, date, and badge fields.
Those fields don't need all that horizontal space and can probably be displayed in a different space.
That's we created the **resource sidebar**.
## Adding fields to the sidebar
Using the `sidebar` block on a resource you may declare fields the same way you would do on the root level. Notice that the sidebar should be declared inside a panel. Each resource can have several panels or main panels and each panel can have it's own sidebars.
```ruby
class Avo::Resources::User < Avo::BaseResource
def fields
main_panel do
field :id, as: :id, link_to_record: true
field :first_name, as: :text, placeholder: "John"
field :last_name, as: :text, placeholder: "Doe"
# We can also add custom resource tools
tool UserTimeline
sidebar do
field :email, as: :gravatar, link_to_record: true, only_on: :show
field :active, as: :boolean, name: "Is active", only_on: :show
end
end
end
end
```
The fields will be stacked in a similar way in a narrower area on the side of the main panel. You may notice that inside each field, the tabel and value zones are also stacked one on top of the other to allow for a larger area to display the field value.
The `panel_wrapper` it's helpful when you want to render a custom tool inside a sidebar and you don't want to apply the `white_panel_classes` to it
```ruby
sidebar panel_wrapper: false do
tool Avo::ResourceTools::SidebarTool
end
```
---
# Tabs
Once your Avo resources reach a certain level of complexity, you might feel the need to better organize the fields, associations, and resource tools into groups. You can already use the `heading` to separate the fields inside a panel, but maybe you'd like to do more.
Tabs are a new layer of abstraction over panels. They enable you to group panels and tools together under a single pavilion and toggle between them.
```ruby
class Avo::Resources::User < Avo::BaseResource
def fields
field :id, as: :id, link_to_record: true
field :email, as: :text, name: "User Email", required: true
tabs do
tab title: "User information", description: "Some information about this user" do
panel do
field :first_name, as: :text, required: true, placeholder: "John"
field :last_name, as: :text, required: true, placeholder: "Doe"
field :active, as: :boolean, name: "Is active", show_on: :show
end
end
field :teams, as: :has_and_belongs_to_many
field :people, as: :has_many
field :spouses, as: :has_many
field :projects, as: :has_and_belongs_to_many
end
end
end
```
To use tabs, you need to open a `tabs` group block. Next, you add your `tab` block where you add fields and panels like you're used to on resource root. Most fields like `text`, `number`, `gravatar`, `date`, etc. need to be placed in a `panel`. However, the `has_one`, `has_many`, and `has_and_belongs_to_many` have their own panels, and they don't require a `panel` or a `tab`.
The tab `title` is mandatory and is what will be displayed on the tab switcher. The tab `description` is what will be displayed in the tooltip on hover.
## Tabs on Show view
Tabs have more than an aesthetic function. They have a performance function too. On the page, if you have a lot of `has_many` type of fields or tools, they won't load right away, making it a bit more lightweight for your Rails app. Instead, they will lazy-load only when they are displayed.
## Tabs on Edit view
All visibility rules still apply on , meaning that `has_*` fields will be hidden by default. However, you can enable them by adding `show_on: :edit`. All other fields will be loaded and hidden on page load. This way, when you submit a form, if you have validation rules in place requiring a field that's in a hidden tab, it will be present on the page on submit-time.
## Durable and "Bookmarkable"
Tabs remain durable within views, meaning that when switch between views, each tab group retains the selected tab. This ensures a consistent UX, allowing for seamless navigation without losing context.
Moreover, you have the ability to bookmark a link with a personalized tab selection.
This functionalities relies on the unique tab group ID. To take full advantage of this feature, it's important to assign a unique ID to each tab group defined in your application.
```ruby {1}
tabs id: :some_random_uniq_id do
field :posts, as: :has_many, show_on: :edit
end
```
## Display counter indicator on tabs switcher
Check this recipe on how to enhance your tabs switcher with a counter for each association tab.
## Visibility control
Both `tabs` and individual `tab` components support a `visible` option that allows you to dynamically control their visibility based on certain conditions. For example, you might want to hide a tab if the user doesn't have the necessary permissions to view its content.
The `visible` option allows you to control the visibility of either a group of tabs or an individual tab. It can be a `boolean` or a lambda.
#### Example
```ruby
tabs visible: -> { resource.record.enabled? } do
tab title: "General Information" do
panel do
field :name, as: :text
field :email, as: :text
end
end
tab title: "Admin Information", visible: -> { current_user.is_admin? } do
panel do
field :role, as: :text
field :permissions, as: :text
end
end
end
```
In this example:
- The entire group of tabs is only visible if the record is enabled (`resource.record.enabled?`).
- Within this group, the "General Information" tab is always visible when the tabs are shown.
- The "Admin Information" tab is only visible for admin records (`resource.record.admin?`).
The `title` option enables you to specify a label for the entire group of tabs. This title serves as an overarching descriptor for the collection, providing context regarding the purpose or content of the tabs.
You can define the title of a tabs group by passing it as an argument to the `tabs` block. The value should be a string that succinctly encapsulates the theme or purpose of the tabs.
```ruby
tabs title: "Tabs group title" do
# ...
end
```
The `description` option allows you to provide an auxiliary explanation or detailed note for the entire group of tabs. This can be used to elaborate on the purpose of the tabs or provide additional guidance.
You can define a description for a tabs group by passing it as an argument to the `tabs` block. The value should be a string that offers further clarity about the content or functionality of the tabs.
```ruby
tabs description: "Tabs group description" do
# ...
end
```
The `lazy_load` option enables deferred loading of tab content, improving performance by fetching data only when the tab is clicked. By default, `lazy_load` is set to `false`, ensuring that all tabs load immediately. However, in form views, this option is automatically disabled to prevent data loss during form submission.
```ruby{2}
tabs do
tab title: "Address", lazy_load: true do
# ...
end
end
```
---
# Array
The `Array` field in allows you to display and manage structured array data. This field supports flexibility in fetching and rendering data, making it suitable for various use cases.
:::tip Important
To use the `Array` field, you must create a resource specifically for it. Refer to the Array Resource documentation for detailed instructions.
For example, to use `field :attendees, as: :array`, you can generate an array resource by running the following command:
```bash
rails generate avo:resource Attendee --array
```
This step ensures the proper setup of your array field within the Avo framework.
:::
### Example 1: Array field with a block
You can define array data directly within a block. This is useful for static or pre-configured data:
```ruby{3-8}
class Avo::Resources::Course < Avo::BaseResource
def fields
field :attendees, as: :array do
[
{ id: 1, name: "John Doe", role: "Software Developer", organization: "TechCorp" },
{ id: 2, name: "Jane Smith", role: "Data Scientist", organization: "DataPros" }
]
end
end
end
```
:::warning Authorization
The `array` field internally inherits many behaviors from `has_many`, including authorization. If you are using authorization and the array field is not rendering, it is most likely not authorized.
To explicitly authorize it, define the following method in the resource's policy:
```ruby{3}
# app/policies/course_policy.rb
class CoursePolicy < ApplicationPolicy
def view_attendees? = true
end
```
For more details, refer to the view_{association}? documentation.
:::
### Example 2: Array field fetching data from the model's method
If no block is defined, Avo will attempt to fetch data by calling the corresponding method on the model:
```ruby
class Course < ApplicationRecord
def attendees
User.all.first(6) # Example fetching first 6 users
end
end
```
Here, the `attendees` field will use the `attendees` method from the `Course` model to render its data dynamically.
### Example 3: Fallback to the `records` method
If neither the block nor the model's method exists, Avo will fall back to the `records` method defined in the resource used to render the array field. This is useful for providing a default dataset.
When neither a block nor a model's method is defined, Avo will fall back to the `records` method in the resource used to render the field. This is a handy fallback for providing default datasets:
```ruby
class Avo::Resources::Attendee < Avo::Resources::ArrayResource
def records
[
{ id: 1, name: "Default Attendee", role: "Guest", organization: "DefaultOrg" }
]
end
end
```
## Summary of Data Fetching Hierarchy
When using `has_many` with `array: true`, Avo will fetch data in the following order:
1. Use data returned by the **block** provided in the field.
2. Fetch data from the **associated model method** (e.g., `Course#attendees`).
3. Fall back to the **`records` method** defined in the resource.
This hierarchy provides maximum flexibility and ensures seamless integration with both dynamic and predefined datasets.
---
# Avatar
The `Avatar` field is a field that displays a user's avatar or initials.
```ruby
field :avatar, as: :avatar
```
It does not take any option and is visible only on the view.
---
# Badge
The `badge` field is used to display an easily recognizable status of a record.
```ruby
field :status, as: :badge,
options: {
success: "Done",
danger: "Cancelled",
warning: "On hold",
green: "In review",
purple: "Idea"
}
```
## Description
The Badge field displays a colored indicator with optional icons. You can customize the color through the `options` mapping, and the `style` and `icon` for each value dynamically using procs.
The `Badge` field is intended to be displayed only on **Index** and **Show** views. To update the value shown by the badge field, use another field like [Text](#text) or [Select](#select) with `hide_on: [:index, :show]`.
## Options
Maps field values to badge colors. Keys are color names (semantic or base colors), and values can be a single value (string/symbol) or an array of values that should display with that color.
#### Available colors
**Base colors:** `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`
**Semantic colors:** `neutral`, `success`, `danger`, `warning`, `info`
:::info Default behavior
If a value doesn't match any of the configured options, it will default to `neutral`.
:::
```ruby
# Map values to colors
field :status, as: :badge,
options: {
success: ["Done", :completed], # Arrays with mixed types
danger: "Cancelled", # Single string
warning: :pending, # Single symbol
violet: "Idea" # Base color
}
```
Controls the badge appearance style.
#### Available styles
- `subtle` - Light background with colored text (default)
- `solid` - Solid colored background with white text
:::info Default behavior
If an invalid style is provided, it will default to `subtle`.
:::
```ruby
field :status, as: :badge,
options: { success: :done },
style: "solid"
# Or dynamically
field :status, as: :badge,
options: { success: :done },
style: -> { record.completed? ? "solid" : "subtle" }
```
Adds an icon to the badge.
```ruby
field :status, as: :badge,
options: { success: :done },
icon: "heroicons/outline/check-circle"
# Or dynamically
field :status, as: :badge,
options: { success: :done },
icon: -> {
record.approved? ? "heroicons/outline/check-circle" : "heroicons/outline/x-circle"
}
```
## Examples
### Using semantic colors
```ruby
field :status, as: :badge,
options: {
success: ["active", "completed"],
info: ["pending", "review"],
danger: ["failed", "cancelled"],
neutral: ["unknown"]
}
```
### Using base colors
```ruby
field :priority, as: :badge,
options: {
green: :low,
amber: :medium,
orange: :high,
red: :urgent
}
```
### Using Badge with a Select field for editing
Since Badge is display-only, pair it with a Select field to allow editing:
```ruby
field :stage,
as: :select,
hide_on: [:show, :index],
options: {
'Discovery': :discovery,
'Idea': :idea,
'Done': :done,
'On hold': 'on hold',
'Cancelled': :cancelled,
'Drafting': :drafting
},
placeholder: 'Choose the stage.'
field :stage,
as: :badge,
options: {
info: ["Discovery", "Idea"],
success: :Done,
warning: "On hold",
danger: "Cancelled",
neutral: :Drafting
},
style: -> { ["Done", "Cancelled"].include?(record.stage) ? "solid" : "subtle" },
icon: -> {
{
"Discovery" => "tabler/outline/zoom",
"Idea" => "tabler/outline/bulb",
"Drafting" => "tabler/outline/file-text",
"Done" => "tabler/outline/circle-check",
"On hold" => "tabler/outline/player-pause",
"Cancelled" => "tabler/outline/xbox-x"
}[record.stage]
}
```
---
# Boolean
The `Boolean` field renders a `input[type="checkbox"]` on **Form** views and a nice green `check` icon/red `X` icon on the **Show** and **Index** views.
```ruby
field :is_published,
as: :boolean,
name: 'Published',
true_value: 'yes',
false_value: 'no'
```
## Options
What should count as true. You can use `1`, `yes`, or a different value.
#### Default value
`[true, "true", "1"]`
What should count as false. You can use `0`, `no`, or a different value.
#### Default value
`[false, "false", "0"]`
When `true`, `nil` values render as a gray minus-circle icon on **Show** and **Index** views instead of the default dash. This keeps the `nil` value intact while making it more visible.
#### Default value
`false`
Render the field as a toggle on the form views.
#### Default value
`false`
---
# Boolean Group
The `BooleanGroup` is used to update a `Hash` with `string` keys and `boolean` values in the database.
It's useful when you have something like a roles hash in your database.
### DB payload example
An example of a boolean group object stored in the database:
```ruby
{
"admin": true,
"manager": true,
"writer": true,
}
```
### Field declaration example
Below is an example of declaring a `boolean_group` field for roles that matches the DB value from the example above:
```ruby
field :roles,
as: :boolean_group,
name: "User roles",
options: {
admin: "Administrator",
manager: "Manager",
writer: "Writer"
}
```
The `options` attribute should be a `Hash` where the keys match the DB keys and the values are the visible labels.
#### Default value
Empty `Hash`.
```ruby
{}
```
#### Computed options
You may need to compute the options dynamically for your `BooleanGroup` field. You can use a lambda for this, which provides access to the `record`, `resource`, `view`, and `field` properties where you can pull data off.
```ruby{5-9}
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
field :features,
as: :boolean_group,
options: -> do
record.features.each_with_object({}) do |feature, hash|
hash[feature.id] = feature.name.humanize
end
end
end
```
The output value must be a hash as described above.
## Updates
Before version Avo would override the whole attribute with only the payload sent from the client.
```json
// Before update.
{
"feature_enabled": true,
"another_feature_enabled": false,
"something_else": "some_value" // this will disappear
}
// After update.
{
"feature_enabled": true,
"another_feature_enabled": false,
}
```
will only update the keys that you send from the client.
```json
// Before update.
{
"feature_enabled": true,
"another_feature_enabled": false,
"something_else": "some_value" // this will be kept
}
// After update.
{
"feature_enabled": true,
"another_feature_enabled": false,
"something_else": "some_value"
}
```
---
# Code
The `Code` field generates a code editor using [codemirror](https://codemirror.net/) package. This field is hidden on **Index** view.
```ruby
field :custom_css, as: :code, theme: 'dracula', language: 'css'
```
## Options
Customize the color theme.
#### Default value
`material-darker`
#### Possible values
`material-darker`, `eclipse`, or `dracula`
Preview the themes here: [codemirror-themes](https://codemirror.net/demo/theme.html).
Customize the syntax highlighting using the language method.
#### Default value
`javascript`
#### Possible values
`css`, `dockerfile`, `htmlmixed`, `javascript`, `markdown`, `nginx`, `php`, `ruby`, `sass`, `shell`, `sql`, `vue` or `xml`.
Customize the height of the editor.
#### Default value
`auto`
#### Possible values
`auto`, or any value in pixels (eg `height: 250px`).
Customize the tab_size of the editor.
#### Default value
`2`
#### Possible values
Any integer value.
Customize the type of indentation.
#### Default value
`false`
#### Possible values
`true` or `false`
Customize whether the editor should apply line wrapping.
#### Default value
`true`
#### Possible values
`true` or `false`
---
# Country
`Country` field generates a [Select](#select) field on **Edit** view that includes all [ISO 3166-1](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) countries. The value stored in the database will be the country code, and the value displayed in Avo will be the name of the country.
:::warning
You must manually require the `countries` gem in your `Gemfile`.
```ruby
# All sorts of useful information about every country packaged as convenient little country objects.
gem "countries"
```
:::
```ruby
field :country, as: :country, display_code: true
```
## Options
You can easily choose to display the `code` of the country on **Index** and **Show** views by declaring `display_code` to `true`.
### Default value
`false`
### Possible values
`true`, `false`
---
# Date
The `Date` field may be used to display date values.
```ruby
field :birthday,
as: :date,
first_day_of_week: 1,
picker_format: "F J Y",
format: "yyyy-LL-dd",
placeholder: "Feb 24th 1955"
```
## Options
Format the date shown to the user on the `Index` and `Show` views.
#### Default
`{{ $frontmatter.default_format }}`
#### Possible values
Use [`luxon`](https://moment.github.io/luxon/#/formatting?id=table-of-tokens) formatting tokens.
Format the date shown to the user on the `Edit` and `New` views.
#### Default
`{{ $frontmatter.default_picker_format }}`
#### Possible values
Use [`flatpickr`](https://flatpickr.js.org/formatting) formatting tokens.
Passes the options here to [flatpickr](https://flatpickr.js.org/).
#### Default
`{}`
#### Possible values
Use [`flatpickr`](https://flatpickr.js.org/options) options.
By default, flatpickr is [disabled on mobile](https://flatpickr.js.org/mobile-support/) because the mobile date pickers tend to give a better experience, but you can override that using `disable_mobile: true` (misleading to set it to `true`, I know. We're just forwarding the option). So that will override that behavior and display flatpickr on mobile devices too.
Set which should be the first date of the week in the picker calendar. Flatpickr [documentation](https://flatpickr.js.org/localization/) on that. 1 is Monday, and 7 is Sunday.
#### Default value
`1`
#### Possible values
`1`, `2`, `3`, `4`, `5`, `6`, and `7`
---
# DateTime
The `DateTime` field is similar to the Date field with two new attributes. `time_24hr` tells flatpickr to use 24 hours format and `timezone` to tell it in what timezone to display the time. By default, it uses your browser's timezone.
```ruby
field :joined_at,
as: :date_time,
name: "Joined at",
picker_format: "Y-m-d H:i:S",
format: "yyyy-LL-dd TT",
time_24hr: true,
timezone: "PST"
```
## Options
Format the date shown to the user on the `Index` and `Show` views.
#### Default
`{{ $frontmatter.default_format }}`
#### Possible values
Use [`luxon`](https://moment.github.io/luxon/#/formatting?id=table-of-tokens) formatting tokens.
Format the date shown to the user on the `Edit` and `New` views.
#### Default
`{{ $frontmatter.default_picker_format }}`
#### Possible values
Use [`flatpickr`](https://flatpickr.js.org/formatting) formatting tokens.
Passes the options here to [flatpickr](https://flatpickr.js.org/).
#### Default
`{}`
#### Possible values
Use [`flatpickr`](https://flatpickr.js.org/options) options.
By default, flatpickr is [disabled on mobile](https://flatpickr.js.org/mobile-support/) because the mobile date pickers tend to give a better experience, but you can override that using `disable_mobile: true` (misleading to set it to `true`, I know. We're just forwarding the option). So that will override that behavior and display flatpickr on mobile devices too.
Set which should be the first date of the week in the picker calendar. Flatpickr [documentation](https://flatpickr.js.org/localization/) on that. 1 is Monday, and 7 is Sunday.
#### Default value
`1`
#### Possible values
`1`, `2`, `3`, `4`, `5`, `6`, and `7`
Displays time picker in 24-hour mode or AM/PM selection.
If `true`, the time will be relative to the configured `timezone`. If the timezone is not configured, the browser's timezone will be used.
If `false`, the time will be displayed as absolute in UTC and not change based on the browser's or configured timezone.
Select in which timezone the values should be cast.
:::warning
This option is only taken into account if the `relative` option is `true`.
:::
#### Default
If nothing is selected, the browser's timezone will be used.
#### Possible values
[TZInfo identifiers](https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html).
```ruby-vue{1,3}
field :start, as: :{{ $frontmatter.field_type }}, relative: true, timezone: "EET"
# Or
field :start, as: :{{ $frontmatter.field_type }}, relative: true, timezone: -> { record.timezone }
```
---
# EasyMDE
:::info
Before Avo 3.17 this field was called `markdown`. It was renamed to `easy_mde` so we can add our own implementation with `markdown`.
:::
The `easy_mde` field renders a [EasyMDE Markdown Editor](https://github.com/Ionaru/easy-markdown-editor) and is associated with a text or textarea column in the database.
`easy_mde` field converts text within the editor into raw Markdown text and stores it back in the database.
```ruby
field :description, as: :easy_mde
```
:::info
The `easy_mde` field is hidden from the **Index** view.
:::
## Options
By default, the content of the `easy_mde` field is not visible on the `Show` view, instead, it's hidden under a `Show Content` link that, when clicked, displays the content. You can set `easy_mde` to always display the content by setting `always_show` to `true`.
#### Default
`false`
#### Possible values
`true`, `false`
Sets the value of the editor
#### Default
`auto`
#### Possible values
`auto` or any number in pixels.
Toggles the editor's spell checker option.
```ruby
field :description, as: :easy_mde, spell_checker: true
```
#### Default
`false`
#### Possible values
`true`, `false`
---
# External image
You may have a field in the database that has the URL to an image, and you want to display that in Avo. That is where the `ExternalImage` field comes in to help.
It will take that value, insert it into an `image_tag`, and display it on the `Index` and `Show` views.
```ruby
field :logo, as: :external_image
```
## Options
All options can be static values or procs that are executed within Avo's execution context. When using procs, you have access to all the defaults that `Avo::ExecutionContext` provides plus:
- `record`
- `resource`
- `view`
- `field`
#### Default value
`40`
#### Possible values
Use any number to size the image, or a proc that returns a number.
#### Example with proc
```ruby
field :logo, as: :external_image, width: -> { view.index? ? 30 : 120 }
```
#### Default value
`40`
#### Possible values
Use any number to size the image, or a proc that returns a number.
#### Example with proc
```ruby
field :logo, as: :external_image, height: -> { view.index? ? 30 : 120 }
```
#### Default value
`0`
#### Possible values
Use any number to set the radius value, or a proc that returns a number.
#### Example with proc
```ruby
field :logo, as: :external_image, radius: -> { view.index? ? 4 : 8 }
```
Wraps the content into an anchor that links to the resource.
## Conditional sizing based on view
You can use procs to set different image dimensions and styling based on the current view:
```ruby
field :logo, as: :external_image,
width: -> { view.index? ? 40 : 150 },
height: -> { view.index? ? 40 : 150 },
radius: -> { view.index? ? 4 : 12 }
```
This example will display smaller, slightly rounded images on the index view (40x40px with 4px radius) and larger, more rounded images on the show view (150x150px with 12px radius).
## Use computed values
Another common scenario is to use a value from your database and create a new URL using a computed value.
```ruby
field :logo, as: :external_image do
"//logo.clearbit.com/#{URI.parse(record.url).host}?size=180"
rescue
nil
end
```
## Use in the Grid `cover` position
Another common place you could use it is in the grid `:cover` position.
```ruby
cover :logo, as: :external_image, link_to_record: true do
"//logo.clearbit.com/#{URI.parse(record.url).host}?size=180"
rescue
nil
end
```
---
# File
:::warning
You must manually require `activestorage` and `image_processing` gems in your `Gemfile`.
```ruby
# Active Storage makes it simple to upload and reference files
gem "activestorage"
# High-level image processing wrapper for libvips and ImageMagick/GraphicsMagick
gem "image_processing"
```
:::
The `File` field is the fastest way to implement file uploads in a Ruby on Rails app using [Active Storage](https://edgeguides.rubyonrails.org/active_storage_overview.html).
Avo will use your application's Active Storage settings with any supported [disk services](https://edgeguides.rubyonrails.org/active_storage_overview.html#disk-service).
```ruby
field :avatar, as: :file, is_image: true
```
## Authorization
:::info
Please ensure you have the `upload_{FIELD_ID}?`, `delete_{FIELD_ID}?`, and `download_{FIELD_ID}?` methods set on your model's **Pundit** policy. Otherwise, the input and download/delete buttons will be hidden.
:::
**Related:**
- Attachment pundit policies
## Variants
When using the `file` field to display an image, you can opt to show a processed variant of that image. This can be achieved using the `format_using` option.
### Example:
```ruby{3-5}
field :photo,
as: :file,
format_using: -> {
value.variant(resize_to_limit: [150, 150]).processed.image
}
```
## Options
Instructs the input to accept only a particular file type for that input using the `accept` option.
```ruby
field :cover_video, as: :file, accept: "image/*"
```
#### Default
`nil`
#### Possible values
`image/*`, `audio/*`, `doc/*`, or any other types from [the spec](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept).
If you have large files and don't want to overload the server with uploads, you can use the `direct_upload` feature, which will upload the file directly to your cloud provider.
```ruby
field :cover_video, as: :file, direct_upload: true
```
Option that specify if the file should have the caption present or not.
```ruby
field :cover_video, as: :file, display_filename: false
```
#### Default
`true`
#### Possible values
`true`, `false`
Wraps the content into an anchor that links to the resource.
---
# Files
:::warning
You must manually require `activestorage` and `image_processing` gems in your `Gemfile`.
```ruby
# Active Storage makes it simple to upload and reference files
gem "activestorage"
# High-level image processing wrapper for libvips and ImageMagick/GraphicsMagick
gem "image_processing"
```
:::
The `Files` field is similar to `File` and enables you to upload multiple files at once using the same easy-to-use [Active Storage](https://edgeguides.rubyonrails.org/active_storage_overview.html) implementation.
```ruby
field :documents, as: :files
```
## Options
Instructs the input to accept only a particular file type for that input using the `accept` option.
```ruby
field :cover_video, as: :file, accept: "image/*"
```
#### Default
`nil`
#### Possible values
`image/*`, `audio/*`, `doc/*`, or any other types from [the spec](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept).
If you have large files and don't want to overload the server with uploads, you can use the `direct_upload` feature, which will upload the file directly to your cloud provider.
```ruby
field :cover_video, as: :file, direct_upload: true
```
Option that specify if the file should have the caption present or not.
```ruby
field :cover_video, as: :file, display_filename: false
```
#### Default
`true`
#### Possible values
`true`, `false`
## Authorization
:::info
Please ensure you have the `upload_{FIELD_ID}?`, `delete_{FIELD_ID}?`, and `download_{FIELD_ID}?` methods set on your model's **Pundit** policy. Otherwise, the input and download/delete buttons will be hidden.
:::
**Related:**
- Attachment pundit policies
Set the default `view_type`.
#### Default
`grid`
#### Possible values
`grid`, `list`
Option to hide the view type switcher component.
#### Default
`false`
#### Possible values
`true`, `false`
---
# Gravatar
The `Gravatar` field turns an email field from the database into an avatar image if it's found in the [Gravatar](https://en.gravatar.com/site/implement/images/) database.
```ruby
field :email,
as: :gravatar,
rounded: false,
size: 60,
default_url: 'some image url'
```
## Options
Choose whether the rendered avatar should be rounded or not on the `Index` view.
On `Show`, the image is always a `square,` and the size is `responsive`.
#### Default
`true`
#### Possible values
`true`, `false`
Set the size of the avatar.
#### Default
`32`
#### Possible values
Any number in pixels. Remember that the size will influence the `Index` table row height.
Set the default image if the email address was not found in Gravatar's database.
#### Default
`32`
#### Possible values
Any number in pixels. Remember that the size will influence the `Index` table row height.
Wraps the content into an anchor that links to the resource.
## Using computed values
You may also pass in a computed value.
```ruby
field :email, as: :gravatar do
"#{record.google_username}@gmail.com"
end
```
---
# Heading
:::code-group
```ruby [Field id]
field :user_information, as: :heading
```
```ruby [Label]
field :some_id, as: :heading, label: "user information"
```
```ruby [Computed]
field :some_id, as: :heading do
"user information"
end
```
:::
The `Heading` field displays a header that acts as a separation layer between different sections.
`Heading` is not assigned to any column in the database and is only visible on the `Show`, `Edit` and `Create` views.
:::warning Computed heading
The computed fields are not rendered on form views, same with heading field, if computed syntax is used it will not be rendered on the form views. Use `label` in order to render it on **all** views.
:::
## Options
The `as_html` option will render it as HTML.
```ruby
field :dev_heading, as: :heading, as_html: true do
'DEV
'
end
```
#### Default
`false`
#### Possible values
`true`, `false`
The content of `label` is the content displayed on the heading space.
```ruby
field :some_id, as: :heading, label: "user information"
```
---
# Hidden
There are scenarios where in order to be able to submit a form, an input should be present but inaccessible to the user. An example of this might be where you want to set a field by default without the option to change, or see it. `Hidden` will render a ` ` element on the `Edit` and `New` page.
> Hidden will only render on the `Edit` and `New` views.
### Example usage:
```ruby
# Basic
field :group_id, as: :hidden
# With default
field :user_id, as: :hidden, default: -> { current_user.id }
# If the current_user is a admin
# 1. Allow them to see and select a user.
# 2. Remove the user_id field to prevent user_id it from overriding the user selection.
# Otherwise set the user_id to the current user and hide the field.
field :user, as: :belongs_to, visible: -> { context[:current_user].admin? }
field :user_id, as: :hidden, default: -> { current_user.id }, visible: -> { !context[:current_user].admin? }
```
---
# ID
The `id` field is used to show the record's id. By default, it's visible only on the `Index` and `Show` views. That is a good field to add the `link_to_record` option to make it a shortcut to the record `Show` page.
```ruby
field :id, as: :id
```
## Options
Wraps the content into an anchor that links to the resource.
---
# KeyValue
The `KeyValue` field makes it easy to edit flat key-value pairs stored in `JSON` format in the database.
```ruby
field :meta, as: :key_value
```
## Options
Customize the label for the key header.
#### Default
`I18n.translate("avo.key_value_field.key")`
#### Possible values
Any string value.
Customize the label for the value header.
#### Default
`I18n.translate("avo.key_value_field.value")`
#### Possible values
Any string value.
Customize the label for the add row button tooltip.
#### Default
`I18n.translate("avo.key_value_field.add_row")`
#### Possible values
Any string value.
Customize the label for the delete row button tooltip.
#### Default
`I18n.translate("avo.key_value_field.delete_row")`
#### Possible values
Any string value.
Set a custom label for the tooltip on the reorder by drag-and-drop row button.
#### Default
`I18n.translate("avo.key_value_field.reorder_row")`
#### Possible values
Any string value.
Toggle on/off the ability to disable editing keys, editing values, adding rows, and deleting rows for that field.
#### Default
`false`
#### Possible values
`true`, `false`
Toggle on/off the ability to edit the keys for that field. Turning this off will allow the user to customize only the value fields.
#### Default
`false`
#### Possible values
`true`, `false`
Toggle on/off the ability to edit the values for that field. Turning this off will allow the user to customize only the key fields.
#### Default
`false`
#### Possible values
`true`, `false`
Toggle on/off the ability to add new rows.
#### Default
`false`
#### Possible values
`true`, `false`
Toggle on/off the ability to delete rows from that field. Turning this on will prevent the user from deleting existing rows.
#### Default
`false`
#### Possible values
`true`, `false`
## Customizing the labels
You can easily customize the labels displayed in the UI by mentioning custom values in `key_label`, `value_label`, `action_text`, and `delete_text` properties when defining the field.
```ruby
field :meta, # The database field ID
as: :key_value, # The field type.
key_label: "Meta key", # Custom value for key header. Defaults to 'Key'.
value_label: "Meta value", # Custom value for value header. Defaults to 'Value'.
action_text: "New item", # Custom value for button to add a row. Defaults to 'Add'.
delete_text: "Remove item" # Custom value for button to delete a row. Defaults to 'Delete'.
```
## Enforce restrictions
You can enforce some restrictions by removing the ability to edit the field's key or value by setting `disable_editing_keys` or `disable_editing_values` to `true` respectively. If `disable_editing_keys` is set to `true`, be aware that this option will also disable adding rows as well. You can separately remove the ability to add a new row by setting `disable_adding_rows` to `true`. Deletion of rows can be enforced by setting `disable_deleting_rows` to `true`.
```ruby
field :meta, # The database field ID
as: :key_value, # The field type.
disable_editing_keys: false, # Option to disable the ability to edit keys. Implies disabling to add rows. Defaults to false.
disable_editing_values: false, # Option to disable the ability to edit values. Defaults to false.
disable_adding_rows: false, # Option to disable the ability to add rows. Defaults to false.
disable_deleting_rows: false # Option to disable the ability to delete rows. Defaults to false.
```
Setting `disabled: true` enforces all restrictions by disabling editing keys, editing values, adding rows, and deleting rows collectively.
```ruby
field :meta, # The database field ID
as: :key_value, # The field type.
disabled: true, # Option to disable editing keys, editing values, adding rows, and deleting rows. Defaults to false.
```
`KeyValue` is hidden on the `Index` view.
---
# Location
The `Location` field is used to display a point on a map.
```ruby
field :coordinates, as: :location
```
:::warning
You need to add the `mapkick-rb` (not `mapkick`) gem to your `Gemfile` and have the `MAPBOX_ACCESS_TOKEN` environment variable with a valid [Mapbox](https://account.mapbox.com/auth/signup/) key.
:::
## Description
By default, the location field is attached to one database column that has the coordinates in plain text with a comma `,` joining them (`latitude,longitude`).
Ex: `44.427946,26.102451`
Avo will take that value, split it by the comma and use the first element as the `latitude` and the second one as the `longitude`.
On the view you'll get in interactive map and on the edit you'll get one field where you can edit the coordinates.
## Options
It's customary to have the coordinates in two distinct database columns, one named `latitude` and another `longitude`.
You can instruct Avo to use those two with the `stored_as` option
#### Default value
`nil`
#### Possible values
`nil`, or `[:latitude, :longitude]`.
```ruby
field :coordinates, as: :location, stored_as: [:latitude, :longitude]
```
By using this notation, Avo will grab the `latitude` and `longitude` from those particular columns to compose the map.
This will also render the view with two separate fields to edit the coordinates.
The `mapkick_options` option allows you to customize the appearance and behavior of the map.
Using this option, you can provide a hash of configuration settings supported by the Mapkick gem, such as specifying the map style, enabling or disabling controls, or adding additional customizations.
#### Default
- When `static` is `true`:
```ruby
{
width: 300,
height: 300
}
```
- When `static` is `false`:
```ruby
{
id: "location-map",
zoom: 15,
controls: true
}
```
#### Possible values
Accepts the options as [specified in the Mapkick-gem](https://github.com/ankane/mapkick#options).
For example:
```ruby{4-7}
field :coordinates,
as: :location,
stored_as: [:latitude, :longitude],
mapkick_options: {
style: 'mapbox://styles/mapbox/satellite-v9',
controls: true
}
```
By using `mapkick_options`, you can tailor the map's look and functionality to suit your application's requirements.
The `static` option enables the rendering of a static map leveraging the power of the [mapkick-static](https://github.com/ankane/mapkick-static) gem.
:::warning
You need to add the [mapkick-static](https://github.com/ankane/mapkick-static) gem to your `Gemfile` and have the `MAPBOX_ACCESS_TOKEN` environment variable with a valid [Mapbox](https://account.mapbox.com/auth/signup/) key.
:::
#### Default
`false`
#### Possible values
`true` or `false`
```ruby{4}
field :coordinates,
as: :location,
stored_as: [:latitude, :longitude],
static: true,
mapkick_options: {
style: 'mapbox://styles/mapbox/satellite-v9'
}
```
---
# Markdown
:::info
In Avo 3.17 we renamed the `markdown` field `easy_mde` and introduced this custom one based on the [Marksmith editor](https://github.com/avo-hq/marksmith).
Please read the docs on the repo for more information on how it works.
:::
This field is inspired by the wonderful GitHub editor we all love and use.
It supports applying styles to the markup, dropping files in the editor, and using the Media Library.
The uploaded files will be taken over by Rails and persisted using Active Storage.
```ruby
field :body, as: :markdown
```
:::warning
Please ensure you have these gems in your `Gemfile`.
```ruby
gem "marksmith"
gem "commonmarker"
```
:::
VIDEO
## Supported features
- [x] ActiveStorage file attachments
- [x] Media Library integration
- [x] Preview panel
- [x] [Ready-to-use renderer](https://github.com/avo-hq/marksmith#built-in-preview-renderer)
- [x] Text formatting
- [x] Lists
- [x] Links
- [x] Images
- [x] Tables
- [x] Code blocks
- [x] Headings
## Customize the renderer
There are two places where we parse the markdown into the HTML you see.
1. In the controller
2. In the field component
You may customize the renderer by overriding the model.
```ruby
# app/models/marksmith/renderer.rb
module Marksmith
class Renderer
def initialize(body:)
@body = body
end
def render
if Marksmith.configuration.parser == "commonmarker"
render_commonmarker
elsif Marksmith.configuration.parser == "kramdown"
render_kramdown
else
render_redcarpet
end
end
def render_commonmarker
# commonmarker expects an utf-8 encoded string
body = @body.to_s.dup.force_encoding("utf-8")
Commonmarker.to_html(body)
end
def render_redcarpet
::Redcarpet::Markdown.new(
::Redcarpet::Render::HTML,
tables: true,
lax_spacing: true,
fenced_code_blocks: true,
space_after_headers: true,
hard_wrap: true,
autolink: true,
strikethrough: true,
underline: true,
highlight: true,
quote: true,
with_toc_data: true
).render(@body)
end
def render_kramdown
body = @body.to_s.dup.force_encoding("utf-8")
Kramdown::Document.new(body).to_html
end
end
end
```
Controls the visibility of the **"Attach from gallery"** option in the markdown editor.
##### Default value
`true`
#### Possible values
- `true`
- `false`
#### Code example
```ruby
field :body, as: :markdown, media_library: false
```
Controls the visibility of the **"Upload files"** option in the markdown editor.
##### Default value
`true`
#### Possible values
- `true`
- `false`
#### Code example
```ruby
field :body, as: :markdown, file_uploads: false
```
Sends additional parameters to the **preview renderer** of the markdown field.
Useful for injecting context-specific data during preview rendering.
##### Default value
`{}`
#### Possible values
- Any `Hash` of key-value pairs.
#### Code example
```ruby
field :body, as: :markdown, extra_preview_params: { foo: :bar }
```
---
# Money
The `Money` field is used to display a monetary value.
```ruby
field :price, as: :money, currencies: %w[EUR USD RON PEN]
```
## Money Field Example
You can explore the implementation of the money field in [avodemo](https://main.avodemo.com/avo/resources/products/new) and it's corresponding code on GitHub [here](https://github.com/avo-hq/main.avodemo.com/blob/main/app/avo/resources/product.rb)
### Example on new
### Example on show with currencies USD
### Example on show with currencies RON
### Example on index
## Installation
This field is a standalone gem.
You have to add it to your `Gemfile` alongside the `money-rails` gem.
:::info Add this field to the `Gemfile`
```ruby
# Gemfile
gem "avo-money_field"
gem "money-rails", "~> 1.12"
```
:::
:::warning Important: Monetization Requirement
In order to fully utilize the money field's features, you must monetize the associated attribute at the model level using the `monetize` method from the `money-rails` gem. ([Usage example](https://github.com/RubyMoney/money-rails?tab=readme-ov-file#usage-example))
For example:
```ruby
monetize :price_cents
```
Without this step, the money field may not behave as expected, and the field might not render.
:::
## Options
The `currencies` option controls which currencies will be visible on the dropdown.
```ruby
field :price, as: :money, currencies: %w[EUR USD RON PEN]
```
#### Default
By default it's going to be an empty array.
`[]`
#### Possible values
Add an array of currencies by the ISO code.
`%w[EUR USD RON PEN]`
---
# Number
The `number` field renders a `input[type="number"]` element.
```ruby
field :age, as: :number
```
## Options
Set the `min` attribute.
#### Default
`nil`
#### Possible values
Any number.
Set the `max` attribute.
#### Default
`nil`
#### Possible values
Any number.
Set the `step` attribute.
#### Default
`nil`
#### Possible values
Any number.
## Examples
```ruby
field :age, as: :number, min: 0, max: 120, step: 5
```
---
# Password
The `Password` field renders a `input[type="password"]` element for that field. By default, it's visible only on the `Edit` and `New` views.
```ruby
field :password, as: :password
```
#### Revealable
You can set the `revealable` to true to show an "eye" icon that toggles the password between hidden or visible.
**Related:**
- Devise password optional
---
# Preview
The `Preview` field adds a tiny icon to each row on the view that, when hovered, it will display a preview popup with more information regarding that record.
```ruby
field :preview, as: :preview
```
## Define the fields
The fields shown in the preview popup are configured similarly to how you configure the visibility in the different views.
When you want to display a field in the preview popup simply call the `show_on :preview` option on the field.
```ruby
field :name, as: :text, show_on :preview
```
## Authorization
Since version the preview request authorization is controller with the `preview?` policy method.
---
# Progress bar
The `ProgressBar` field renders a `progress` element on `Index` and `Show` views and and a `input[type=range]` element on `Edit` and `New` views.
```ruby
field :progress, as: :progress_bar
```
## Options
Sets the maximum value of the progress bar.
#### Default
`100`
#### Possible values
Any number.
Sets the step in which the user can move the slider on the `Edit` and `New` views.
#### Default
`1`
#### Possible values
Any number.
Choose if the value is displayed on the `Edit` and `New` views above the slider.
#### Default
`true`
#### Possible values
`true`, `false`
Set a string value to be displayed after the value above the progress bar.
#### Default
`nil`
#### Possible values
`%` or any other string.
## Examples
```ruby
field :progress,
as: :progress_bar,
max: 150,
step: 10,
display_value: true,
value_suffix: "%"
```
---
# Radio
The `Radio` field is used to render radio buttons. It's useful when only one value can be selected in a given options group.
### Field declaration example
Below is an example of declaring a `radio` field for a role:
```ruby
field :role,
as: :radio,
name: "User role",
options: {
admin: "Administrator",
manager: "Manager",
writer: "Writer"
}
```
The `options` attribute accepts either a `Hash` or a proc, allowing the incorporation of custom logic. Within this block, you gain access to all attributes of `Avo::ExecutionContext` along with the `record`, `resource`, `view` and `field`.
This attribute represents the options that should be displayed in the radio buttons.
#### Default value
Empty `Hash`.
```ruby
{}
```
#### Possible values
Any `Hash`. The keys represent the value that will be persisted and the values are the visible labels. Example:
```ruby
options: {
admin: "Administrator",
manager: "Manager",
writer: "Writer"
}
```
Or a `Proc`:
```ruby
options: -> do
record.roles.each_with_object({}) do |role, hash|
hash[role.id] = role.name.humanize
end
end
```
---
# Record link
Sometimes you just need to link to a field. That's it!
This is what this field does. You give it a record and it will link to it.
That record can come off an association a method or any kind of property on the record instance.
:::info Add this field to the `Gemfile`
```ruby
# Gemfile
gem "avo-record_link_field"
```
:::
:::warning
That record you're pointing to should have a resource configured.
:::
```ruby{14,19}
class Comment < ApplicationRecord
# Your model must return an instance of a record
has_one :post
# or
belongs_to :post
# or
def post
# trivially find a post
Post.find 42
end
end
# Calling the method like so will give us an instance of a Post
Comment.first.post => #
class Avo::Resources::Comment < Avo::BaseResource
def fields
# This will run `record.post` and try to display whatever is returned.
field :post, as: :record_link
end
end
```
## Options
Besides some of the default options, there are a few custom ones.
In case you want to set the target to `_blank`.
#### Default value
`nil`
#### Possible values
`:self`, `:blank`
#### Example
```ruby
field :post, as: :record_link, target: :blank
```
Because you only give it an instance of a record, Avo will try to guess which resource it should use to display the title of the record and how to compute it's link.
With more advanced configurations (when you have multiple resources for the same model) that resource might not be the one that you wish for.
Using the `use_resource` configuration value you can tell Avo which resource it should use.
#### Default value
`nil`
#### Possible values
`big_post`, `AdminUser`, `Avo::Resources::TinyPhoto`
#### Example
```ruby
field :post, as: :record_link, use_resource: "big_post"
field :admin, as: :record_link, use_resource: "AdminUser"
field :thumbnail, as: :record_link, use_resource: "Avo::Resources::TinyPhoto"
```
In other places where Avo generates a link to a record like in the `belongs_to` field, Avo adds `via` params to the URL so it knows how to generate the back button link.
That URL can also be passed on to other team mates and everyone can have the same navigation experience.
In the `record_link` field Avo adds these params automatically, but that might not be what you want. You can remove those `via` params by setting the `add_via_params` option to `false`.
#### Default value
`true`
#### Possible values
`true`, `false`
#### Example
```ruby
# This will generate a link similar to this
# https://example.com/avo/resources/projects/40?via_record_id=40&via_resource_class=Avo%3A%3AResources%3A%3AProject
field :post, as: :record_link, add_via_params: true
# This will generate a link similar to this
# https://example.com/avo/resources/projects/40
field :post, as: :record_link, add_via_params: false
```
## Using computed values
Of course you can take full control of this field and use your computed values too.
In order to do that, open a block and run some ruby query to return an instance of a record.
#### Example
```ruby
field :post, as: :record_link do
# This will generate a link similar to this
# https://example.com/avo/resources/posts/42
Post.find 42
end
# or
field :creator, as: :record_link, add_via_params: false do
user_id = SomeService.new(comment: record).fetch_user_id # returns 31
# This will generate a link similar to this
# https://example.com/avo/resources/users/31
User.find user_id
end
# or
field :creator, as: :record_link, use_resource: "AdminUser", add_via_params: false do
user_id = SomeService.new(comment: record).fetch_user_id # returns 31
# This will generate a link similar to this
# https://example.com/avo/resources/admin_users/31
User.find user_id
end
```
---
# Rhino
The wonderful [Rhino Editor](https://rhino-editor.vercel.app/) built by [Konnor Rogers](https://www.konnorrogers.com/) is available and fully integrated with Avo.
```ruby
field :body, as: :rhino
```
Rhino is based on [TipTap](https://tiptap.dev/) which is a powerful and flexible WYSIWYG editor.
It supports [ActiveStorage](https://guides.rubyonrails.org/active_storage_overview.html) file attachments, [ActionText](https://guides.rubyonrails.org/action_text_overview.html), and seamlessly integrates with the Media Library.
## Options
By default, the content of the field is not visible on the `Show` view; instead, it's hidden under a `Show Content` link that, when clicked, displays the content. You can set it to display the content by setting `always_show` to `true`.
---
# Select
The `Select` field renders a `select` field.
```ruby
field :type, as: :select, options: { 'Large container': :large, 'Medium container': :medium, 'Tiny container': :tiny }, display_value: true, placeholder: 'Choose the type of the container.'
```
A `Hash` representing the options that should be displayed in the select. The keys represent the labels, and the values represent the value stored in the database.
The options get cast as `ActiveSupport::HashWithIndifferentAccess` objects if they are a `Hash`.
#### Default
`nil`
#### Possible values
- `{ 'Large container': :large, 'Medium container': :medium, 'Tiny container': :tiny }` or any other `Hash`.
- A lambda function that returns a `Hash` (computed options)
### Computed options
You may want to compute the values on the fly for your `Select` field. You can use a lambda for that where you have access to the `record`, `resource`, `view`, and `field` properties where you can pull data off.
```ruby{5-7}
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
field :type,
as: :select,
options: -> do
record.get_types_from_the_database.map { |type| [type.name, type.id] }
end,
placeholder: 'Choose the type of the container.'
end
```
The output value must be a supported [`options_for_select`](https://apidock.com/rails/ActionView/Helpers/FormOptionsHelper/options_for_select) value.
When you need to organize your select options into groups, you can use `grouped_options` instead of `options`. This creates optgroups in the select field, making it easier for users to navigate large sets of options.
The `grouped_options` supports the same data structures as Rails' [`grouped_options_for_select`](https://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-grouped_options_for_select) helper.
#### Default
`nil`
#### Possible values
You can use either **Array syntax** or **Hash syntax**:
**Array syntax:**
```ruby
field :country,
as: :select,
grouped_options: [
['North America', [['United States', 'US'], 'Canada']],
['Europe', ['Denmark', 'Germany', 'France']]
]
```
**Hash syntax:**
```ruby
field :country,
as: :select,
grouped_options: {
'North America' => [['United States', 'US'], 'Canada'],
'Europe' => ['Denmark', 'Germany', 'France']
}
```
### Computed grouped options
Just like with regular options, you can compute grouped options dynamically using a lambda:
```ruby
field :country,
as: :select,
grouped_options: -> do
{
'North America' => Country.north_american.map { |c| [c.name, c.code] },
'Europe' => Country.european.map { |c| [c.name, c.code] },
'Asia' => Country.asian.map { |c| [c.name, c.code] }
}
end
```
:::warning
You should use either `options`, `grouped_options`, or `enum` - not multiple at the same time.
:::
Set the select options as an Active Record [enum](https://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html). You may use `options` or `enum`, not both.
```ruby{3,10}
# app/models/project.rb
class Project < ApplicationRecord
enum type: { 'Large container': 'large', 'Medium container': 'medium', 'Tiny container': 'small' }
end
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
field :type,
as: :select,
enum: ::Project.types,
display_value: true,
placeholder: 'Choose the type of the container.'
end
```
#### Default
`nil`
#### Possible values
`Post::statuses` or any other `enum` stored on a model.
You may want to display the values from the database and not the labels of the options. You may configure this behaviour by setting `display_value` to `true`. Note that this setting has no effect if an array of options is provided.
```ruby{5}
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
field :type,
as: :select,
display_value: true
end
```
#### Default
`false`
#### Possible values
`true`, `false`
The `Select` field also has the `include_blank` option. That can have three values.
If it's set to `false` (default), it will not show any blank option but only the options you configured.
If it's set to `true` and you have a `placeholder` value assigned, it will use that placeholder string as the first option.
If it's a string `include_blank: "No country"`, the `No country` string will appear as the first option in the `` and will set the value empty or `nil` depending on your settings.
```ruby{5}
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
field :type,
as: :select,
include_blank: 'No type'
end
```
#### Default
`nil`
#### Possible values
`nil`, `true`, `false`, or a string to be used as the first option.
If it's set to `false` (default), it will only allow selecting a single option from the list.
If it's set to `true`, it will enable multiple selections, allowing users to choose more than one option at a time.
```ruby{5}
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
field :categories,
as: :select,
multiple: true
end
```
#### Default
`false`
#### Possible values
`true` or `false`
---
# Stars
The `stars` field renders a star rating display on and views, and interactive clickable stars on and views. It's ideal for ratings, reviews, or any numeric value you want to represent visually as stars.
```ruby
field :rating, as: :stars
```
:::info
This field needs to be backed by a numeric column in your database (e.g., `integer`, `decimal`, or `float`).
:::
## Options
Sets the maximum number of stars to display.
#### Default
`5`
#### Possible values
Any positive integer.
## Examples
```ruby
field :rating, as: :stars
```
```ruby
field :rating, as: :stars, max: 10
```
The field stores a numeric value (e.g., `0` to `5` for a 5-star rating). On edit forms, users can click on stars to set the rating. Filled stars represent the current value, while unfilled stars show the remaining capacity up to the maximum.
---
# Status
Displays the status of a record in three ways; `loading`, `failed`, `success`, or `neutral`.
You may select the `loading`, `failed`, and `success` state values, and everything else will fall back to `neutral`.
```ruby
field :progress,
as: :status,
failed_when: [:closed, :rejected, :failed],
loading_when: [:loading, :running, :waiting, "in progress"],
success_when: [:done],
```
## Options
Set the values for when the status is `failed`.
#### Default value
`[]`
#### Possible values
`[:closed, :rejected, :failed]` or an array with strings or symbols that indicate the `failed` state.
Set the values for when the status is `loading`.
#### Default value
`[]`
#### Possible values
`[:loading, :running, :waiting, "in progress"]` or an array with strings or symbols that indicate the `loading` state.
Set the values for when the status is `success`.
#### Default value
`[]`
#### Possible values
`[:done, :success, :deployed, "ok"]` or an array with strings or symbols that indicate the `success` state.
Set the values for when the status is `neutral`.
#### Default value
`[]`
#### Possible values
`[:holding, "waiting"]` or an array with strings or symbols that indicate a `neutral` state.
---
# Tags field
Adding a list of things to a record is something we need to do pretty frequently; that's why having the `tags` field is helpful.
```ruby
field :skills, as: :tags
```
## Options
:::warning
**This warning no longer applies**
If you're using this field as `filterable`, dynamic filters are not yet picking these suggestions.
Please use the custom dynamic filters suggestions option to specify filter suggestions.
:::
You can give suggestions to your users to pick from which will be displayed to the user as a dropdown under the field.
```ruby{4,10-12}
# app/avo/resources/course.rb
class Avo::Resources::Course < Avo::BaseResource
def fields
field :skills, as: :tags, suggestions: -> { record.skill_suggestions }
end
end
# app/models/course.rb
class Course < ApplicationRecord
def skill_suggestions
['example suggestion', 'example tag', self.name]
end
end
```
#### Default
`[]`
#### Possible values
The `suggestions` option can be an array of strings, an object with the keys `value`, `label`, and (optionally) `avatar`, or a lambda that returns an array of that type of object.
The lambda is run inside a `ExecutionContext`, so it has access to the `record`, `resource`, `request`, `params`, `view`, and `view_context` along with other things.
```ruby{5-21}
# app/models/post.rb
class Post < ApplicationRecord
def self.tags_suggestions
# Example of an array of more advanced objects
[
{
value: 1,
label: 'one',
avatar: 'https://images.unsplash.com/photo-1560363199-a1264d4ea5fc?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&w=256&h=256&fit=crop',
},
{
value: 2,
label: 'two',
avatar: 'https://images.unsplash.com/photo-1567254790685-6b6d6abe4689?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&w=256&h=256&fit=crop',
},
{
value: 3,
label: 'three',
avatar: 'https://images.unsplash.com/photo-1560765447-da05a55e72f8?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&w=256&h=256&fit=crop',
},
]
end
end
```
The `disallowed` param works similarly to `suggestions`. Use it to prevent the user from adding specific values.
```ruby{3}
field :skills,
as: :tags,
disallowed: ["not", "that"]
```
#### Default
`[]`
#### Possible values
An array of strings representing the value that can't be stored in the database.
Set whether the field should accept other values outside the suggested ones. If set to `true` the user won't be able to add anything else than what you posted in the `suggestions` option.
```ruby{4}
field :skills,
as: :tags,
suggestions: %w(one two three),
enforce_suggestions: true
```
#### Default
`false`
#### Possible values
`true`, `false`
Set of suggestions that can be displayed at once. The excessive items will be hidden and the user will have to narrow down the query to see them.
```ruby{4}
field :skills,
as: :tags,
suggestions: %w(one two three),
suggestions_max_items: 2
```
#### Default
`20`
#### Possible values
Integers
Set whether the `suggestions` dropdown should close after the user makes a selection.
```ruby{4}
field :items,
as: :tags,
suggestions: -> { Post.tags_suggestions },
close_on_select: true
```
#### Default
`false`
#### Possible values
`true`, `false`
Set the field the `acts_as_taggable_on` is set.
#### Default
`nil`
#### Possible values
Any string or symbol you have configured on your corresponding model.
Set the characters that will cut off the content into tags when the user inputs the tags.
```ruby{3}
field :skills,
as: :tags,
delimiters: [",", " "]
```
#### Default
`[","]`
#### Possible values
`[",", " "]`
Valid values are comma `,` and space ` `.
By default, the tags field produces an array of items (ex: categories for posts), but in some scenarios you might want it to produce a single value (ex: dynamically search for users and select just one). Use `mode: :select` to make the field produce a single value as opposed to an array of values.
```ruby{3}
field :skills,
as: :tags,
mode: :select
```
#### Default
`nil`
#### Possible values
Valid values are `nil` for array values and `select` for a single value.
There might be cases where you want to dynamically fetch the values from an API. The `fetch_values_from` option enables you to pass a URL from where the field should suggest values.
This options works wonderful when used in Actions.
```ruby{3}
field :skills,
as: :tags,
fetch_values_from: "/avo/resources/skills/skills_for_user"
```
When the user searches for a record, the field will perform a request to the server to fetch the records that match that query.
#### Default
`nil`
#### Possible values
Valid values are `nil`, a string, or a block that evaluates to a string. The string should resolve to an endpoint that returns an array of objects with the keys `value` and `label`.
The endpoint will receive the user input as `q` in the params. It is accessible by using `params["q"]`.
::: code-group
```ruby{2-10} [app/controllers/avo/skills_controller.rb]
class Avo::SkillsController < Avo::ResourcesController
def skills_for_user
# You can access the user input by using params["q"]
skills = Skill.all.map do |skill|
{
value: skill.id,
label: skill.name
}
end
render json: skills
end
end
```
```ruby{13} [config/routes.rb]
Rails.application.routes.draw do
# your routes
authenticate :user, ->(user) { user.is_admin? } do
mount_avo
end
end
if defined? ::Avo
Avo::Engine.routes.draw do
scope :resources do
# Add route for the skills_for_user action
get "skills/skills_for_user", to: "skills#skills_for_user"
end
end
end
```
:::
:::info
When using the `fetch_labels_from` pattern, on the and views you will see the `id` of those options instead of the label.
That is expected, because you are storing the `id`s in the database and the field can't know what labels those `id`s have.
To mitigate that use the `fetch_labels` option.
:::
:::warning
Deprecated since in favor of `format_using`
:::
The `fetch_labels` option allows you to pass an array of custom strings to be displayed on the tags field. This option is useful when Avo is displaying a bunch of IDs and you want to show some custom label from that ID's record.
```ruby{4-6}
field :skills,
as: :tags,
fetch_values_from: "/avo/resources/skills/skills_for_user",
fetch_labels: -> {
Skill.where(id: record.skills).pluck(:name)
}
```
In the above example, `fetch_labels` is a lambda that retrieves the names of the skills stored in the record's `skills` property.
When you use `fetch_labels`, Avo passes the current `resource` and `record` as arguments to the lambda function. This gives you access to the hydrated resource and the current record.
#### Default
Avo's default behavior on tags
#### Possible values
- Array of strings
:::info
Since
:::
The `format_using` option allows you to pass an array of custom strings or hashes to be displayed on the tags field. This option is useful when Avo is displaying a bunch of IDs and you want to show some custom label from that ID's record.
```ruby{4-11}
field :skills,
as: :tags,
fetch_values_from: "/avo/resources/skills/skills_for_user",
format_using: -> {
Skill.find(value).map do |skill|
{
value: skill.id,
label: skill.name
}
end
}
```
In the above example, `format_using` is a lambda that retrieves the names and the ids of the skills stored in the record's `skills` property.
When you use `format_using`, Avo passes the `value`, current `resource` and `record` as arguments to the lambda function. This gives you access to the hydrated resource and the current record.
#### Default
Avo's default behavior on tags
#### Possible values
- Array of strings, notice that this will replace the DB values
- Array of hashes with `value` and `label` keys. WIll show the `label` and store the `value`
## PostgreSQL array fields
You can use the tags field with the PostgreSQL array field.
```ruby{11}
# app/avo/resources/course.rb
class Avo::Resources::Course < Avo::BaseResource
def fields
field :skills, as: :tags
end
end
# db/migrate/add_skills_to_courses.rb
class AddSkillsToCourses < ActiveRecord::Migration[6.0]
def change
add_column :courses, :skills, :text, array: true, default: []
end
end
```
## Acts as taggable on
One popular gem used for tagging is [`acts-as-taggable-on`](https://github.com/mbleigh/acts-as-taggable-on). The tags field integrates very well with it.
You need to add `gem 'acts-as-taggable-on', '~> 9.0'` in your `Gemfile`, add it to your model `acts_as_taggable_on :tags`, and use `acts_as_taggable_on` on the field.
```ruby{6}
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
def fields
field :tags,
as: :tags,
acts_as_taggable_on: :tags,
close_on_select: false,
placeholder: 'add some tags',
suggestions: -> { Post.tags_suggestions },
enforce_suggestions: true,
help: 'The only allowed values here are `one`, `two`, and `three`'
end
end
# app/models/post.rb
class Post < ApplicationRecord
acts_as_taggable_on :tags
end
```
That will let Avo know which attribute should be used to fill with the user's tags.
:::info Related
You can set up the tags as a resource using this guide.
:::
## Array fields
We haven't tested all the scenarios, but the tags field should play nicely with any array fields provided by Rails.
```ruby{10-12,14-16}
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
def fields
field :items, as: :tags
end
end
# app/models/post.rb
class Post < ApplicationRecord
def items=(items)
puts ["items->", items].inspect
end
def items
%w(1 2 3 4)
end
end
```
---
# Text
The `Text` field renders a regular ` ` element.
```ruby
field :title, as: :text
```
## Options
Displays the value as HTML on the `Index` and `Show` views. Useful when you need to link to another record.
```ruby
field :title, as: :text, as_html: true do
'Avo '
end
```
#### Default
`false`
#### Possible values
`true`, `false`
Render the value with a protocol prefix on the `Index` and `Show` views. So, for example, you can make a text field a `mailto` link very quickly.
```ruby{3}
field :email,
as: :text,
protocol: :mailto
```
#### Default
`nil`
#### Possible values
`mailto`, `tel`, or any other string value you need to pass to it.
Wraps the content into an anchor that links to the resource.
## Customization
You may customize the `Text` field with as many options as you need.
```ruby
field :title, # The database field ID
as: :text, # The field type
name: 'Post title', # The label you want displayed
required: true, # Display it as required
readonly: true, # Display it disabled
as_html: true # Should the output be parsed as html
placeholder: 'My shiny new post', # Update the placeholder text
format_using: -> { value.truncate 3 } # Format the output
```
---
# Textarea
The `textarea` field renders a `` element.
:::tip
By default, the `textarea` field don't have a component for the Index view. For this reason, on the Index view the field is not even visible.
Follow the Generating a custom component for a field guide to add a component to the index view for this field.
:::
```ruby
field :body, as: :textarea
```
## Options
Set the number of rows visible in the `Edit` and `New` views.
```ruby
field :body, as: :textarea, rows: 5
```
#### Default
`5`
#### Possible values
Any integer.
---
# Time
The `Time` field is similar to the DateTime field. It uses the time picker of flatpickr (without the calendar).
```ruby
field :starting_at,
as: :time,
picker_format: 'H:i',
format: "HH:mm",
relative: true,
picker_options: {
time_24hr: true
}
```
## Options
Format the date shown to the user on the `Index` and `Show` views.
#### Default
`{{ $frontmatter.default_format }}`
#### Possible values
Use [`luxon`](https://moment.github.io/luxon/#/formatting?id=table-of-tokens) formatting tokens.
Format the date shown to the user on the `Edit` and `New` views.
#### Default
`{{ $frontmatter.default_picker_format }}`
#### Possible values
Use [`flatpickr`](https://flatpickr.js.org/formatting) formatting tokens.
Passes the options here to [flatpickr](https://flatpickr.js.org/).
#### Default
`{}`
#### Possible values
Use [`flatpickr`](https://flatpickr.js.org/options) options.
By default, flatpickr is [disabled on mobile](https://flatpickr.js.org/mobile-support/) because the mobile date pickers tend to give a better experience, but you can override that using `disable_mobile: true` (misleading to set it to `true`, I know. We're just forwarding the option). So that will override that behavior and display flatpickr on mobile devices too.
Displays time picker in 24-hour mode or AM/PM selection.
If `true`, the time will be relative to the configured `timezone`. If the timezone is not configured, the browser's timezone will be used.
If `false`, the time will be displayed as absolute in UTC and not change based on the browser's or configured timezone.
Select in which timezone the values should be cast.
:::warning
This option is only taken into account if the `relative` option is `true`.
:::
#### Default
If nothing is selected, the browser's timezone will be used.
#### Possible values
[TZInfo identifiers](https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html).
```ruby-vue{1,3}
field :start, as: :{{ $frontmatter.field_type }}, relative: true, timezone: "EET"
# Or
field :start, as: :{{ $frontmatter.field_type }}, relative: true, timezone: -> { record.timezone }
```
---
# Tip Tap
The `TipTap` field is deprecated in favor of the Rhino field.
The Rhino field is a fork of the TipTap editor with some additional features and improvements.
The Rhino field is fully integrated with Avo and provides a seamless experience for managing rich text content using the [ActiveStorage](https://guides.rubyonrails.org/active_storage_overview.html) integration and the Media Library.
---
# Trix
```ruby
field :body, as: :trix
```
The `Trix` field renders a [WYSIWYG Editor](https://trix-editor.org/) and can be associated with a `string` or `text` column in the database. The value stored in the database will be the editor's resulting `HTML` content.
It supports [ActiveStorage](https://guides.rubyonrails.org/active_storage_overview.html) file attachments, [ActionText](https://guides.rubyonrails.org/action_text_overview.html), and seamlessly integrates with the Media Library.
Trix field is hidden from the `Index` view.
## Options
By default, the content of the field is not visible on the `Show` view; instead, it's hidden under a `Show Content` link that, when clicked, displays the content. You can set it to display the content by setting `always_show` to `true`.
Hides the attachments button from the Trix toolbar.
#### Default
`false`
#### Possible values
`true`, `false`
Hides the attachment's name from the upload output in the field value.
#### Default
`false`
#### Possible values
`true`, `false`
Hides the attachment size from the upload output in the field value.
#### Default
`false`
#### Possible values
`true`, `false`
Hides the attachment URL from the upload output in the field value.
#### Default
`false`
#### Possible values
`true`, `false`
Enables file attachments.
#### Default
`nil`
#### Possible values
`nil`, or a symbol representing the `has_many_attachments` key on the model.
## File attachments
:::warning
You must manually require `activestorage` and `image_processing` gems in your `Gemfile`.
```ruby
# Active Storage makes it simple to upload and reference files
gem "activestorage"
# High-level image processing wrapper for libvips and ImageMagick/GraphicsMagick
gem "image_processing"
```
:::
Trix supports drag-and-drop file attachments. To enable **Active Storage** integration, you must add the `attachment_key` option to your Trix field.
```ruby
field :body, as: :trix, attachment_key: :trix_attachments
```
That `attachment_key` has to have the same name as the model.
```ruby{2}
class Post < ApplicationRecord
has_many_attached :trix_attachments
end
```
Now, when you upload a file in the Trix field, Avo will create an Active Record attachment.
## Disable attachments
You may want to use Trix only as a text editor and disable the attachments feature. Adding the `attachments_disabled` option will hide the attachments button (paperclip icon).
```ruby
field :body, as: :trix, attachments_disabled: true
```
## Remove attachment attributes
By default, Trix will add some meta-data in the editor (filename, filesize, and URL) when adding an attachment. You might not need those to be present in the document. You can hide them using `hide_attachment_filename`, `hide_attachment_filesize`, and `hide_attachment_url`.
## Active Storage
Trix integrates seamlessly with Active Storage. When you use it with a plain database column on a record table (not with Action Text) you have to set the `attachment_key` option (documented above).
## Action Text
Trix integrates seamlessly with Action Text. It will automatically work with Action Text as well and it won't require you to add an `attachment_key`.
## Demo app
We prepared a [demo](https://trix.avodemo.com/) to showcase Trix's abilities to work with Action Text and Active Storage.
## Javascript Alert Messages
You can customize the javascript alert messages for various actions in the Trix editor. Below are the default messages that can be translated or modified:
```yml
avo:
this_field_has_attachments_disabled: This field has attachments disabled.
you_cant_upload_new_resource: You can't upload files into the Trix editor until you save the resource.
you_havent_set_attachment_key: You haven't set an `attachment_key` to this Trix field.
```
Refer to the [default](https://github.com/avo-hq/avo/blob/main/lib/generators/avo/templates/locales/avo.en.yml) for more details.
---
# Associations
One of the most amazing things about Ruby on Rails is how easy it is to create [Active Record associations](https://guides.rubyonrails.org/association_basics.html) between models. We try to keep the same simple approach in Avo too.
:::warning
It's important to set the `inverse_of` as often as possible to your model's association attribute.
:::
- Belongs to
- Has one
- Has many
- Has many through
- Has and belongs to many
Nested association forms (the `nested` option on those fields) require the **`avo-nested`** gem in addition to your usual Avo gems. Use the same source and credentials as for your other private Avo gems; see Gem server authentication.
## Single Table Inheritance (STI)
When you have models that share behavior and fields with STI, Rails will cast the model as the final class no matter how you query it.
```ruby
# app/models/user.rb
class User < ApplicationRecord
end
# app/models/super_user.rb
class SuperUser < User
end
# User.all.map(&:class) => [User, SuperUser]
```
For example, when you have two models, `User` and `SuperUser` with STI, when you call `User.all`, Rails will return an instance of `User` and an instance of `SuperUser`. That confuses Avo in producing the proper resource of `User`. That's why when you deal with STI, the final resource `Avo::Resources::SuperUser` should receive the underlying `model_class` so Avo knows which model it represents.
```ruby{5}
# app/avo/resources/super_user.rb
class Avo::Resources::SuperUser < Avo::BaseResource
self.title = :name
self.includes = []
self.model_class = "SuperUser"
def fields
field :id, as: :id
field :name, as: :text
end
end
```
## Link to child resource when using STI
Let's take another example. We have a `Person` model and `Sibling` and `Spouse` models that inherit from it.
You may want to use the `Avo::Resources::Person` to list all the records, but when your user clicks on a person, you want to use the inherited resources (`Avo::Resources::Sibiling` and `Avo::Resources::Spouse`) to display the details. The reason is that you may want to display different fields or resource tools for each resource type.
There are two ways you can use this:
1. `self.link_to_child_resource = true` Declare this option on the parent resource. When a user is on the view of your the `Avo::Resources::Person` and clicks on the view button of a `Person` they will be redirected to a `Child` or `Spouse` resource instead of a `Person` resource.
2. `field :peoples, as: :has_many, link_to_child_resource: false` Use it on a `has_many` field. On the `Avo::Resources::Person` you may want to show all the related people on the page, but when someone click on a record, they are redirected to the inherited `Child` or `Spouse` resource.
## Add custom labels to the associations' pages
You might want to change the name that appears on the association page. For example, if you're displaying a `team_members` association, your users will default see `Team members` as the title, but you'd like to show them `Members`.
You can customize that using fields localization.
---
# Belongs to
```ruby
field :user, as: :belongs_to
```
You will see three field types when you add a `BelongsTo` association to a model.
## Options
Turns the attach field/modal from a `select` input to a searchable experience
```ruby{5}
class Avo::Resources::CourseLink < Avo::BaseResource
def fields
field :links,
as: :has_many,
searchable: true
end
end
```
:::warning
Avo uses the **resource search feature** behind the scenes, so **make sure the target resource has the `search_query` option configured**.
:::
```ruby{3-7}
# app/avo/resources/course_link.rb
class Avo::Resources::CourseLink < Avo::BaseResource
self.search = {
query: -> {
query.ransack(id_eq: q, link_cont: q, m: "or").result(distinct: false)
}
}
end
```
#### Default
`false`
#### Possible values
`true`, `false`
Keeps the field enabled when visiting from the parent record.
#### Default
`false`
#### Possible values
`true`, `false`
Scope out the records the user sees on the Attach modal.
#### Default
`nil`
#### Possible values
```ruby{3}
field :user,
as: :belongs_to,
attach_scope: -> { query.non_admins }
```
Pass in a block where you attach scopes to the `query` object and `parent` object, which is the actual record where you want to assign the association. The block is executed in the `ExecutionContext`.
:::warning
The `attach_scope` will not filter the records in the listing from `has_many` or `has_and_belongs_to_many` associations.
Use [`scope`](#scope) or a Pundit policy `Scope` for that.
:::
```ruby-vue{3}
field :members,
as: :{{ $frontmatter.field_type }},
attach_scope: -> { query.where.not(team_id: parent.id) }
```
In this example, in the `attach_scope`, we ensure that when attaching members to a team, only those who are not already members will appear in the list of options.
Sets the field as polymorphic with the key set on the model.
#### Default
`nil`
#### Possible values
A symbol, used on the `belongs_to` association with `polymorphic: true`.
:::warning
You must use this option with the `types` option.
:::
#### Example
```ruby
field :commentable, as: :belongs_to, polymorphic_as: :commentable, types: [::Post, ::Project]
```
Sets the types the field can morph to.
#### Default
`[]`
#### Possible values
`[Post, Project, Team]`. Any array of model names.
:::warning
You must use this option with the `polymorphic_as` option.
:::
#### Example
```ruby
field :commentable, as: :belongs_to, polymorphic_as: :commentable, types: [::Post, ::Project]
```
Sets the help text for the polymorphic type dropdown. Useful when you need to specify to the user why and what they need to choose as polymorphic.
#### Default
`nil`
#### Possible values
Any string.
Sets a different resource to be used when displaying (or redirecting to) the association table.
#### Default
`nil`. When nothing is selected, Avo infers the resource type from the reflected association.
#### Possible values
`Avo::Resources::Post`, `Avo::Resources::PhotoComment`, or any Avo resource class.
The value can be the actual class or a string representation of that class.
```ruby
# the class
Avo::Resources::Post
# the string representation of the class
"Avo::Resources::Post"
```
Controls the creation link visibility on forms.
#### Default
`true`
#### Possible values
`true`, `false`
:::warning Since version , the target resource policy takes precedence over this option.
`field :user, as: :belongs_to, can_create: true`
In this example, even if the `can_create` option is set to `true`, if the `UserPolicy` responds with `false` to the `create?` method, the creation link will **NOT** be visible.
:::
## Overview
On the `Index` and `Show` views, Avo will generate a link to the associated record containing the `self.title` value of the target resource.
On the `Edit` and `New` views, Avo will generate a dropdown element with the available records where the user can change the associated model.
## Polymorphic `belongs_to`
To use a polymorphic relation, you must add the `polymorphic_as` and `types` properties.
```ruby{13}
class Avo::Resources::Comment < Avo::BaseResource
self.title = :id
def fields
field :id, as: :id
field :body, as: :textarea
field :excerpt, as: :text, show_on: :index do
ActionView::Base.full_sanitizer.sanitize(record.body).truncate 60
rescue
""
end
field :commentable, as: :belongs_to, polymorphic_as: :commentable, types: [::Post, ::Project]
end
end
```
## Polymorphic help
When displaying a polymorphic association, you will see two dropdowns. One selects the polymorphic type (`Post` or `Project`), and one for choosing the actual record. You may want to give the user explicit information about those dropdowns using the `polymorphic_help` option for the first dropdown and `help` for the second.
```ruby{17-18}
class Avo::Resources::Comment < Avo::BaseResource
self.title = :id
def fields
field :id, as: :id
field :body, as: :textarea
field :excerpt, as: :text, show_on: :index do
ActionView::Base.full_sanitizer.sanitize(record.body).truncate 60
rescue
""
end
field :reviewable,
as: :belongs_to,
polymorphic_as: :reviewable,
types: [::Post, ::Project, ::Team],
polymorphic_help: "Choose the type of record to review",
help: "Choose the record you need."
end
end
```
## Searchable `belongs_to`
There might be the case that you have a lot of records for the parent resource, and a simple dropdown won't cut it. This is where you can use the `searchable` option to get a better search experience for that resource.
```ruby{8}
class Avo::Resources::Comment < Avo::BaseResource
self.title = :id
def fields
field :id, as: :id
field :body, as: :textarea
field :user, as: :belongs_to, searchable: true
end
end
```
`searchable` works with `polymorphic` `belongs_to` associations too.
```ruby{8}
class Avo::Resources::Comment < Avo::BaseResource
self.title = :id
def fields
field :id, as: :id
field :body, as: :textarea
field :commentable, as: :belongs_to, polymorphic_as: :commentable, types: [::Post, ::Project], searchable: true
end
end
```
:::info
Avo uses the resource search feature behind the scenes, so **make sure the target resource has the `query` option configured inside the `search` block**.
:::
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.search = {
query: -> {
query.ransack(id_eq: q, name_cont: q, body_cont: q, m: "or").result(distinct: false)
}
}
end
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
self.search = {
query: -> {
query.ransack(id_eq: q, name_cont: q, country_cont: q, m: "or").result(distinct: false)
}
}
end
```
## Belongs to attach scope
When you edit a record that has a `belongs_to` association, on the edit screen, you will have a list of records from which you can choose a record to associate with.
For example, a `Post` belongs to a `User`. So on the post edit screen, you will have a dropdown (or a search field if it's [searchable](#searchable-belongs-to)) with all the available users. But that's not ideal. For example, maybe you don't want to show all the users in your app but only those who are not admins.
You can use the `attach_scope` option to keep only the users you need in the `belongs_to` dropdown field.
You have access to the `query` that you can alter and return it and the `parent` object, which is the actual record where you want to assign the association (the true `Post` in the below example).
```ruby
# app/models/user.rb
class User < ApplicationRecord
scope :non_admins, -> { where "(roles->>'admin')::boolean != true" }
end
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
def fields
field :user, as: :belongs_to, attach_scope: -> { query.non_admins }
end
end
```
For scenarios where you need to add a record associated with that resource (you create a `Post` through a `Category`), the `parent` is unavailable (the `Post` is not persisted in the database). Therefore, Avo makes the `parent` an instantiated object with its parent populated (a `Post` with the `category_id` populated with the parent `Category` from which you started the creation process) so you can better scope out the data (you know from which `Category` it was initiated).
## Allow detaching via the association
When you visit a record through an association, that `belongs_to` field is disabled. There might be cases where you'd like that field not to be disabled and allow your users to change that association.
You can instruct Avo to keep that field enabled in this scenario using `allow_via_detaching`.
```ruby{12}
class Avo::Resources::Comment < Avo::BaseResource
self.title = :id
def fields
field :id, as: :id
field :body, as: :textarea
field :commentable,
as: :belongs_to,
polymorphic_as: :commentable,
types: [::Post, ::Project],
allow_via_detaching: true
end
end
```
---
# Has One
:::warning
It's important to set the `inverse_of` as often as possible to your model's association attribute.
:::
# Has One
The `HasOne` association shows the unfolded view of your `has_one` association. It's like peaking on the `Show` view of that associated record. The user can also access the `Attach` and `Detach` buttons.
```ruby
field :admin, as: :has_one
```
## Options
Turns the attach field/modal from a `select` input to a searchable experience
```ruby{5}
class Avo::Resources::CourseLink < Avo::BaseResource
def fields
field :links,
as: :has_many,
searchable: true
end
end
```
:::warning
Avo uses the **resource search feature** behind the scenes, so **make sure the target resource has the `search_query` option configured**.
:::
```ruby{3-7}
# app/avo/resources/course_link.rb
class Avo::Resources::CourseLink < Avo::BaseResource
self.search = {
query: -> {
query.ransack(id_eq: q, link_cont: q, m: "or").result(distinct: false)
}
}
end
```
#### Default
`false`
#### Possible values
`true`, `false`
Scope out the records the user sees on the Attach modal.
#### Default
`nil`
#### Possible values
```ruby{3}
field :user,
as: :belongs_to,
attach_scope: -> { query.non_admins }
```
Pass in a block where you attach scopes to the `query` object and `parent` object, which is the actual record where you want to assign the association. The block is executed in the `ExecutionContext`.
:::warning
The `attach_scope` will not filter the records in the listing from `has_many` or `has_and_belongs_to_many` associations.
Use [`scope`](#scope) or a Pundit policy `Scope` for that.
:::
```ruby-vue{3}
field :members,
as: :{{ $frontmatter.field_type }},
attach_scope: -> { query.where.not(team_id: parent.id) }
```
In this example, in the `attach_scope`, we ensure that when attaching members to a team, only those who are not already members will appear in the list of options.
## Show on edit screens
By default, the `{{ $frontmatter.field_type }}` field is only visible in the show view. To make it available in the edit view as well, include the `show_on: :edit` option. This ensures that the `{{ $frontmatter.field_type }}` show view component is also rendered within the edit view.
## Nested in Forms
You can use ["Show on edit screens"](#show-on-edit-screens) to make the `{{ $frontmatter.field_type }}` field available in the edit view. However, this will render it using the show view component.
To enable nested creation for the `{{ $frontmatter.field_type }}` field, allowing it to be created and / or edited alongside its parent record within the same form, use the `nested` option which is a hash with configurable option.
:::info The `avo-nested` gem
Nested association forms are provided by the **`avo-nested`** gem. Add it to your `Gemfile` using the same private gem source as your other Avo paid gems:
```ruby
gem "avo-nested", source: "https://packager.dev/avo-hq/"
```
Run `bundle install`. If you have not set up packager.dev access yet, see Gem server authentication.
:::
Keep in mind that this will display the fieldβs resource as it appears in the edit view.
Enables this field as a nested form in the specified views.
##### Default value
`{}`
#### Possible values
A hash with the following options:
- `on:` Views in which to enable nesting. Accepted values:
- `:new` - Enables nesting in the new view.
- `:edit` - Enables nesting in the edit view.
- `:forms` - Enables nesting in the new and edit views.
- `limit:` *(Only for `has_many` and `has_and_belongs_to_many` fields)* Hides the "Add" button when the specified limit is reached.
:::tip
Setting `nested: true` is a shortcut for `nested: { on: :forms }`.
:::
#### Example
```ruby-vue{4,5,7,8,10,11,13-14,16-19}
# app/avo/resources/book.rb
class Avo::Resources::Book < Avo::BaseResource
def fields
# Shortcut for full nesting
field :{{ $frontmatter.field_type === 'has_one' ? 'author' : 'authors' }}, as: :{{ $frontmatter.field_type }}, nested: true
# Explicit nesting on new only
field :{{ $frontmatter.field_type === 'has_one' ? 'author' : 'authors' }}, as: :{{ $frontmatter.field_type }}, nested: { on: :new }
# Explicit nesting on edit only
field :{{ $frontmatter.field_type === 'has_one' ? 'author' : 'authors' }}, as: :{{ $frontmatter.field_type }}, nested: { on: :edit }
# Explicit nesting on both new and edit
field :{{ $frontmatter.field_type === 'has_one' ? 'author' : 'authors' }}, as: :{{ $frontmatter.field_type }}, nested: { on: :forms }
# Limit nested creation (for has_many or has_and_belongs_to_many only)
field :authors,
as: :{{ $frontmatter.field_type }},
nested: { on: [:new, :edit], limit: 2 }
end
end
```
---
# Has Many
By default, the `HasMany` field is visible only on the `Show` view. You will see a new panel with the model's associated records below the regular fields panel.
```ruby
field :projects, as: :has_many
```
## Options
Turns the attach field/modal from a `select` input to a searchable experience
```ruby{5}
class Avo::Resources::CourseLink < Avo::BaseResource
def fields
field :links,
as: :has_many,
searchable: true
end
end
```
:::warning
Avo uses the **resource search feature** behind the scenes, so **make sure the target resource has the `search_query` option configured**.
:::
```ruby{3-7}
# app/avo/resources/course_link.rb
class Avo::Resources::CourseLink < Avo::BaseResource
self.search = {
query: -> {
query.ransack(id_eq: q, link_cont: q, m: "or").result(distinct: false)
}
}
end
```
#### Default
`false`
#### Possible values
`true`, `false`
Scope out the records the user sees on the Attach modal.
#### Default
`nil`
#### Possible values
```ruby{3}
field :user,
as: :belongs_to,
attach_scope: -> { query.non_admins }
```
Pass in a block where you attach scopes to the `query` object and `parent` object, which is the actual record where you want to assign the association. The block is executed in the `ExecutionContext`.
:::warning
The `attach_scope` will not filter the records in the listing from `has_many` or `has_and_belongs_to_many` associations.
Use [`scope`](#scope) or a Pundit policy `Scope` for that.
:::
```ruby-vue{3}
field :members,
as: :{{ $frontmatter.field_type }},
attach_scope: -> { query.where.not(team_id: parent.id) }
```
In this example, in the `attach_scope`, we ensure that when attaching members to a team, only those who are not already members will appear in the list of options.
Scope out the records displayed in the table.
#### Default
`nil`
#### Possible values
```ruby{3}
field :user,
as: :belongs_to,
scope: -> { query.approved }
```
Pass in a block where you attach scopes to the `query` object. The block gets executed in the `ExecutionContext`.
With version 2.5.0, you'll also have access to the `parent` record so that you can use that to scope your associated models even better.
Starting with version 3.12, access to `resource` and `parent_resource` was additionally provided.
Changes the text displayed as association name.

#### Default
Plural association name.
#### Possible values
Any string or any zero arity lambda function.
Within lambda, you have access to all attributes of `Avo::ExecutionContext`.
Changes the text displayed under the association name.
#### Default
`nil`
#### Possible values
Any string or any zero arity lambda function.
Within lambda, you have access to `query` and all attributes of `Avo::ExecutionContext`.
Sets a different resource to be used when displaying (or redirecting to) the association table.
#### Default
`nil`. When nothing is selected, Avo infers the resource type from the reflected association.
#### Possible values
`Avo::Resources::Post`, `Avo::Resources::PhotoComment`, or any Avo resource class.
The value can be the actual class or a string representation of that class.
```ruby
# the class
Avo::Resources::Post
# the string representation of the class
"Avo::Resources::Post"
```
Hides the pagination details when only there's only one page for that association.
#### Default
`false`
#### Possible values
`true`, `false`
Hides the search input displayed on the association table.
#### Default
`false`. When nothing is selected and the `search_query` of association's resource is configured, Avo displays the search input.
#### Possible values
`true`, `false`.
Hides the filters button displayed on the association table.
#### Default
`false`. The filters button is displayed by default and only hidden when `hide_filter_button` is set to `true`
#### Possible values
`true`, `false`.
Sets which resource should be used in an STI scenario.
See more on this in the STI section.
#### Default
`false`. When it's `false` it will use the same resource.
#### Possible values
`true`, `false`.
## Search query scope
If the resource used for the `has_many` association has the `search` block configured with a `query`, Avo will use that to scope out the search query to that association.
For example, if you have a `Team` model that `has_many` `User`s, now you'll be able to search through that team's users instead of all of them.
You can target that search using `params[:via_association]`. When the value of `params[:via_association]` is `has_many`, the search has been mad inside a has_many association.
For example, if you want to show the records in a different order, you can do this:
```ruby
self.search = {
query: -> {
if params[:via_association] == 'has_many'
query.ransack(id_eq: q, m: "or").result(distinct: false).order(name: :asc)
else
query.ransack(id_eq: q, m: "or").result(distinct: false)
end
}
}
```
You can add use this option to make the association title clickable. That link will open a new page with the same view.
This feature doesn't go deeper than this. It just helps you see the association table easier in a separate page.
## Has Many Through
The `HasMany` association also supports the `:through` option.
```ruby{3}
field :members,
as: :has_many,
through: :memberships
```
If you have extra fields defined in the through table and would like to display them when attaching use the `attach_fields` option.
```ruby{4,5,6}
field :patrons,
as: :has_many,
through: :patronships,
attach_fields: -> {
field :review, as: :text
}
```
:::warning
If the through model uses **polymorphism**, the type must be included as a hidden field:
```ruby{6}
field :patrons,
as: :has_many,
through: :patronships,
attach_fields: -> {
field :review, as: :text
field :patronship_type, as: :hidden, default: "TheType"
}
```
:::
## Show on edit screens
By default, the `{{ $frontmatter.field_type }}` field is only visible in the show view. To make it available in the edit view as well, include the `show_on: :edit` option. This ensures that the `{{ $frontmatter.field_type }}` show view component is also rendered within the edit view.
## Nested in Forms
You can use ["Show on edit screens"](#show-on-edit-screens) to make the `{{ $frontmatter.field_type }}` field available in the edit view. However, this will render it using the show view component.
To enable nested creation for the `{{ $frontmatter.field_type }}` field, allowing it to be created and / or edited alongside its parent record within the same form, use the `nested` option which is a hash with configurable option.
:::info The `avo-nested` gem
Nested association forms are provided by the **`avo-nested`** gem. Add it to your `Gemfile` using the same private gem source as your other Avo paid gems:
```ruby
gem "avo-nested", source: "https://packager.dev/avo-hq/"
```
Run `bundle install`. If you have not set up packager.dev access yet, see Gem server authentication.
:::
Keep in mind that this will display the fieldβs resource as it appears in the edit view.
Enables this field as a nested form in the specified views.
##### Default value
`{}`
#### Possible values
A hash with the following options:
- `on:` Views in which to enable nesting. Accepted values:
- `:new` - Enables nesting in the new view.
- `:edit` - Enables nesting in the edit view.
- `:forms` - Enables nesting in the new and edit views.
- `limit:` *(Only for `has_many` and `has_and_belongs_to_many` fields)* Hides the "Add" button when the specified limit is reached.
:::tip
Setting `nested: true` is a shortcut for `nested: { on: :forms }`.
:::
#### Example
```ruby-vue{4,5,7,8,10,11,13-14,16-19}
# app/avo/resources/book.rb
class Avo::Resources::Book < Avo::BaseResource
def fields
# Shortcut for full nesting
field :{{ $frontmatter.field_type === 'has_one' ? 'author' : 'authors' }}, as: :{{ $frontmatter.field_type }}, nested: true
# Explicit nesting on new only
field :{{ $frontmatter.field_type === 'has_one' ? 'author' : 'authors' }}, as: :{{ $frontmatter.field_type }}, nested: { on: :new }
# Explicit nesting on edit only
field :{{ $frontmatter.field_type === 'has_one' ? 'author' : 'authors' }}, as: :{{ $frontmatter.field_type }}, nested: { on: :edit }
# Explicit nesting on both new and edit
field :{{ $frontmatter.field_type === 'has_one' ? 'author' : 'authors' }}, as: :{{ $frontmatter.field_type }}, nested: { on: :forms }
# Limit nested creation (for has_many or has_and_belongs_to_many only)
field :authors,
as: :{{ $frontmatter.field_type }},
nested: { on: [:new, :edit], limit: 2 }
end
end
```
## Add scopes to associations
When displaying `has_many` associations, you might want to scope out some associated records. For example, a user might have multiple comments, but on the user's `Show` page, you don't want to display all the comments, but only the approved ones.
```ruby{5,16,22}
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :user, optional: true
scope :approved, -> { where(approved: true) }
end
# app/models/user.rb
class User < ApplicationRecord
has_many :comments
end
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
field :comments, as: :has_many, scope: -> { query.approved }
end
end
```
The `comments` query on the user `Index` page will have the `approved` scope attached.
With version 2.5.0, you'll also have access to the `parent` record so that you can use that to scope your associated models even better.
Starting with version 3.12, access to `resource` and `parent_resource` was additionally provided.
All the `has_many` associations have the `attach_scope` option available too.
## Show/hide buttons
You will want to control the visibility of the attach/detach/create/destroy/actions buttons visible throughout your app. You can use the policy methods to do that.
Find out more on the authorization page.
The reloadable option adds a reload icon next to the association title so users can easily reload just that turbo-frame instead of doing a full page reload.
#### Usage
To enable the reloadable feature, you have two options:
1. Direct Boolean Value:
Provide a boolean value directly to the reloadable option. This sets a default behavior where the reloadable feature is either enabled or disabled based on this boolean value.
```ruby-vue
field :reviews, as: :{{ $frontmatter.field_type }}, reloadable: true
```
2. Dynamic Conditions with a Block:
For more dynamic behavior, you can provide a block to the reloadable option. Within this block, you can specify conditions under which the reloadable should be displayed.
```ruby-vue
field :reviews, as: :{{ $frontmatter.field_type }},
reloadable: -> {
current_user.is_admin?
}
```
In the above example, the reloadable will be visible if the current_user is an admin.
#### ExecutionContext
The reloadable block executes within the `ExecutionContext`, granting access to all default methods and attributes.
The `for_attribute` option allows to specify the association used for a certain field. This option make possible to define same association with different scopes and different name several times on the same resource.
#### Usage
```ruby-vue
field :reviews,
as: :{{ $frontmatter.field_type }}
field :special_reviews,
as: :{{ $frontmatter.field_type }},
for_attribute: :reviews,
scope: -> { query.special_reviews }
```
---
# Has And Belongs To Many
The `HasAndBelongsToMany` association works similarly to `HasMany`.
```ruby
field :users, as: :has_and_belongs_to_many
```
## Options
Turns the attach field/modal from a `select` input to a searchable experience
```ruby{5}
class Avo::Resources::CourseLink < Avo::BaseResource
def fields
field :links,
as: :has_many,
searchable: true
end
end
```
:::warning
Avo uses the **resource search feature** behind the scenes, so **make sure the target resource has the `search_query` option configured**.
:::
```ruby{3-7}
# app/avo/resources/course_link.rb
class Avo::Resources::CourseLink < Avo::BaseResource
self.search = {
query: -> {
query.ransack(id_eq: q, link_cont: q, m: "or").result(distinct: false)
}
}
end
```
#### Default
`false`
#### Possible values
`true`, `false`
Scope out the records the user sees on the Attach modal.
#### Default
`nil`
#### Possible values
```ruby{3}
field :user,
as: :belongs_to,
attach_scope: -> { query.non_admins }
```
Pass in a block where you attach scopes to the `query` object and `parent` object, which is the actual record where you want to assign the association. The block is executed in the `ExecutionContext`.
:::warning
The `attach_scope` will not filter the records in the listing from `has_many` or `has_and_belongs_to_many` associations.
Use [`scope`](#scope) or a Pundit policy `Scope` for that.
:::
```ruby-vue{3}
field :members,
as: :{{ $frontmatter.field_type }},
attach_scope: -> { query.where.not(team_id: parent.id) }
```
In this example, in the `attach_scope`, we ensure that when attaching members to a team, only those who are not already members will appear in the list of options.
Scope out the records displayed in the table.
#### Default
`nil`
#### Possible values
```ruby{3}
field :user,
as: :belongs_to,
scope: -> { query.approved }
```
Pass in a block where you attach scopes to the `query` object. The block gets executed in the `ExecutionContext`.
With version 2.5.0, you'll also have access to the `parent` record so that you can use that to scope your associated models even better.
Starting with version 3.12, access to `resource` and `parent_resource` was additionally provided.
Changes the text displayed as association name.

#### Default
Plural association name.
#### Possible values
Any string or any zero arity lambda function.
Within lambda, you have access to all attributes of `Avo::ExecutionContext`.
Changes the text displayed under the association name.
#### Default
`nil`
#### Possible values
Any string or any zero arity lambda function.
Within lambda, you have access to `query` and all attributes of `Avo::ExecutionContext`.
Sets a different resource to be used when displaying (or redirecting to) the association table.
#### Default
`nil`. When nothing is selected, Avo infers the resource type from the reflected association.
#### Possible values
`Avo::Resources::Post`, `Avo::Resources::PhotoComment`, or any Avo resource class.
The value can be the actual class or a string representation of that class.
```ruby
# the class
Avo::Resources::Post
# the string representation of the class
"Avo::Resources::Post"
```
Hides the pagination details when only there's only one page for that association.
#### Default
`false`
#### Possible values
`true`, `false`
Hides the search input displayed on the association table.
#### Default
`false`. When nothing is selected and the `search_query` of association's resource is configured, Avo displays the search input.
#### Possible values
`true`, `false`.
Hides the filters button displayed on the association table.
#### Default
`false`. The filters button is displayed by default and only hidden when `hide_filter_button` is set to `true`
#### Possible values
`true`, `false`.
## Search query scope
If the resource used for the `has_many` association has the `search` block configured with a `query`, Avo will use that to scope out the search query to that association.
For example, if you have a `Team` model that `has_many` `User`s, now you'll be able to search through that team's users instead of all of them.
You can target that search using `params[:via_association]`. When the value of `params[:via_association]` is `has_many`, the search has been mad inside a has_many association.
For example, if you want to show the records in a different order, you can do this:
```ruby
self.search = {
query: -> {
if params[:via_association] == 'has_many'
query.ransack(id_eq: q, m: "or").result(distinct: false).order(name: :asc)
else
query.ransack(id_eq: q, m: "or").result(distinct: false)
end
}
}
```
## Show on edit screens
By default, the `{{ $frontmatter.field_type }}` field is only visible in the show view. To make it available in the edit view as well, include the `show_on: :edit` option. This ensures that the `{{ $frontmatter.field_type }}` show view component is also rendered within the edit view.
## Nested in Forms
You can use ["Show on edit screens"](#show-on-edit-screens) to make the `{{ $frontmatter.field_type }}` field available in the edit view. However, this will render it using the show view component.
To enable nested creation for the `{{ $frontmatter.field_type }}` field, allowing it to be created and / or edited alongside its parent record within the same form, use the `nested` option which is a hash with configurable option.
:::info The `avo-nested` gem
Nested association forms are provided by the **`avo-nested`** gem. Add it to your `Gemfile` using the same private gem source as your other Avo paid gems:
```ruby
gem "avo-nested", source: "https://packager.dev/avo-hq/"
```
Run `bundle install`. If you have not set up packager.dev access yet, see Gem server authentication.
:::
Keep in mind that this will display the fieldβs resource as it appears in the edit view.
Enables this field as a nested form in the specified views.
##### Default value
`{}`
#### Possible values
A hash with the following options:
- `on:` Views in which to enable nesting. Accepted values:
- `:new` - Enables nesting in the new view.
- `:edit` - Enables nesting in the edit view.
- `:forms` - Enables nesting in the new and edit views.
- `limit:` *(Only for `has_many` and `has_and_belongs_to_many` fields)* Hides the "Add" button when the specified limit is reached.
:::tip
Setting `nested: true` is a shortcut for `nested: { on: :forms }`.
:::
#### Example
```ruby-vue{4,5,7,8,10,11,13-14,16-19}
# app/avo/resources/book.rb
class Avo::Resources::Book < Avo::BaseResource
def fields
# Shortcut for full nesting
field :{{ $frontmatter.field_type === 'has_one' ? 'author' : 'authors' }}, as: :{{ $frontmatter.field_type }}, nested: true
# Explicit nesting on new only
field :{{ $frontmatter.field_type === 'has_one' ? 'author' : 'authors' }}, as: :{{ $frontmatter.field_type }}, nested: { on: :new }
# Explicit nesting on edit only
field :{{ $frontmatter.field_type === 'has_one' ? 'author' : 'authors' }}, as: :{{ $frontmatter.field_type }}, nested: { on: :edit }
# Explicit nesting on both new and edit
field :{{ $frontmatter.field_type === 'has_one' ? 'author' : 'authors' }}, as: :{{ $frontmatter.field_type }}, nested: { on: :forms }
# Limit nested creation (for has_many or has_and_belongs_to_many only)
field :authors,
as: :{{ $frontmatter.field_type }},
nested: { on: [:new, :edit], limit: 2 }
end
end
```
### Searchable `has_and_belongs_to_many`
Similar to `belongs_to`, the `has_many` associations support the `searchable` option.
## Add scopes to associations
When displaying `has_many` associations, you might want to scope out some associated records. For example, a user might have multiple comments, but on the user's `Show` page, you don't want to display all the comments, but only the approved ones.
```ruby{5,16,22}
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :user, optional: true
scope :approved, -> { where(approved: true) }
end
# app/models/user.rb
class User < ApplicationRecord
has_many :comments
end
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
field :comments, as: :has_many, scope: -> { query.approved }
end
end
```
The `comments` query on the user `Index` page will have the `approved` scope attached.
With version 2.5.0, you'll also have access to the `parent` record so that you can use that to scope your associated models even better.
Starting with version 3.12, access to `resource` and `parent_resource` was additionally provided.
All the `has_many` associations have the `attach_scope` option available too.
## Show/hide buttons
You will want to control the visibility of the attach/detach/create/destroy/actions buttons visible throughout your app. You can use the policy methods to do that.
Find out more on the authorization page.
The reloadable option adds a reload icon next to the association title so users can easily reload just that turbo-frame instead of doing a full page reload.
#### Usage
To enable the reloadable feature, you have two options:
1. Direct Boolean Value:
Provide a boolean value directly to the reloadable option. This sets a default behavior where the reloadable feature is either enabled or disabled based on this boolean value.
```ruby-vue
field :reviews, as: :{{ $frontmatter.field_type }}, reloadable: true
```
2. Dynamic Conditions with a Block:
For more dynamic behavior, you can provide a block to the reloadable option. Within this block, you can specify conditions under which the reloadable should be displayed.
```ruby-vue
field :reviews, as: :{{ $frontmatter.field_type }},
reloadable: -> {
current_user.is_admin?
}
```
In the above example, the reloadable will be visible if the current_user is an admin.
#### ExecutionContext
The reloadable block executes within the `ExecutionContext`, granting access to all default methods and attributes.
The `for_attribute` option allows to specify the association used for a certain field. This option make possible to define same association with different scopes and different name several times on the same resource.
#### Usage
```ruby-vue
field :reviews,
as: :{{ $frontmatter.field_type }}
field :special_reviews,
as: :{{ $frontmatter.field_type }},
for_attribute: :reviews,
scope: -> { query.special_reviews }
```
---
# Actions Overview
Actions in Avo are powerful tools that transform the way you interact with your data. They enable you to perform operations on one or multiple records simultaneously, extending your interface with custom functionality that goes beyond basic CRUD operations.
## What Are Actions?
Think of Actions as custom operations you can trigger from your admin interface. They're like specialized commands that can:
- Process single records or work in batch mode
- Collect additional information through customizable forms
- Trigger background jobs
- Generate reports or export data
- Modify record states
- Send notifications
- And much more...
## Key Benefits
### 1. Streamlined Workflows
Instead of building custom interfaces for common operations, Actions provide a standardized way to perform complex tasks right from your admin panel.
### 2. Flexibility
Actions can be as simple or as complex as you need:
- Simple toggles for changing record states
- Multi-step processes with user input on each step
- Background job triggers for heavy operations
- API integrations with external services
### 3. Batch Operations
Save time by performing operations on multiple records at once. Whether you're updating statuses, sending notifications, or processing data, batch actions have you covered.
### 4. User Input Forms
When additional information is needed, Actions can present custom forms to collect data before execution. These forms are fully customizable and support various field types.
## Common Use Cases
- **User Management**: Activate/deactivate accounts, reset passwords, or send welcome emails
- **Content Moderation**: Approve/reject content, flag items for review
- **Data Processing**: Generate reports, export data, or trigger data transformations
- **Communication**: Send notifications, emails, or SMS messages
- **State Management**: Change status, toggle features, or update permissions
- **Batch Updates**: Modify multiple records with consistent changes
- **Integration Triggers**: Connect with external APIs or services
Common use cases include managing user states, sending notifications, and automating data processing. Their flexibility makes them essential for building robust interfaces, streamlining workflows, and managing data efficiently.
---
# Action Generator
Avo provides a powerful Rails generator to create action files quickly and efficiently.
## Basic Generator Usage
Generate a new action file using the Rails generator:
```bash
bin/rails generate avo:action toggle_inactive
```
This command creates a new action file at `app/avo/actions/toggle_inactive.rb` with the following structure:
```ruby
# app/avo/actions/toggle_inactive.rb
class Avo::Actions::ToggleInactive < Avo::BaseAction
self.name = "Toggle Inactive"
# self.visible = -> do
# true
# end
# def fields
# # Add Action fields here
# end
def handle(query:, fields:, current_user:, resource:, **args)
query.each do |record|
# Do something with your records.
end
end
end
```
## Generator Options
### `--standalone`
By default, actions require at least one record to be selected before they can be triggered, unless specifically configured as standalone actions.
The `--standalone` option creates an action that doesn't require record selection. This is particularly useful for:
- Generating reports
- Exporting all records
- Running global operations
```bash
bin/rails generate avo:action export_users --standalone
```
You can also make an existing action standalone by manually setting `self.standalone = true` in the action class:
```ruby{5}
# app/avo/actions/export_users.rb
class Avo::Actions::ExportUsers < Avo::BaseAction
self.name = "Export Users"
self.standalone = true
# ... rest of the action code
end
```
## Best Practices
When generating actions, consider the following:
1. Use descriptive names that reflect the action's purpose (e.g., `toggle_published`, `send_newsletter`, `archive_records`)
2. Follow Ruby naming conventions (snake_case for file names)
3. Group related actions in namespaces using subdirectories
4. Use the `--standalone` flag when the action doesn't operate on specific records
## Examples
```bash
# Generate a regular action
bin/rails generate avo:action mark_as_featured
# Generate a standalone action
bin/rails generate avo:action generate_monthly_report --standalone
# Generate an action in a namespace
bin/rails generate avo:action admin/approve_user
```
---
# Registration
Actions are registered within a resource by using the resource's `actions` method. This method defines which actions are available for that specific resource.
## `action`
The `action` method is used to register an action within the `actions` block. It accepts the action class as its first argument and optional configuration parameters like `arguments` and `icon`
```ruby{5}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def actions
# Basic registration
action Avo::Actions::ToggleInactive
end
end
```
:::warning
Using the Pundit policies, you can restrict access to actions using the `act_on?` method. If you think you should see an action on a resource and you don't, please check the policy method.
More info here
:::
Once attached, the action will appear in the **Actions** dropdown menu. By default, actions are available on all views.
:::info
You may use the customizable controls feature to show the actions outside the dropdown.
:::
The `arguments` option allows you to pass custom data to your action. These arguments are accessible throughout the entire action class including the `handle` and `fields` methods.
```ruby{5-7,11-15}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def actions
action Avo::Actions::ToggleInactive,
arguments: {
special_message: true
}
# Or as a proc to make it dynamic
action Avo::Actions::ToggleInactive,
arguments: -> do
{
special_message: resource.view.index? && current_user.is_admin?
}
end
end
end
```
Now, the arguments can be accessed all over the action class like inside `handle` and `fields` methods.
```ruby{4-8}
# app/avo/actions/toggle_inactive.rb
class Avo::Actions::ToggleInactive < Avo::BaseAction
def handle(**args)
if arguments[:special_message]
succeed "I love π₯"
else
succeed "Success response βοΈ"
end
end
end
```
The `icon` option lets you specify the icon to display next to the action in the dropdown menu. Avo supports [Heroicons](https://heroicons.com) by default.
Here's an example of how you can define actions with icons:
```ruby{4}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def actions
action Avo::Actions::ToggleInactive, icon: "heroicons/outline/globe"
end
end
```
---
## `divider`
Action dividers allow you to organize and separate actions into logical groups, improving the overall layout and usability.
This will create a visual separator in the actions dropdown menu, helping you group related actions together.
```ruby{8}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def actions
# User status actions
action Avo::Actions::ActivateUser
action Avo::Actions::DeactivateUser
divider
# Communication actions
action Avo::Actions::SendWelcomeEmail
action Avo::Actions::SendPasswordReset
end
end
```
You can also add a label to the divider for better organization:
```ruby{5}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def actions
action Avo::Actions::ActivateUser
divider label: "Communication"
action Avo::Actions::SendWelcomeEmail
end
end
```
---
# Execution flow
When a user triggers an action in Avo, the following flow occurs:
1. Record selection phase:
- This phase can be bypassed by setting `self.standalone = true`
- For bulk actions on the index page, Avo collects all the records selected by the user
- For actions on the show page or row controls, Avo uses that record as the target of the action
2. The action is initiated by the user through the index page (bulk actions), show page (single record actions), or resource controls (custom action buttons)
3. Form display phase (optional):
- This phase can be bypassed by setting `self.confirmation = false`
- By default, a modal is displayed where the user can confirm or cancel the action
- If the action has defined fields, they will be shown in the modal for the user to fill out
- The user can then choose to run the action or cancel it
- If the user cancels, the execution stops here
4. Action execution:
- The `handle` method processes selected records, form values, current user, and resource details
- Your custom business logic is executed within the `handle` method
- User feedback is configured ([`succeed`](#succeed), [`warn`](#warn), [`inform`](#inform), [`error`](#error), or [`silent`](#silent))
- Response type is configured ([`redirect_to`](#redirect_to), [`reload`](#reload), [`keep_modal_open`](#keep_modal_open), and [more](#response-types))
## Fields
An action can define fields, which will be shown to the user in the action's modal. Fields on an action work the same way as on resources. Whenever an action runs on a single record, those fields are tied to that record. Otherwise, they're just form inputs that are passed to the action's `handle` method.
Check out the Fields page for more information.
## The `handle` method
The `handle` method is where you define what happens when your action is executed. This is the core of your action's business logic and receives the following arguments:
- `query` Contains the selected record(s). Single records are automatically wrapped in an array for consistency
- `fields` Contains the values submitted through the action's form fields
- `current_user` The currently authenticated user
- `resource` The Avo resource instance that triggered the action
```ruby{10-23}
# app/avo/actions/toggle_inactive.rb
class Avo::Actions::ToggleInactive < Avo::BaseAction
self.name = "Toggle Inactive"
def fields
field :notify_user, as: :boolean
field :message, as: :textarea
end
def handle(query:, fields:, current_user:, resource:, **args)
query.each do |record|
# Toggle the inactive status
record.update!(inactive: !record.inactive)
# Send notification if requested
if fields[:notify_user]
# Assuming there's a notify method
record.notify(fields[:message])
end
end
succeed "Successfully toggled status for #{query.count}"
end
end
```
## Feedback notifications
After an action runs, you can respond to the user with different types of notifications or no feedback at all. The default feedback is an `Action ran successfully` message of type `inform`.
All feedback notification methods (`succeed`, `warn`, `inform`, `error`) support an optional `timeout` parameter to control how long the notification remains visible:
```ruby
# Display notification for 5 seconds
succeed 'Task completed successfully', timeout: 5000
# Keep notification open indefinitely, until the user dismisses it
warn 'Important warning - requires attention', timeout: :forever
# Use default timeout (falls back to global configuration)
inform 'Action completed'
```
:::info
Set the `timeout` to `:forever` to keep the notification open indefinitely until the user dismisses it.
The default timeout is set to `config.alert_dismiss_time` in the Avo configuration.
:::
Displays a **green** success alert to indicate successful completion.
Displays an **orange** warning alert for cautionary messages.
Displays a **blue** info alert for general information.
Displays a **red** error alert to indicate failure or errors.
You may want to run an action and show no notification when it's done. That is useful for redirect scenarios. You can use the `silent` response for that.
```ruby{5}
# app/avo/actions/toggle_inactive.rb
class Avo::Actions::ToggleInactive < Avo::BaseAction
def handle(**args)
redirect_to "/admin/some-tool"
silent
end
end
```
:::info
You can show multiple notifications at once by calling multiple feedback methods (`succeed`, `warn`, `inform`, `error`) in your action's `handle` method. Each notification will be displayed in sequence.
:::
```ruby{4-7}
# app/avo/actions/toggle_inactive.rb
class Avo::Actions::ToggleInactive < Avo::BaseAction
def handle(**args)
succeed "Success response βοΈ"
warn "Warning response βοΈ"
inform "Info response βοΈ"
error "Error response βοΈ"
end
end
```
## Response types
After an action completes, you can control how the UI responds through various response types. These powerful responses give you fine-grained control over the user experience by allowing you to:
- **Navigate**: Reload pages or redirect users to different parts of your application
- **Manipulate UI**: Control modals, update specific page elements, or refresh table rows
- **Handle Files**: Trigger file downloads and handle data exports
- **Show Feedback**: Combine with notification messages for clear user communication
You can use these responses individually or combine them to create sophisticated interaction flows. Here are all the available action responses:
The `reload` response triggers a full-page reload. This is the default behavior if no other response type is specified.
```ruby{9}
def handle(query:, **args)
query.each do |project|
project.update active: false
end
succeed 'Done!'
reload # This is optional since reload is the default behavior
end
```
`redirect_to` will execute a redirect to a new path of your app. It accept `allow_other_host`, `status` and any other arguments.
Example:
`redirect_to path, allow_other_host: true, status: 303`
```ruby{9}
def handle(query:, **args)
query.each do |project|
project.update active: false
end
succeed 'Done!'
redirect_to avo.resources_users_path
end
```
`download` will start a file download to your specified `path` and `filename`.
:::warning
**Ignore this warning if you are using Avo 3.2.2 or later.**
You need to set `self.may_download_file` to true for the download response to work like below.
:::
:::code-group
```ruby{3-4,17} [app/avo/actions/download_file.rb]
class Avo::Actions::DownloadFile < Avo::BaseAction
self.name = "Download file"
# Only required for versions before 3.2.2
self.may_download_file = true
def handle(query:, **args)
filename = "projects.csv"
report_data = []
query.each do |project|
report_data << project.generate_report_data
end
succeed 'Done!'
if report_data.present? and filename.present?
download report_data, filename
end
end
end
```
```ruby{8} [app/avo/resources/project.rb]
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
def fields
# fields here
end
def actions
action Avo::Actions::DownloadFile
end
end
```
:::
There might be situations where you want to run an action and if it fails, respond back to the user with some feedback but still keep it open with the inputs filled in.
`keep_modal_open` will tell Avo to keep the modal open.
```ruby
class Avo::Actions::KeepModalOpenAction < Avo::BaseAction
self.name = "Keep Modal Open"
self.standalone = true
def fields
field :name, as: :text
field :birthday, as: :date
end
def handle(fields:, **args)
User.create fields
succeed "All good βοΈ"
rescue => error
error "Something happened: #{error.message}"
keep_modal_open
end
end
```
This type of response becomes useful when you are working with a form and need to execute an action without redirecting, ensuring that the form remains filled as it is.
`close_modal` will flash all the messages gathered by [action responses](#action-responses) and will close the modal using turbo streams keeping the page still.
```ruby{7,9}
class Avo::Actions::CloseModal < Avo::BaseAction
self.name = "Close modal"
def handle(**args)
# do_something_here
succeed "Modal closed!!"
close_modal
# or
do_nothing
end
end
```
`do_nothing` is an alias for `close_modal`.
```ruby{7}
class Avo::Actions::CloseModal < Avo::BaseAction
self.name = "Close modal"
def handle(**args)
# do_something_here
succeed "Modal closed!!"
do_nothing
end
end
```
You may want to redirect to another action. Here's an example of how to create a multi-step process, passing arguments from one action to another.
In this example the initial action prompts the user to select the fields they wish to update, and in the subsequent action, the chosen fields will be accessible for updating.
:::code-group
```ruby[PreUpdate]
class Avo::Actions::City::PreUpdate < Avo::BaseAction
self.name = "Update"
def fields
field :name, as: :boolean
field :population, as: :boolean
end
def handle(query:, fields:, **args)
navigate_to_action Avo::Actions::City::Update,
arguments: {
cities: query.map(&:id),
render_name: fields[:name],
render_population: fields[:population]
}
end
end
```
```ruby[Update]
class Avo::Actions::City::Update < Avo::BaseAction
self.name = "Update"
self.visible = -> { false }
def fields
field :name, as: :text if arguments[:render_name]
field :population, as: :number if arguments[:render_population]
end
def handle(fields:, **args)
City.find(arguments[:cities]).each do |city|
city.update! fields
end
succeed "City updated!"
end
end
```
:::
You can see this multi-step process in action by visiting the [avodemo](https://main.avodemo.com/avo/resources/cities). Select one of the records, click on the "Update" action, choose the fields to update, and then proceed to update the selected fields in the subsequent action.
Avo action responses are in the `turbo_stream` format. You can use the `append_to_response` method to append additional turbo stream responses to the default response.
```ruby{5-7}
def handle(**args)
succeed "Modal closed!!"
close_modal
append_to_response -> {
turbo_stream.set_title("Cool title ;)")
}
end
```
The `append_to_response` method accepts a Proc or lambda function. This function is executed within the context of the action's controller response.
The block should return either a single `turbo_stream` response or an array of multiple `turbo_stream` responses.
:::code-group
```ruby[Array]{2-5}
append_to_response -> {
[
turbo_stream.set_title("Cool title"),
turbo_stream.set_title("Cool title 2")
]
}
```
```ruby[Single]{2}
append_to_response -> {
turbo_stream.set_title("Cool title")
}
```
:::
:::warning
This option **only** works on **Index** pages, **NOT** on **associations**.
:::
This option leverages Turbo Stream to refresh specific table rows and grid view cards in response to an action. For individual records, you can use the `reload_record` alias method.
```ruby{8}
def handle(query:, fields:, **args)
query.each do |record|
record.update! active: !record.active
record.notify fields[:message] if fields[:notify_user]
end
reload_records(query)
end
```
The `reload_records` and `reload_record` methods are aliases, and they accept either an array of records or a single record.
:::code-group
```ruby[Array]{1}
reload_records([record_1, record_2])
```
```ruby[Single]{1}
reload_record(record)
```
:::
---
# Customization
Actions can be customized in several ways to enhance the user experience. You can modify the action's display name, confirmation message, button labels, and confirmation behavior between other things.
There are 2 types of customization, visual and behavioral.
## Visual customization
Visual customization is the process of modifying the action's appearance. This includes changing the action's name, message and button labels.
All visual customization options can be set as a string or a block.
The blocks are executed using `Avo::ExecutionContext`. Within these blocks, you gain access to:
- All attributes of `Avo::ExecutionContext`
- `resource` - The current resource instance
- `record` - The current record
- `view` - The current view
- `arguments` - Any passed arguments
- `query` - The current query parameters
The `name` option is used to change the action's display name.
```ruby{3,5-8}
# app/avo/actions/release_fish.rb
class Avo::Actions::ReleaseFish < Avo::BaseAction
self.name = "Release fish"
# Or as a block
self.name = -> {
record.present? ? "Release #{record.name}?" : "Release fish"
}
end
```
The `description` option adds a short description displayed below the action's name in the modal header. This helps users understand what the action does before confirming.
```ruby{3,5-8}
# app/avo/actions/release_fish.rb
class Avo::Actions::ReleaseFish < Avo::BaseAction
self.description = "Release the fish back into the ocean"
# Or as a block
self.description = -> {
record.present? ? "Release #{record.name} back into the ocean" : "Release fish back into the ocean"
}
end
```
The `message` option is used to change the action's confirmation message.
```ruby{3,5-12}
# app/avo/actions/release_fish.rb
class Avo::Actions::ReleaseFish < Avo::BaseAction
self.message = "Are you sure you want to release the fish?"
# Or as a block
self.message = -> {
if resource.record.present?
"Are you sure you want to release the #{resource.record.name}?"
else
"Are you sure you want to release the fish?"
end
}
end
```
The `confirm_button_label` option is used to change the action's confirmation button label.
```ruby{3,5-12}
# app/avo/actions/release_fish.rb
class Avo::Actions::ReleaseFish < Avo::BaseAction
self.confirm_button_label = "Release fish"
# Or as a block
self.confirm_button_label = -> {
if resource.record.present?
"Release #{resource.record.name}"
else
"Release fish"
end
}
end
```
The `cancel_button_label` option is used to change the action's cancel button label.
```ruby{3,5-12}
# app/avo/actions/release_fish.rb
class Avo::Actions::ReleaseFish < Avo::BaseAction
self.cancel_button_label = "Cancel release"
# Or as a block
self.cancel_button_label = -> {
if resource.record.present?
"Cancel release on #{resource.record.name}"
else
"Cancel release"
end
}
end
```
## Behavioral customization
Behavioral customization is the process of modifying the action's behavior. This includes changing the action's confirmation behavior and authorization.
By default, actions display a confirmation modal before execution. You can bypass this modal by setting `self.confirmation = false`, which will execute the action immediately upon triggering.
```ruby{3}
# app/avo/actions/release_fish.rb
class Avo::Actions::ReleaseFish < Avo::BaseAction
self.confirmation = false
end
```
This is particularly useful for actions that:
- Are safe to execute without confirmation
- Need to provide immediate feedback
- Are part of a multi-step workflow where confirmation is handled elsewhere
Standalone actions allow you to execute operations that aren't tied to specific model records. These are useful for global operations like:
- Generating system-wide reports
- Running maintenance tasks
- Triggering background jobs
You can create a standalone action in two ways:
1. Using the generator with the `--standalone` flag:
```bash
bin/rails generate avo:action global_action --standalone
```
2. Adding `self.standalone = true` to an existing action:
```ruby{4}
# app/avo/actions/global_report.rb
class Avo::Actions::GlobalReport < Avo::BaseAction
self.name = "Generate Global Report"
self.standalone = true
end
```
Standalone actions will be active in the Actions dropdown even when no records are selected. They can be used alongside regular record-based actions in the same resource.
:::tip
Standalone actions work well with the [`fields`](#fields) feature to collect additional input needed for the operation.
:::
You may want to hide specific actions on some views, like a standalone action on the `Show` and `Edit` views, and show it only on the `Index` view. You can do that using the `self.visible` attribute.
```ruby{5,8}
# app/avo/actions/global_report.rb
class Avo::Actions::GlobalReport < Avo::BaseAction
self.name = "Generate Global Report"
self.standalone = true
self.visible = true
# Or as a block
self.visible = -> { view.index? }
end
```
The `visible` attribute accepts a boolean or a block.
The block will be executed within the `Avo::ExecutionContext` environment, giving you access to important contextual attributes like:
- `view` - The current view type (index, show, edit)
- `resource` - The current resource instance
- `parent_resource` - The parent resource (if applicable).
- You can access the `parent_record` by `parent_resource.record`
- Plus all other `Avo::ExecutionContext` default attributes
The `authorize` attribute is used to restrict access to actions based on custom logic.
If an action is unauthorized, it will be hidden. If a bad actor attempts to proceed with the action, the controller will re-evaluate the authorization and block unauthorized requests.
```ruby{2,4-7}
class Avo::Actions::GlobalReport < Avo::BaseAction
self.authorize = false
# Or as a block
self.authorize = -> {
current_user.is_admin?
}
end
```
The `authorize` attribute accepts a boolean or a proc.
The block will be executed within the `Avo::ExecutionContext` environment, giving you access to important contextual attributes like:
- `action` - The current action instance
- `resource` - The current resource instance
- `view` - The current view type (index, show, edit)
- All other `Avo::ExecutionContext` attributes
By default, action modals use a dynamic backdrop.
Add `self.close_modal_on_backdrop_click = false` in case you want to prevent the user from closing the modal when clicking on the backdrop.
```ruby{3}
# app/avo/actions/toggle_inactive.rb
class Avo::Actions::ToggleInactive < Avo::BaseAction
self.close_modal_on_backdrop_click = false
end
```
The `turbo` attribute is used to control the Turbo behavior of actions.
There are times when you don't want to perform the actions with Turbo. In such cases, turbo should be set to false.
```ruby{3}
# app/avo/actions/toggle_inactive.rb
class Avo::Actions::ToggleInactive < Avo::BaseAction
self.turbo = false
end
```
The `turbo` attribute accepts a boolean.
---
# WIP
this section is under construction
## Helpers
### `link_arguments`
The `link_arguments` method is used to generate the arguments for an action link.
You may want to dynamically generate an action link. For that you need the action class and a resource instance (with or without record hydrated). Call the action's class method `link_arguments` with the resource instance as argument and it will return the `[path, data]` that are necessary to create a proper link to a resource.
Let's see an example use case:
```ruby{4-,16} [Current Version]
# app/avo/resources/city.rb
class Avo::Resources::City < Avo::BaseResource
field :name, as: :text, name: "Name (click to edit)", only_on: :index do
path, data = Avo::Actions::City::Update.link_arguments(
resource: resource,
arguments: {
cities: Array[resource.record.id],
render_name: true
}
)
link_to resource.record.name, path, data: data
end
end
```
:::tip
#### Generate an Action Link Without a Resource Instance
Sometimes, you may need to generate an action link without having access to an instantiated resource.
#### Scenario
Imagine you want to trigger an action from a custom partial card on a dashboard, but there is no resource instance available.
#### Solution
In this case, you can create a new resource instance (with or without record) and use it as follows:
```ruby
path, data = Avo::Actions::City::Update.link_arguments(
resource: Avo::Resources::City.new(record: city)
)
link_to "Update city", path, data: data
```
:::
## Guides
### StimulusJS
Please follow our extended StimulusJS guides for more information.
### Passing Params to the Action Show Page
When navigation to an action from a resource or views, it's sometimes useful to pass parameters to an action.
One particular example is when you'd like to populate a field in that action with some particular value based on that param.
```ruby
class Action
def fields
field :some_field, as: :hidden, default: -> { if previous_param == yes ? :yes : :no}
end
end
```
Consider the following scenario:
1. Navigate to `https://main.avodemo.com/avo/resources/users`.
2. Add the parameter `hey=ya` to the URL: `https://main.avodemo.com/avo/resources/users?hey=ya`
3. Attempt to run the dummy action.
4. After triggering the action, verify that you can access the `hey` parameter.
5. Ensure that the retrieved value of the `hey` parameter is `ya`.
**Implementation**
To achieve this, we'll reference the `request.referer` object and extract parameters from the URL. Here is how to do it:
```ruby
class Action
def fields
# Accessing the parameters passed from the parent view
field :some_field, as: :hidden, default: -> {
# Parsing the request referer to extract parameters
parent_params = URI.parse(request.referer).query.split("&").map { |param| param.split("=")}.to_h.with_indifferent_access
# Checking if the `hei` parameter equals `ya`
if parent_params[:hey] == 'ya'
:yes
else
:no
end
}
end
end
```
Parse the `request.referer` to extract parameters using `URI.parse`.
Split the query string into key-value pairs and convert it into a hash.
Check if the `hey` parameter equals `ya`, and set the default value of `some_field` accordingly.
---
# Filters
Most content management systems need a way to filter the data.
Avo provides two types of filters you can use when building your app.
1. Basic filters
2. Dynamic filters
## Differences
### 1. Basic filters
- configured as one filter per file
- there are four types of filters (Text, Boolean, Select, Multiple select)
- they are more configurable
- you can scope out the information better
- you can use outside APIs or configurations
- you must add and configure each filter for a resource
### 2. Dynamic filters
- easier to set up. They only require one option on the field
- the user can choose the condition on which they filter the records
- a lot more conditions than basic filters
- the user can add multiple conditions per attribute
- they are more composable
---
# Filters
Filters allow you to better scope the index queries for records you are looking for.
Each filter is configured in a class with a few dedicated [methods and options](#filter-options). To use a filter on a resource you must [register it](#register-filters) and it will be displayed on the view.
## Filter options
`self.name` is what is going to be displayed to the user in the filters panel.
```ruby
self.name = "User names filter"
```
```ruby
self.name = -> { I18n.t("avo.filter.name") }
```
Within this block, you gain access to all attributes of `Avo::ExecutionContext` along with the `arguments`.
The value of `self.button_label` is the label displayed on the button that applies the filter.
```ruby
self.button_label = "Filter by user names"
```
```ruby
self.button_label = -> { I18n.t("avo.filter.button_label") }
```
Within this block, you gain access to all attributes of `Avo::ExecutionContext` along with the `arguments`.
You may want to show/hide the filter in some scenarios. You can do that using the `self.visible` attribute.
Inside the visible block you can acces the following variables and you should return a boolean (`true`/`false`).
```ruby
self.visible = -> do
# You have access to:
# block
# context
# current_user
# params
# parent_model
# parent_resource
# resource
# view
# view_context
true
end
```
There might be times when you will want to show a message to the user when you're not returning any options.
More on this in the [Empty message guide](#empty-message-text).
Some filters allow you to pass options to the user. For example on the [select filter](#select_filter) you can set the options in the dropdown, and on the [boolean filter](#boolean_filter) you may set the checkbox values.
Each filter type has their own `options` configuration explained below.
In the `options` method you have access to the `request`, `params`, `context`, `view_context`, and `current_user` objects.
The `apply` method is what is going to be run when Avo fetches the records on the view.
It recieves the `request` form which you can get all the `params` if you need them, it gets the `query` which is the query Avo made to fetch the records. It's a regular [Active Record](https://guides.rubyonrails.org/active_record_querying.html) which you can manipulate.
It also receives the `values` variable which holds the actual choices the user made on the front-end for the [options](#options) you set.
You may set default values for the `options` you set. For example you may set which option to be selected for the [select filter](#select_filter) and which checkboxes to be set for the [boolean filter](#boolean_filter).
In the `default` method you have access to the `request`, `params`, `context`, `view_context`, and `current_user` objects.
This is a hook in which you can change the value of the filter based on what other filters have for values.
More on this in the [React to filters guide](#react-to-filters)
## Register filters
In order to use a filter you must register it on a `Resource` using the `filter` method inside the `filters` method.
```ruby{9}
class Avo::Resources::Post < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id
end
def filters
filter Avo::Filters::Published
end
end
```
## Filter types
Avo has several types of filters available [Boolean filter](#Boolean%20Filter), [Select filter](#Select%20Filter), [Multiple select filter](#Multiple%20select%20filter), [Text filter](#Text%20Filter) and since version [Date time filter](#Date%20time%20Filter).
### Filter values
Because the filters get serialized back and forth, the final `value`/`values` in the `apply` method will be stringified or have the stringified keys if they are hashes. You can declare them as regular hashes in the `options` method, but they will get stringified.
The boolean filter is a filter where the user can filter the records using one or more checkboxes.
To generate one run:
```bash
bin/rails generate avo:filter featured
```
or
```bash
bin/rails generate avo:filter featured --type boolean
```
Here's a sample filter
```ruby
class Avo::Filters::Featured < Avo::Filters::BooleanFilter
self.name = 'Featured filter'
# `values` comes as a hash with stringified keys
# Eg:
# {
# 'is_featured': true
# }
def apply(request, query, values)
return query if values['is_featured'] && values['is_unfeatured']
if values['is_featured']
query = query.where(is_featured: true)
elsif values['is_unfeatured']
query = query.where(is_featured: false)
end
query
end
def options
{
is_featured: "Featured",
is_unfeatured: "Unfeatured"
}
end
# Optional method to set the default state.
# def default
# {
# is_featured: true
# }
# end
end
```
Each filter file comes with a `name`, `apply`, and `options` methods.
The `name` method lets you set the name of the filter.
The `apply` method is responsible for filtering out the records by giving you access to modify the `query` object. The `apply` method also gives you access to the current `request` object and the passed `values`. The `values` object is a `Hash` containing all the configured `options` with the option name as the key and `true`/`false` as the value.
```ruby
# Example values payload
{
'is_featured': true,
'is_unfeatured': false,
}
```
The `options` method defines the available values of your filter. They should return a `Hash` with the option id as a key and option label as value.
### Default value
You can set a default value to the filter, so it has a predetermined state on load. To do that, return the state you desire from the `default` method.
```ruby{23-27}
class Avo::Filters::Featured < Avo::Filters::BooleanFilter
self.name = 'Featured status'
def apply(request, query, values)
return query if values['is_featured'] && values['is_unfeatured']
if values['is_featured']
query = query.where(is_featured: true)
elsif values['is_unfeatured']
query = query.where(is_featured: false)
end
query
end
def options
{
is_featured: "Featured",
is_unfeatured: "Unfeatured"
}
end
def default
{
is_featured: true
}
end
end
```
Select filters are similar to Boolean ones but they give the user a dropdown with which to filter the values.
```bash
rails generate avo:filter published --type select
```
The most significant difference from the **Boolean filter** is in the `apply` method. You only get back one `value` attribute, which represents which entry from the `options` method is selected.
A finished, select filter might look like this.
```ruby
class Avo::Filters::Published < Avo::Filters::SelectFilter
self.name = 'Published status'
# `value` comes as a string
# Eg: 'published'
def apply(request, query, value)
case value
when 'published'
query.where.not(published_at: nil)
when 'unpublished'
query.where(published_at: nil)
else
query
end
end
def options
{
published: "Published",
unpublished: "Unpublished"
}
end
# Optional method to set the default state.
# def default
# :published
# end
end
```
### Default value
The select filter supports setting a default too. That should be a string or symbol with the select item. It will be stringified by Avo automatically.
```ruby{22-24}
class Avo::Filters::Published < Avo::Filters::SelectFilter
self.name = 'Published status'
def apply(request, query, value)
case value
when 'published'
query.where.not(published_at: nil)
when 'unpublished'
query.where(published_at: nil)
else
query
end
end
def options
{
'published': 'Published',
'unpublished': 'Unpublished',
}
end
def default
:published
end
end
```
You may also use a multiple select filter.
```bash
rails generate avo:filter post_status --type multiple_select
```
```ruby
class Avo::Filters::PostStatus < Avo::Filters::MultipleSelectFilter
self.name = "Status"
# `value` comes as an array of strings
# Ex: ['admins', 'non_admins']
def apply(request, query, value)
if value.include? 'admins'
query = query.admins
end
if value.include? 'non_admins'
query = query.non_admins
end
query
end
def options
{
admins: "Admins",
non_admins: "Non admins",
}
end
# Optional method to set the default state.
# def default
# ['admins', 'non_admins']
# end
end
```
### Dynamic options
The select filter can also take dynamic options:
```ruby{15-17}
class Avo::Filters::Author < Avo::Filters::SelectFilter
self.name = 'Author'
def apply(request, query, value)
query = query.where(author_id: value) if value.present?
query
end
# Example `applied_filters`
# applied_filters = {
# "Avo::Filters::CourseCountryFilter" => {
# "USA" => true,
# "Japan" => true,
# "Spain" => false,
# "Thailand" => false,
# }
# }
def options
# Here you have access to the `applied_filters` object too
Author.select(:id, :name).each_with_object({}) { |author, options| options[author.id] = author.name }
end
end
```
You can add complex text filters to Avo using the Text filter
```bash
rails generate avo:filter name --type text
```
```ruby
class Avo::Filters::Name < Avo::Filters::TextFilter
self.name = "Name filter"
self.button_label = "Filter by name"
# `value` comes as text
# Eg: 'avo'
def apply(request, query, value)
query.where('LOWER(name) LIKE ?', "%#{value}%")
end
# def default
# 'avo'
# end
end
```
The ideal filter for date selection. This filter allows you to generate a date input, with options to include time selection and even a range selection mode. Customizable to suit your specific needs.
:::warning Timezone Handling
This filter sends the selected value exactly as selected, without any timezone adjustments. If you need to apply timezone conversion or adjustments, please ensure to handle it during the [`apply`](#apply) method.
:::
Generate one by using:
```bash
rails generate avo:filter created_at --type date_time
```
The generated file should be following a similar format:
```ruby
# frozen_string_literal: true
class Avo::Filters::CreatedAt < Avo::Filters::DateTimeFilter
self.name = "Created at"
# self.type = :date_time
# self.mode = :range
# self.visible = -> do
# true
# end
def apply(request, query, value)
query
end
# def format
# case type
# when :date_time
# 'yyyy-LL-dd TT'
# when :date
# 'yyyy-LL-dd'
# end
# end
# def picker_format
# case type
# when :date_time
# 'Y-m-d H:i:S'
# when :time
# 'Y-m-d'
# end
# end
end
```
### Type
Determines the format of the input field.
##### Default value
`:date_time`
By default, the input allows users to select both a date and a time.
##### Possible values
- `:date`
- This option restricts the input to date selection only, ideal for scenarios where time input is unnecessary.
- `:time`
- This option limits the input to time selection only, suitable to apply where only the time is relevant.
- `:date_time`
- This combined option enables both date and time selection, providing a comprehensive input for more detailed needs.
### Mode
Defines whether the input allows selection of a single date or a range of dates.
##### Default value
`:range`
By default, the input permits users to select a range of dates, ideal for scenarios such as booking periods or event durations.
##### Possible values
- `:range`
- Allows users to choose a start and end date, making it suitable for applications that require a time span, such as reservations or scheduling.
:::info
In `:range` mode the `value` will be formatted as `"2024-08-13 to 2024-08-16"`.
To separate the start and end dates, use `date_1, date_2 = value.split(" to ")`, which will split the value into `["2024-08-13", "2024-08-16"]`
:::
- `:single`
- Limits the selection to a single date, perfect for use cases where only one specific day needs to be selected, such as an appointment or event date.
### `picker_options`
This filter uses [flatpickr](https://flatpickr.js.org) as the date and time picker. If you wish to customize the pickerβs options, you can do so by overriding the [`picker_options(value)`](https://github.com/avo-hq/avo/blob/menu/lib/avo/filters/date_time_filter.rb#L22) method. You can merge your custom options with those provided by [flatpickr](https://flatpickr.js.org), which are detailed [here](https://flatpickr.js.org/options/).
```ruby{10-14}
# frozen_string_literal: true
class Avo::Filters::StartingAt < Avo::Filters::DateTimeFilter
self.name = "The starting at filter"
self.button_label = "Filter by start time"
self.empty_message = "Search by start time"
self.type = :time
self.mode = :single
def picker_options(value)
super.merge({
minuteIncrement: 3
})
end
def apply(request, query, value)
query.where("to_char(starting_at, 'HH24:MI:SS') = ?", value)
end
end
```
## Dynamic filter options
You might want to compose more advanced filters, like when you have two filters, one for the country and another for cities, and you'd like to have the cities one populated with cities from the selected country.
Let's take the `Avo::Resources::Course` as an example.
```ruby{3-5,7-14}
# app/models/course.rb
class Course < ApplicationRecord
def self.countries
["USA", "Japan", "Spain", "Thailand"]
end
def self.cities
{
USA: ["New York", "Los Angeles", "San Francisco", "Boston", "Philadelphia"],
Japan: ["Tokyo", "Osaka", "Kyoto", "Hiroshima", "Yokohama", "Nagoya", "Kobe"],
Spain: ["Madrid", "Valencia", "Barcelona"],
Thailand: ["Chiang Mai", "Bangkok", "Phuket"]
}
end
end
```
We will create two filtersβone for choosing countries and another for cities.
```ruby{4-5}
# app/avo/resources/course.rb
class Avo::Resources::Course < Avo::BaseResource
def filters
filter Avo::Filters::CourseCountryFilter
filter Avo::Filters::CourseCityFilter
end
end
```
The country filter is pretty straightforward. Set the query so the `country` field to be one of the selected countries and the `options` are the available countries as `Hash`.
```ruby{6,10}
# app/avo/filters/course_country.rb
class Avo::Filters::CourseCountry < Avo::Filters::BooleanFilter
self.name = "Course country filter"
def apply(request, query, values)
query.where(country: values.select { |country, selected| selected }.keys)
end
def options
Course.countries.map { |country| [country, country] }.to_h
end
end
```
The cities filter has a few more methods to manage the data better, but the gist is the same. The `query` makes sure the records have the city value in one of the cities that have been selected.
The `options` method gets the selected countries from the countries filter (`Avo::Filters::CourseCountryFilter`) and formats them to a `Hash`.
```ruby{6,10}
# app/avo/filters/course_city.rb
class Avo::Filters::CourseCity < Avo::Filters::BooleanFilter
self.name = "Course city filter"
def apply(request, query, values)
query.where(city: values.select { |city, selected| selected }.keys)
end
def options
cities_for_countries countries
end
private
# Get a hash of cities for certain countries
# Example payload:
# countries = ["USA", "Japan"]
def cities_for_countries(countries_array = [])
countries_array
.map do |country|
# Get the cities for this country
Course.cities.stringify_keys[country]
end
.flatten
# Prepare to transform to a Hash
.map { |city| [city, city] }
# Turn to a Hash
.to_h
end
# Get the value of the selected countries
# Example payload:
# applied_filters = {
# "Avo::Filters::CourseCountryFilter" => {
# "USA" => true,
# "Japan" => true,
# "Spain" => false,
# "Thailand" => false,
# }
# }
def countries
if applied_filters["Avo::Filters::CourseCountryFilter"].present?
# Fetch the value of the countries filter
applied_filters["Avo::Filters::CourseCountryFilter"]
# Keep only the ones selected
.select { |country, selected| selected }
# Pluck the name of the coutnry
.keys
else
# Return empty array
[]
end
end
end
```
The `countries` method above will check if the `Avo::Filters::CourseCountryFilter` has anything selected. If so, get the names of the chosen ones. This way, you show only the cities from the selected countries and not all of them.
## React to filters
Going further with the example above, a filter can react to other filters. For example, let's say that when a user selects `USA` from the list of countries, you want to display a list of cities from the USA (that's already happening in `options`), and you'd like to select the first one on the list. You can do that with the `react` method.
```ruby{21-36}
# app/avo/filters/course_city.rb
class Avo::Filters::CourseCity < Avo::Filters::BooleanFilter
self.name = "Course city filter"
def apply(request, query, values)
query.where(city: values.select { |city, selected| selected }.keys)
end
def options
cities_for_countries countries
end
# applied_filters = {
# "Avo::Filters::CourseCountryFilter" => {
# "USA" => true,
# "Japan" => true,
# "Spain" => false,
# "Thailand" => false,
# }
# }
def react
# Check if the user selected a country
if applied_filters["Avo::Filters::CourseCountryFilter"].present? && applied_filters["Avo::Filters::CourseCityFilter"].blank?
# Get the selected countries, get their cities, and select the first one.
selected_countries = applied_filters["Avo::Filters::CourseCountryFilter"].select do |name, selected|
selected
end
# Get the first city
cities = cities_for_countries(selected_countries.keys)
first_city = cities.first.first
# Return the first city as selected
[[first_city, true]].to_h
end
end
private
# Get a hash of cities for certain countries
# Example payload:
# countries = ["USA", "Japan"]
def cities_for_countries(countries_array = [])
countries_array
.map do |country|
# Get the cities for this country
Course.cities.stringify_keys[country]
end
.flatten
# Prepare to transform to a Hash
.map { |city| [city, city] }
# Turn to a Hash
.to_h
end
# Get the value of the selected countries
# Example `applied_filters` payload:
# applied_filters = {
# "Avo::Filters::CourseCountryFilter" => {
# "USA" => true,
# "Japan" => true,
# "Spain" => false,
# "Thailand" => false,
# }
# }
def countries
if applied_filters["Avo::Filters::CourseCountryFilter"].present?
# Fetch the value of the countries filter
applied_filters["Avo::Filters::CourseCountryFilter"]
# Keep only the ones selected
.select { |country, selected| selected }
# Pluck the name of the coutnry
.keys
else
# Return empty array
[]
end
end
end
```
After all, filters are applied, the `react` method is called, so you have access to the `applied_filters` object.
Using the applied filter payload, you can return the value of the current filter.
```ruby
def react
# Check if the user selected a country
if applied_filters["Avo::Filters::CourseCountryFilter"].present? && applied_filters["Avo::Filters::CourseCityFilter"].blank?
# Get the selected countries, get their cities, and select the first one.
selected_countries = applied_filters["Avo::Filters::CourseCountryFilter"]
.select do |name, selected|
selected
end
# Get the first city
cities = cities_for_countries(selected_countries.keys)
first_city = cities.first.first
# Return the first city selected as a Hash
[[first_city, true]].to_h
end
end
```
Besides checking if the countries filter is populated (`applied_filters["Avo::Filters::CourseCountryFilter"].present?`), we also want to allow the user to customize the cities filter further, so we need to check if the user has added a value to that filter (`applied_filters["Avo::Filters::CourseCountryFilter"].blank?`).
If these conditions are true, the country filter has a value, and the user hasn't selected any values from the cities filter, we can react to it and set a value as the default one.
Of course, you can modify the logic and return all kinds of values based on your needs.
## Empty message text
There might be times when you will want to show a message to the user when you're not returning any options. You may customize that message using the `empty_message` option.
```ruby{4}
# app/avo/filters/course_city.rb
class Avo::Filters::CourseCity < Avo::Filters::BooleanFilter
self.name = "Course city filter"
self.empty_message = "Please select a country to view options."
def apply(request, query, values)
query.where(city: values.select { |city, selected| selected }.keys)
end
def options
if countries.present?
[]
else
["Los Angeles", "New York"]
end
end
private
def countries
# logic to fetch the countries
end
end
```
## Keep filters panel open
There are scenarios where you wouldn't want to close the filters panel when you change the values. For that, you can use the `keep_filters_panel_open` resource option.
More on this on the `keep_filters_panel_open` resource option.
## Filter arguments
Filters can have different behaviors according to their host resource. In order to achieve that, arguments must be passed like on the example below:
```ruby{12-14}
class Avo::Resources::Fish < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id
field :name, as: :text
field :user, as: :belongs_to
field :type, as: :text, hide_on: :forms
end
def filters
filter Avo::Filters::NameFilter, arguments: {
case_insensitive: true
}
end
end
```
Now, the arguments can be accessed inside `Avo::Filters::NameFilter` ***`apply` method***, ***`options` method*** and on the ***`visible` block***!
```ruby{4-6,8-14}
class Avo::Filters::Name < Avo::Filters::TextFilter
self.name = "Name filter"
self.button_label = "Filter by name"
self.visible = -> do
arguments[:case_insensitive]
end
def apply(request, query, value)
if arguments[:case_insensitive]
query.where("LOWER(name) LIKE ?", "%#{value.downcase}%")
else
query.where("name LIKE ?", "%#{value}%")
end
end
end
```
## Manually create encoded URLs
You may want to redirect users to filtered states of the view from other places in your app. In order to create those filtered states you may use these helpers functions or Rails helpers.
### Rails helpers
Decodes the `filters` param. This Rails helper can be used anywhere in a view or off the `view_context`.
#### Usage
```ruby
# in a view
decode_filter_params params[:filters] # {"NameFilter"=>"Apple"}
# Or somewhere in an Avo configuration file
class Avo::Actions::DummyAction < Avo::BaseAction
self.name = "Dummy action"
def handle(**args)
filters = view_context.decode_filter_params(params[:filters])
do_something_important_with_the_filters filters
end
end
```
Encodes a `filters` object into a serialized state that Avo understands. This Rails helper can be used anywhere in a view or off the `view_context`.
#### Usage
```ruby
# in a view
filters = {"NameFilter"=>"Apple"}
encode_filter_params filters # eyJOYW1lRmlsdGVyIjoiQXBwbGUifQ==
# Or somewhere in an Avo configuration file
class Avo::Actions::DummyAction < Avo::BaseAction
self.name = "Dummy action"
def handle(**args)
do_something_important
redirect_to avo.resources_users_path(filters: view_context.decode_filter_params({"NameFilter"=>"Apple"}))
end
end
```
### Standalone helpers
Decodes the `filters` param. This standalone method can be used anywhere.
#### Usage
```ruby
class Avo::Actions::DummyAction < Avo::BaseAction
self.name = "Dummy action"
def handle(**args)
filters = Avo::Filters::BaseFilter.decode_filters(params[:filters])
do_something_important_with_the_filters filters
end
end
```
Encodes a `filters` object into a serialized state that Avo understands. This standalone method can be used anywhere.
#### Usage
```ruby
class Avo::Actions::DummyAction < Avo::BaseAction
self.name = "Dummy action"
def handle(**args)
do_something_important
redirect_to avo.resources_users_path(encoded_filters: Avo::Filters::BaseFilter.encode_filters({"Avo::Filters::NameFilter"=>"Apple"}))
end
end
```
---
# Dynamic filters
The Dynamic filters make it so easy to add multiple, composable, and dynamic filters to the view.
The first thing you need to do is add the `filterable: true` attribute to the fields you need to filter through. We use `ransack` behind the scenes so it's essential to configure the `ransackable_attributes` list to ensure that every filterable field is incorporated within it.
:::info Filter Combination Logic
When multiple filters are applied:
- Filters on the same attribute are combined using OR conditions
- Filters on different attributes are combined using AND conditions
For example, if you have two filters on the `name` field (one for "John" and one for "Jane"), the query will find records where the name is either "John" OR "Jane". However, if you have one filter on `name` for "John" and another on `status` for "active", the query will find records where the name is "John" AND the status is "active".
:::
```ruby{4-6} [Fields]
class Avo::Resources::Project < Avo::BaseResource
def fields
field :name, as: :text
field :status, as: :status, filterable: true
field :stage, as: :badge, filterable: true
field :country, as: :country, filterable: true
end
end
```
Authorize ransackable_attributes
```ruby{3,11}
class Project < ApplicationRecord
def self.ransackable_attributes(auth_object = nil)
["status", "stage", "country"] # the array items should be strings not symbols
end
end
# Or authorize ALL attributes at once
class Project < ApplicationRecord
def self.ransackable_attributes(auth_object = nil)
authorizable_ransackable_attributes
end
end
```
:::warning
Ensure the array items are strings, not symbols.
:::
This will make Avo add this new "Filters" button to the view of your resource.
When the user clicks the button, a new filters bar will appear below enabling them to add filters based on the attributes you marked as filterable.
The user can add multiple filters for the same attribute if they desire so.
## Filter types
The filter type determines the kind of input provided by the filter.
For instance, a [text](#text) type filter will render a text input field, while a [select](#select) type filter will render a dropdown menu with predefined options fetched from the field.
#### Conditions
Each filter type also offers a different set of conditions. Conditions specify how the input value should be applied to filter the data. For example, [text](#text) filters have conditions such as `Contains` or `Starts with`, while number filters include `=` (equals) or `>` (greater than).
#### Query
Avo uses the input value and the specified condition to build a Ransack query. The filter conditions and input values are translated into Ransack predicates, which are then used to fetch the filtered data.
For instance, in the text filter example above, the `Contains` condition and the input value `John` are translated into a Ransack query resulting into the SQL `LIKE` operator to find all records where the name contains `John`.
### Conditions
- Is true
- Is false
- Is null
- Is not null
```ruby
{
is_true: "Is true",
is_false: "Is false",
is_null: "Is null",
is_not_null: "Is not null",
}.invert
```
Test it on [avodemo](https://main.avodemo.com/avo/resources/users?filters[is_admin?][is_true][]=), check the [source code](https://github.com/avo-hq/main.avodemo.com/blob/main/app/avo/resources/user.rb#L38)
### Conditions
- Is
- Is not
- Is on or before
- Is on or after
- Is within
- Is null
- Is not null
```ruby
{
is: "Is",
is_not: "Is not",
lte: "Is on or before",
gte: "Is on or after",
is_within: "Is within",
is_null: "Is null",
is_not_null: "Is not null",
}.invert
```
Test it on [avodemo](https://main.avodemo.com/avo/resources/teams?filters[created_at][lte][]=2024-07-02%2012%3A00), check the [source code](https://github.com/avo-hq/main.avodemo.com/blob/main/app/avo/resources/team.rb#L50)
Same as **Date** (same conditions and visuals), but the picker also enables time selection.
```ruby
dynamic_filter :created_at, type: :date_time
```
Same as **Date** (same conditions and visuals), but the picker is time-only (no calendar).
```ruby
dynamic_filter :published_at, type: :time
```
### Conditions
- `=` (equals)
- `!=` (is different)
- `>` (greater than)
- `>=` (greater than or equal to)
- `<` (lower than)
- `<=` (lower than or equal to)
- Is within
- Is null
- Is not null
```ruby
{
is: "=",
is_not: "!=",
gt: ">",
gte: ">=",
lt: "<",
lte: "<=",
is_within: "Is within",
is_null: "Is null",
is_not_null: "Is not null",
}.invert
```
Test it on [avodemo](https://main.avodemo.com/avo/resources/teams?filters[id][gte][]=2), check the [source code](https://github.com/avo-hq/main.avodemo.com/blob/main/app/avo/resources/team.rb#L27)
### Conditions
- Is
- Is not
- Is null
- Is not null
```ruby
{
is: "Is",
is_not: "Is not",
is_null: "Is null",
is_not_null: "Is not null",
}.invert
```
Test it on [avodemo](https://main.avodemo.com/avo/resources/courses?filters[country][is][]=USA), check the [source code](https://github.com/avo-hq/main.avodemo.com/blob/main/app/avo/resources/course.rb#L55)
### Conditions
- Contains
- Does not contain
- Is
- Is not
- Starts with
- Ends with
- Is null
- Is not null
- Is present
- Is blank
```ruby
{
contains: "Contains",
does_not_contain: "Does not contain",
is: "Is",
is_not: "Is not",
starts_with: "Starts with",
ends_with: "Ends with",
is_null: "Is null",
is_not_null: "Is not null",
is_present: "Is present",
is_blank: "Is blank",
}.invert
```
Test it on [avodemo](https://main.avodemo.com/avo/resources/users?filters[first_name][contains][]=Avo), check the [source code](https://github.com/avo-hq/main.avodemo.com/blob/main/app/avo/resources/user.rb#L33)
### Conditions
- Are
- Contain
- Overlap
- Contained in ([`active_record_extended`](https://github.com/GeorgeKaraszi/ActiveRecordExtended) gem required)
```ruby
{
array_is: "Are",
array_contains: "Contain",
array_overlap: "Overlap",
array_contained_in: "Contained in" # (active_record_extended gem required)
}.invert
```
:::warning
Contained in will not work when using the `acts-as-taggable-on` gem.
:::
Test it on [avodemo](https://main.avodemo.com/avo/resources/courses?filters[skills][array_contains][]=), check the [source code](https://github.com/avo-hq/main.avodemo.com/blob/main/app/avo/resources/course.rb#L46)
:::info
The source code uses custom dynamic filters DSL available
Check how to do a more advanced configuration on the [custom dynamic filters](#custom-dynamic-filters) section.
:::
## Options
You can have a few customization options available that you can add in your `avo.rb` initializer file.
```ruby
Avo.configure do |config|
# Other Avo configurations
end
if defined?(Avo::DynamicFilters)
Avo::DynamicFilters.configure do |config|
config.button_label = "Advanced filters"
config.always_expanded = true
end
end
```
This will change the label on the expand label.
You may opt-in to have them always expanded and have the button hidden.
## Field to filter matching
On versions **lower** than the filters are not configurable so each field will have a dedicated filter type. Check how to do a more advanced configuration on the [custom dynamic filters](#custom-dynamic-filters) section.
Field-to-filter matching in versions **lower** than :
```ruby
def field_to_filter(type)
case type.to_sym
when :boolean
:boolean
when :date, :date_time, :time
:date
when :id, :number, :progress_bar
:number
when :select, :badge, :country, :status
:select
when :text, :textarea, :code, :markdown, :password, :trix
:text
else
:text
end
end
```
## Caveats
At some point we'll integrate the Basic filters into the dynamic filters bar. Until then, if you have both basic and dynamic filters on your resource you'll have two `Filters` buttons on your view.
To mitigate that you can toggle the `always_expanded` option to true.
## Custom Dynamic Filters
Dynamic filters are great but strict, as each field creates a specific filter type, each with its own icon and query. The query remains static, targeting only that particular field. Since version , dynamic filters have become customizable and, even better, can be declared without being bound to a field.
There are two ways to define custom dynamic filters: the field's `filterable` option and the `dynamic_filter` method.
### Defining custom dynamic filters
To start customizing a dynamic filter from the `filterable` option, change its value to a hash:
```ruby
field :first_name,
as: :text,
filterable: true # [!code --]
filterable: { } # [!code ++]
```
From this hash, you can configure several options specified below.
Alternatively, you can define a custom dynamic filter using the `dynamic_filter` method, which should be called inside the `filters` method:
```ruby
def filters
# ...
dynamic_filter :first_name
# ...
end
```
Each option specified below can be used as a key in the hash definition or as a keyword argument in the method definition.
:::info Filters order
The filter order is computed. Dynamic filters defined by the `dynamic_filter` method will respect the definition order and will be rendered first in the filter list. Filters declared using the field's `filterable` option will be sorted by label.
:::
:::warning Custom Dynamic Filter IDs
When using a custom dynamic filter, the generated filter ID may not directly correspond to a database column. In such cases, you should use the [`query_attributes`](#query_attributes) option to specify which database columns the filter should apply to.
For example, consider a `City` model with a `population` column in the database:
```ruby
# The filter ID is custom_population
# However, the filter should apply the query to the population attribute.
dynamic_filter :custom_population, query_attributes: :population
```
:::
Customize filter's label
##### Default value
Field's / filter's ID humanized.
#### Possible values
Any string
Customize filter's icon. Check icons documentation
##### Default value
Boolean filter - `heroicons/outline/check-circle`
Calendar filter - `heroicons/outline/calendar-days`
Number filter - `heroicons/outline/hashtag`
Select filter - `heroicons/outline/arrow-down-circle`
Tags filter - `heroicons/outline/tag`
Text filter - `avo/font`
#### Possible values
Any icon from [avo](https://github.com/avo-hq/avo/tree/feature/allow_actions_to_render_turbo_streams/app/assets/svgs/avo) or [heroicons](https://heroicons.com/).
Customize filter's type
##### Default value
Computed from field using [`field_to_filter` method](#field-to-filter-matching).
#### Possible values
- [`:boolean`](#boolean)
- [`:date`](#date)
- [`:date_time`](#date-time) (same behavior as Date, with time enabled)
- [`:time`](#time) (same behavior as Date, time-only picker)
- [`:number`](#number)
- [`:select`](#select)
- [`:text`](#text)
- [`:tags`](#tags)
:::info
the default filtering system is no longer applied when a `query` is specified on a dynamic filter.
:::
Customize filter's query
##### Default value
Applies the condition to the field's attribute. For example, if the field is `first_name`, the condition is `contains`, and the value is `Bill`, the query will restrict to all records where the first name contains `Bill`.
#### Possible values
Any lambda function.
Within the function, you have access to `query` and `filter_param` as well as all attributes of `Avo::ExecutionContext`.
`filter_param` is an Avo object that stores the filter's `id`, the applied `condition` and the `value`.
Usage example:
```ruby {6-13,19-26}
# Using field's filterable option
field :first_name,
as: :text,
filterable: {
# ...
conditions: {
case_sensitive: "Is (case sensitive)",
not_case_sensitive: "Is (case insensitive)"
}.invert,
query: -> {
case filter_param.condition.to_sym
when :case_sensitive
query.where("name = ?", filter_param.value)
when :not_case_sensitive
query.where("LOWER(name) = ?", filter_param.value.downcase)
end
}
# ...
}
# Using dynamic_filter method
dynamic_filter :first_name,
conditions: {
case_sensitive: "Is (case sensitive)",
not_case_sensitive: "Is (case insensitive)"
}.invert,
query: -> {
case filter_param.condition.to_sym
when :case_sensitive
query.where("name = ?", filter_param.value)
when :not_case_sensitive
query.where("LOWER(name) = ?", filter_param.value.downcase)
end
}
```
Customize filter's conditions
##### Default value
Check default conditions for each filter type above on this page.
#### Possible values
- A hash with the desired key-values to customize available conditions
- An empty hash `{}` to hide conditions dropdown and use the first default condition
##### Usage examples
###### Custom conditions
```ruby {6-9,15-18}
# Using field's filterable option
field :first_name,
as: :text,
filterable: {
# ...
conditions: {
case_sensitive: "Case sensitive",
not_case_sensitive: "Not case sensitive"
}.invert
# ...
}
# Using dynamic_filter method
dynamic_filter :first_name,
conditions: {
case_sensitive: "Case sensitive",
not_case_sensitive: "Not case sensitive"
}.invert
```
###### Hide conditions dropdown
When set to an empty hash (`{}`), this option hides the conditions dropdown and automatically applies the first default condition for each filter type. This is particularly useful when you want to simplify the filter interface by removing the conditions selection, especially for filters where only one condition makes sense.
```ruby{3}
dynamic_filter :last_name,
type: :select,
conditions: {},
options: User.pluck(:last_name).compact
```
```ruby{4}
field :department,
as: :select,
filterable: {
conditions: {},
type: :select,
options: ["Engineering", "Marketing", "Sales", "Support"]
}
```
:::info
When `conditions: {}` is used, the filter will automatically use the first condition from the default conditions list for that filter type. For example, a select filter will use "Is" condition, and a text filter will use "Contains" condition.
:::
Customize filter's query attributes
##### Default value
Field's / filter's id
#### Possible values
Any model DB column(s). Use an array of symbols for multiple columns or a single symbol for a single column. If your model has DB columns like `first_name` and `last_name`, you can combine both on a single filter:
```ruby {6,13}
# Using field's filterable option
field :name,
as: :text,
filterable: {
# ...
query_attributes: [:first_name, :last_name]
# ...
}
# Using dynamic_filter method
dynamic_filter :name,
type: :text,
query_attributes: [:first_name, :last_name]
```
You can also add query attributes for a `belongs_to` association. For example, with a model that belongs to `User`:
```ruby {7,13}
# Using field's filterable option
field :user,
as: :belongs_to,
filterable: {
label: "User (email & first_name)",
icon: "heroicons/solid/users",
query_attributes: [:user_email, :user_first_name]
}
# Using dynamic_filter method
dynamic_filter label: "User (email & first_name)",
icon: "heroicons/solid/users",
query_attributes: [:user_email, :user_first_name]
```
This is possible due to a Ransack feature. To use it, you need to add the association name before the attribute.
Suggestions work on filters that provide text input, enhancing the user experience by offering relevant options. This functionality is especially useful in scenarios where users might need guidance or where the filter values are numerous or complex.
##### Default value
`nil`
:::info
on `tags` fields the `suggestions` are fetched from the field.
:::
#### Possible values
- Array of strings
```ruby {6,12}
# Using field's filterable option
field :first_name,
as: :text,
filterable: {
# ...
suggestions: ["Avo", "Cado"]
# ...
}
# Using dynamic_filter method
dynamic_filter :first_name,
suggestions: ["Avo", "Cado"]
```
- Proc that returns an array of strings
when the filter is applied to an association, the `parent_record` becomes accessible within the `suggestions` block.
```ruby {6,12}
# Using field's filterable option
field :first_name,
as: :text,
filterable: {
# ...
suggestions: -> { ["Avo", "Cado", params[:extra_suggestion]] }
# ...
}
# Using dynamic_filter method
dynamic_filter :first_name,
suggestions: -> { ["Avo", "Cado", params[:extra_suggestion]] }
```
- Array of hashes with the keys `value`, `label` and optionally an `avatar`
:::warning Applicable only to filters with type tags.
:::
:::code-group
```ruby {6-13,19-26} [Direct assign]
# Using field's filterable option
field :tags,
as: :tags,
filterable: {
# ...
suggestions: [
{
value: 1,
label: 'one',
avatar: 'https://images.unsplash.com/photo-1560363199-a1264d4ea5fc?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&w=256&h=256&fit=crop',
},
# ...
]
# ...
}
# Using dynamic_filter method
dynamic_filter :tags,
suggestions: [
{
value: 1,
label: 'one',
avatar: 'https://images.unsplash.com/photo-1560363199-a1264d4ea5fc?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&w=256&h=256&fit=crop',
},
# ...
]
```
```ruby {6-15,21-30} [Proc]
# Using field's filterable option
field :tags,
as: :tags,
filterable: {
# ...
suggestions: -> {
[
{
value: 1,
label: 'one', # or params[:something]
avatar: 'https://images.unsplash.com/photo-1560363199-a1264d4ea5fc?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&w=256&h=256&fit=crop',
},
# ...
]
}
# ...
}
# Using dynamic_filter method
dynamic_filter :tags,
suggestions: -> {
[
{
value: 1,
label: 'one', # or params[:something]
avatar: 'https://images.unsplash.com/photo-1560363199-a1264d4ea5fc?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&w=256&h=256&fit=crop',
},
# ...
]
}
```
:::
:::warning
This option is compatible **only** with `tags` filters.
:::
In some cases, you may need to retrieve values dynamically from an API. The `fetch_values_from` option allows you to provide a URL from which the filter will suggest values, functioning similarly to the `fetch_values_from` option in the tags field.
When a user searches for a record, the filter's input will send a request to the server to fetch records that match the query.
##### Default value
`nil`
:::info
If you're using a `filterable` field the `fetch_values_from` are fetched from the field.
```ruby
field :tags, as: :tags,
fetch_values_from: -> { "/avo-filters/resources/cities/tags" }
filterable: true
```
:::
#### Possible values
- String
```ruby
fetch_values_from: "/avo-filters/resources/cities/tags"
```
- Proc that evaluates to a string.
```ruby
fetch_values_from: -> { "/avo-filters/resources/cities/tags" }
```
The endpoint should handle two different scenarios:
1. **Search functionality**: When a user types in the filter input, the endpoint receives the user input as `q` in the params (`params["q"]`)
2. **Initial load**: When the filter already has selected values (like on page load), the endpoint receives an array of values in `params[:value]` to fetch the corresponding labels
The endpoint should return an array of objects with the keys `value`, `label` and optionally `avatar`.
::: code-group
```ruby{3-33} [app/controllers/avo/cities_controller.rb]
class Avo::CitiesController < Avo::ResourcesController
def tags
if params[:value].present?
# Handle initial load: return labels for selected values
# params[:value] contains an array of selected values
selected_cities = City.where(id: params[:value])
render json: selected_cities.map do |city|
{
value: city.id,
label: city.name,
avatar: city.avatar_url
}
end
elsif params["q"].present?
# Handle search: return cities matching the query
cities = City.where("name ILIKE ?", "%#{params["q"]}%").limit(10)
render json: cities.map do |city|
{
value: city.id,
label: city.name,
avatar: city.avatar_url
}
end
else
# Handle empty state: return some default suggestions
render json: [
{
value: 1,
label: "New York",
avatar: "https://images.unsplash.com/photo-1560363199-a1264d4ea5fc?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&w=256&h=256&fit=crop"
}
]
end
end
end
```
```ruby{5-11} [config/routes.rb]
Rails.application.routes.draw do
# your routes...
end
if defined? ::Avo
Avo::Engine.routes.draw do
scope :resources do
get "cities/tags", to: "cities#tags"
end
end
end
```
:::
Customize the options **for select type filters**. **This is available only for select type filters** and determines the options visible in the select dropdown.
##### Default value
Fetched from field if bond to a field or `[]`
#### Possible values
An array or hash where the key-value pairs represent the options.
- If a hash is provided, the key is the option label and the value is the option value.
- If an array is provided, the array elements are used as both the option value and the option label.
##### Usage examples
###### Array
```ruby{3}
dynamic_filter :version,
type: :select,
options: ["Label 1", "Label 2"]
```
###### Hash (with invert)
```ruby{3-6}
dynamic_filter :version,
type: :select,
options: {
value_1: "Label 1",
value_2: "Label 2"
}.invert
```
###### Hash (without invert)
```ruby{3-6}
dynamic_filter :version,
type: :select,
options: {
"Label 1" => :value_1,
"Label 2" => :value_2
}
```
Controls whether the "Apply" button should be rendered in the filter interface.
##### Default value
`true`
#### Possible values
Boolean value (`true` or `false`).
When set to `false`, the apply button will be hidden from the filter interface. This is particularly useful when combined with `apply_on_select: true` to create an immediate filtering experience.
##### Usage examples
```ruby{3}
dynamic_filter :status,
type: :select,
render_apply_button: false
```
```ruby{4-5}
field :status,
as: :select,
filterable: {
render_apply_button: false,
apply_on_select: true,
options: ["active", "inactive", "pending"]
}
```
Controls whether the filter should be applied immediately when the selected value changes, without requiring the user to click the "Apply" button.
##### Default value
`false`
#### Possible values
Boolean value (`true` or `false`).
When set to `true`, the filter will automatically apply as soon as the user selects or changes a value. This creates a more responsive user experience, especially when combined with `render_apply_button: false`.
##### Usage examples
```ruby{3-4}
dynamic_filter :category,
type: :select,
apply_on_select: true,
render_apply_button: false
```
```ruby{4-5}
field :priority,
as: :select,
filterable: {
apply_on_select: true,
render_apply_button: false,
options: ["high", "medium", "low"]
}
```
Allows you to customize how filter values are displayed in the filter interface by providing humanized, user-friendly representations of the internal filter values.
##### Default value
`value` - The filter will display the raw filter values.
#### Possible values
A lambda/proc that returns a string representing the humanized value. Within the function, you have access to the `value` and `filter` object which contains the current filter's condition, as well as all attributes of `Avo::ExecutionContext`. Additionally `parent_record` (when the filter is applied to an association) is available.
##### Usage examples
```ruby{4-11}
field :is_capital,
as: :boolean,
filterable: {
humanized_value: -> {
case filter.condition
when "is_true"
"yes"
when "is_false"
"no"
end
}
}
```
```ruby{4-8}
dynamic_filter label: "Tags with fetch_values_from",
type: :tags,
fetch_values_from: -> { "/avo-filters/resources/cities/tags" },
humanized_value: -> {
City.controller_suggestions.select do |suggestion|
suggestion[:value].to_s.in?(value.split(","))
end.map { _1[:label] }.join(", ")
}
```
Allows you to customize how filter conditions are displayed in the filter's pill interface by providing humanized, user-friendly representations of the internal filter conditions.
##### Default value
`condition` - The filter will display the auto-generated label for the filter condition.
#### Possible values
A lambda/proc that returns a string representing the humanized value. Within the function, you have access to the `condition` and `filter` object which contains the current filter's condition, as well as all attributes of `Avo::ExecutionContext`. Additionally `parent_record` (when the filter is applied to an association) is available.
##### Usage examples
```ruby{5-7}
dynamic_filter :author,
type: :tags,
icon: "heroicons/outline/users",
conditions: {},
humanized_condition: -> {
(filter.value.split(",").count > 1) ? "are" : "is"
},
query: -> {
query.where(author_id: filter_param.value.split(","))
}
```
## Guides & Tutorials
Learn how to effectively filter records based on their associations in Avo. This video tutorial demonstrates how to set up and use dynamic filters to query records through the attributes of their associations, enabling powerful and flexible data filtering capabilities.
#### `belongs_to` example
```ruby{5-11,16-18}
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
# Using field's filterable option
def fields
field :user,
as: :belongs_to,
filterable: {
label: "User (email & first_name)",
icon: "heroicons/solid/users",
query_attributes: [:user_email, :user_first_name]
}
end
# OR using dynamic_filter method
def filters
dynamic_filter label: "User (email & first_name)",
icon: "heroicons/solid/users",
query_attributes: [:user_email, :user_first_name]
end
end
```
### `has_many` example
```ruby{19-22}
class Avo::Resources::Author < Avo::BaseResource
self.record_selector = false
def fields
field :preview, as: :preview
field :book_list, only_on: :preview do
tag.div do
tag.ul do
safe_join(
record.books.map do |book|
tag.li("#{book.title} (#{book.genre})")
end
)
end
end
end
field :name, filterable: true
# Filter the books by title and genre
field :books, as: :has_many, filterable: {
query_attributes: [:books_title, :books_genre]
}
end
end
```
When you have multiple fields that require similar filtering logic, you can create reusable filter helpers to avoid code duplication. This is particularly useful when working with JSON columns, complex queries, or any scenario where multiple fields share the same filtering pattern.
This guide demonstrates four different approaches to create composable filters, each with its own benefits and use cases.
### The Problem: Repetitive Filter Code
Before diving into the solutions, let's look at a common problem where filter logic is repeated across multiple fields:
```ruby
class Avo::Resources::Feedback < Avo::BaseResource
def fields
field :company_size, filterable: {
type: :select,
options: -> { Feedback.pluck(Arel.sql("answers->>'company_size'")).uniq },
query: -> { query.where(Arel.sql("answers->>'company_size' = '#{filter_param.value}'")) }
}
field :company_industry, filterable: {
type: :select,
options: -> { Feedback.pluck(Arel.sql("answers->>'company_industry'")).uniq },
query: -> { query.where(Arel.sql("answers->>'company_industry' = '#{filter_param.value}'")) }
}
field :title, filterable: {
type: :select,
options: -> { Feedback.pluck(Arel.sql("answers->>'title'")).uniq },
query: -> { query.where(Arel.sql("answers->>'title' = '#{filter_param.value}'")) }
}
field :description, filterable: {
type: :select,
options: -> { Feedback.pluck(Arel.sql("answers->>'description'")).uniq },
query: -> { query.where(Arel.sql("answers->>'description' = '#{filter_param.value}'")) }
}
end
end
```
As you can see, the same filtering logic is repeated for each field, which violates the DRY (Don't Repeat Yourself) principle and makes the code harder to maintain.
### Method 1: Helper Method with Field Configuration
This approach extracts the common filtering logic into a helper method that returns the filterable configuration hash. It's the most straightforward refactoring and maintains the existing field-based approach.
```ruby
class Avo::Resources::Feedback < Avo::BaseResource
def filterable_helper(field_name)
{
type: :select,
options: -> { Feedback.pluck(Arel.sql("answers->>'#{field_name}'")).uniq },
query: -> { query.where(Arel.sql("answers->>'#{field_name}' = '#{filter_param.value}'")) }
}
end
def fields
field :company_size, filterable: filterable_helper(:company_size)
field :company_industry, filterable: filterable_helper(:company_industry)
field :title, filterable: filterable_helper(:title)
field :description, filterable: filterable_helper(:description)
end
end
```
**Benefits:**
- Simple refactoring that maintains the existing field structure
- Easy to understand and implement
- Minimal changes to existing code
**Best for:** Quick refactoring of existing resources with repetitive filter logic.
:::warning
When you're using this approach within a `with_options` block, you need allow the extra args that are passed to the helper method.
```ruby
def filterable_helper(field_name, **args)
# ...
end
```
:::
### Method 2: Separate Fields and Filters with Helper
This approach separates the field definitions from the filter definitions using the `dynamic_filter` method. The helper method now directly creates the dynamic filter instead of returning a configuration hash.
```ruby
class Avo::Resources::Feedback < Avo::BaseResource
def fields
field :company_size
field :company_industry
field :title
field :description
end
def filterable_helper(field_name)
dynamic_filter field_name,
type: :select,
options: -> { Feedback.pluck(Arel.sql("answers->>'#{field_name}'")).uniq },
query: -> { query.where(Arel.sql("answers->>'#{field_name}' = '#{filter_param.value}'")) }
end
def filters
filterable_helper(:company_size)
filterable_helper(:company_industry)
filterable_helper(:title)
filterable_helper(:description)
end
end
```
**Benefits:**
- Clear separation between field definitions and filter logic
- Uses the more flexible `dynamic_filter` method
- Filters can be defined independently of fields
**Best for:**
- Resources where you want to maintain clean separation between display fields and filtering logic.
- Dynamic filters that are common across multiple resources.
### Method 3: Programmatic Filter Generation
This approach uses Ruby's array iteration to programmatically generate multiple filters with the same logic. It's the most concise and reduces the code to its essential elements.
```ruby
class Avo::Resources::Feedback < Avo::BaseResource
def fields
field :company_size
field :company_industry
field :title
field :description
end
def filters
[:company_size, :company_industry, :title, :description].map do |field_name|
dynamic_filter field_name,
type: :select,
options: -> { Feedback.pluck(Arel.sql("answers->>'#{field_name}'")).uniq },
query: -> { query.where(Arel.sql("answers->>'#{field_name}' = '#{filter_param.value}'")) }
end
end
end
```
**Benefits:**
- Most concise code
- Easy to add or remove filterable fields by modifying the array
- Clearly shows which fields share the same filtering logic
**Best for:** Resources with many fields that share identical filtering logic, especially when the list of filterable fields might change frequently.
### Method 4: Custom DSL with Method Override
This approach creates a custom DSL by overriding the `field` method to intercept a special symbol (`:by_answer`). This provides the cleanest syntax at the field level while hiding the complexity in the method override.
```ruby
class Avo::Resources::Feedback < Avo::BaseResource
def fields
field :company_size, filterable: :by_answer
field :company_industry, filterable: :by_answer
field :title, filterable: :by_answer
field :description, filterable: :by_answer
end
def field(field_name, **args, &block)
if args[:filterable] == :by_answer
args[:filterable] = {
type: :select,
options: -> { Feedback.pluck(Arel.sql("answers->>'#{field_name}'")).uniq },
query: -> { query.where(Arel.sql("answers->>'#{field_name}' = '#{filter_param.value}'")) }
}
end
super(field_name, **args, &block)
end
end
```
**Benefits:**
- Creates a clean, semantic DSL
- Hides complexity while maintaining readable field definitions
- Easy to extend with additional filter types
- Most maintainable for resources with many similar filterable fields
**Best for:** Resources where you want to create a custom, reusable filtering pattern that feels natural and integrated with Avo's field DSL.
### Choosing the Right Approach
- **Method 1** for quick refactoring of existing code
- **Method 2** when you want clear separation between fields and filters
- **Method 3** when you have many fields with identical filtering logic
- **Method 4** when you want to create a clean, reusable DSL for your team
All approaches achieve the same goal of eliminating code duplication while providing different levels of abstraction and maintainability.
---
# Customization options
## Change the app name
On the main navbar next to the logo, Avo generates a link to the homepage of your app. The label for the link is usually computed from your Rails app name. You can customize that however, you want using `config.app_name = 'Avocadelicious'`.
The `app_name` option is also callable using a block. This is useful if you want to reference a `I18n.t` method or something more dynamic.
```ruby
Avo.configure do |config|
config.app_name = -> { I18n.t "app_name" }
end
```
## Timezone and Currency
Your data-rich app might have a few fields where you reference `date`, `datetime`, and `currency` fields. You may customize the global timezone and currency with `config.timezone = 'UTC'` and `config.currency = 'USD'` config options.
## Resource Index view
There are a few customization options to change how resources are displayed in the **Index** view.
### Resources per page
You may customize how many resources you can view per page with `config.per_page = 24`.
### Per page steps
Similarly customize the per-page steps in the per-page picker with `config.per_page_steps = [12, 24, 48, 72]`.
### Resources via per page
For `has_many` associations you can control how many resources are visible in their `Index view` with `config.via_per_page = 8`.
### Default view type
The `ResourceIndex` component supports two view types `:table` and `:grid`. You can change that by `config.default_view_type = :table`. Read more on the grid view configuration page.
## ID links to resource
On the **Index** view, each row has the controls component at the end, which allows the user to go to the **Show** and **Edit** views and delete that entry. If you have a long row and a not-so-wide display, it might not be easy to scroll to the right-most section to click the **Show** link.
You can enable the `id_links_to_resource` config option to make it easier.
```ruby{4}
Avo.configure do |config|
config.root_path = '/avo'
config.app_name = 'Avocadelicious'
config.id_links_to_resource = true
end
```
That will render all `id` fields in the **Index** view as a link to that resource.
## Resource controls on the left or both sides
:::warning
`resource_controls_placement` option is **obsolete**.
Check row controls configuration on table view instead
:::
By default, the resource controls are located on the right side of the record rows, which might be hidden if there are a lot of columns. You might want to move the controls to the left side in that situation using the `resource_controls_placement` option.
```ruby{3}
# config/initializers/avo.rb
Avo.configure do |config|
config.resource_controls_placement = :left
end
```
You're able to render the controls on both sides
```ruby{3}
# config/initializers/avo.rb
Avo.configure do |config|
config.resource_controls_placement = :both
end
```
## Container width
Control how wide Avo's main content area is. The default keeps index views in a large constrained container and show/form views in a narrower one.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
# Apply one width to all views
config.container_width = :full
# Or target specific views with a hash
config.container_width = { index: :full }
end
```
### Width options
| Value | Behaviour |
| -------- | ------------------------------------------- |
| `:large` | Constrained container (default for index) |
| `:small` | Narrow container (default for show / forms) |
| `:full` | Full viewport width |
### Hash keys
Pass a hash to override specific views. Both individual view keys and group aliases are supported.
**Individual view keys:** `:index`, `:show`, `:new`, `:edit`, `:create`, `:update`
**Group aliases:**
| Alias | Expands to |
| ---------- | ---------------------------------------------- |
| `:forms` | `:new`, `:edit`, `:create`, `:update` |
| `:display` | `:index`, `:show` |
| `:single` | `:show`, `:new`, `:edit`, `:create`, `:update` |
When a specific key and a group alias target the same view, the specific key wins.
### Examples
```ruby
# All views full-width
config.container_width = :full
# Only the index is full-width; show and forms keep their defaults
config.container_width = { index: :full }
# All single-record views full-width; index stays large
config.container_width = { single: :full }
# Forms full-width, show and index keep defaults
config.container_width = { forms: :full }
# Mix: single full-width, but show overridden back to small
config.container_width = { single: :full, show: :small }
```
### Upgrading from `full_width_container` / `full_width_index_view`
| Old | New |
| --- | --- |
| `config.full_width_container = true` | `config.container_width = :full` |
| `config.full_width_container = false` | Remove the line (default is correct) |
| `config.full_width_index_view = true` | `config.container_width = { index: :full }` |
## Cache resources on the `Index` view
Avo caches each resource row (or Grid item for Grid view) for performance reasons. You can disable that cache using the `cache_resources_on_index_view` configuration option. The cache key is using the record's `id` and `created_at` attributes and the resource file `md5`.
:::info
If you use the `visibility` option to show/hide fields based on the user's role, you should disable this setting.
:::
```ruby{2}
Avo.configure do |config|
config.cache_resources_on_index_view = false
end
```
## Context
In the `Resource` and `Action` classes, you have a global `context` object to which you can attach a custom payload. For example, you may add the `current_user`, the current request `params`, or any other arbitrary data.
You can configure it using the `set_context` method in your initializer. The block you pass in will be instance evaluated in `Avo::ApplicationController`, so it will have access to the `_current_user` method or `Current` object.
```ruby{3-6}
Avo.configure do |config|
config.set_context do
{
foo: 'bar',
params: request.params,
}
end
end
```
:::warning `_current_user`
It's recommended you don't store your current user here but using the `current_user_method` config.
:::
You can access the context data with `::Avo::Current.context` object.
## Eject
This section has moved.
## Breadcrumbs
This section has moved to the Breadcrumbs page.
## Toggle the sidebar button visibility
By default, Avo displays a toggle button in the navbar that allows users to collapse and expand the sidebar on desktop. You can hide this button using the `sidebar_toggle_visible` configuration option. On mobile, the sidebar toggle is always visible regardless of this setting.
```ruby{2}
Avo.configure do |config|
config.sidebar_toggle_visible = false
end
```
When set to `false`, the sidebar will remain permanently open on desktop and users won't be able to collapse it.
## Body classes
You can add custom CSS classes to Avo's `` tag using the `body_classes` configuration option. This is useful for applying global styles, theme variations, or targeting specific layouts with CSS.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.body_classes = "custom-theme compact-layout"
end
```
You can also pass an array:
```ruby
Avo.configure do |config|
config.body_classes = ["custom-theme", "compact-layout"]
end
```
For dynamic classes, use a block. It's evaluated with Avo's `ExecutionContext`, so you have access to `current_user`, `request`, `params`, and other context methods.
```ruby
Avo.configure do |config|
config.body_classes = -> {
classes = []
classes << "admin-mode" if current_user&.admin?
classes << "dark-preference" if request.cookies["theme"] == "dark"
classes
}
end
```
## Page titles
When you want to update the page title for a custom tool or page, you only need to assign a value to the `@page_title` instance variable in the controller method.
```ruby{3}
class Avo::ToolsController < Avo::ApplicationController
def custom_tool
@page_title = "Custom tool page title"
end
end
```
Avo uses the [meta-tags](https://github.com/kpumuk/meta-tags) gem to compile and render the page title.
## Home path
When a user clicks your logo inside Avo or goes to the `/avo` URL, they will be redirected to one of your resources. You might want to change that path to something else, like a custom page. You can do that with the `home_path` configuration.
```ruby{2}
Avo.configure do |config|
config.home_path = "/avo/dashboard"
end
```
### Use a lambda function for the home_path
You can also use a lambda function to define that path.
```ruby{2}
Avo.configure do |config|
config.home_path = -> { avo_dashboards.dashboard_path(:dashy) }
end
```
When you configure the `home_path` option, the `Get started` sidebar item will be hidden in the development environment.
Now, users will be redirected to `/avo/dashboard` whenever they click the logo. You can use this configuration option alongside the `set_initial_breadcrumbs` option to create a more cohesive experience.
```ruby{2-5}
Avo.configure do |config|
config.home_path = "/avo/dashboard"
config.set_initial_breadcrumbs do
add_breadcrumb "Dashboard", "/avo/dashboard"
end
end
```
## Mount Avo under a nested path
You may need to mount Avo under a nested path, something like `/uk/admin`. In order to do that, you need to consider a few things.
1. Move the engine mount point below any route for custom tools.
```ruby{7,10}
Rails.application.routes.draw do
# other routes
authenticate :user, ->(user) { user.is_admin? } do
scope :uk do
scope :admin do
get "dashboard", to: "avo/tools#dashboard" # custom tool added before engine
end
mount_avo # engine mounted last
end
end
end
```
2. The `root_path` configuration should only be the last path segment.
```ruby
# π« Don't add the scope to the root_path
Avo.configure do |config|
config.root_path = "/uk/admin"
end
# β
Do this instead
Avo.configure do |config|
config.root_path = "/admin"
end
```
3. Use full paths for other configurations.
```ruby
Avo.configure do |config|
config.home_path = "/uk/admin/dashboard"
config.set_initial_breadcrumbs do
add_breadcrumb "Dashboard", "/uk/admin/dashboard"
end
end
```
## Custom `view_component` path
You may not keep your view components under `app/components` and want the generated field `view_component`s to be generated in your custom directory. You can change that using the `view_component_path` configuration key.
```ruby
Avo.configure do |config|
config.view_component_path = "app/frontend/components"
end
```
## Custom query scopes
You may want to change Avo's queries to add sorting or use gems like [friendly](https://github.com/norman/friendly_id).
You can do that using `index_query` for multiple records and `find_record_method` when fetching one record.
### Custom scope for `Index` page
Using `index_query` you tell Avo how to fetch the records for the `Index` view.
```ruby
class Avo::Resources::User < Avo::BaseResource
self.index_query = -> {
query.order(last_name: :asc)
}
end
```
### Custom find method for `Show` and `Edit` pages
Using `find_record_method` you tell Avo how to fetch one record for `Show` and `Edit` views and other contexts where a record needs to be fetched from the database.
This is very useful when you use something like `friendly` gem, custom `to_param` methods on your model, and even the wonderful `prefix_id` gem.
#### Custom `to_param` method
The following example shows how you can update the `to_param` (to use the post name) method on the `User` model to use a custom attribute and then update the `Avo::Resources::User` so it knows how to search for that model.
::: code-group
```ruby [app/avo/resources/post.rb]
class Avo::Resource::Post < Avo::BaseResource
self.find_record_method = -> {
# When using friendly_id, we need to check if the id is a slug or an id.
# If it's a slug, we need to use the find_by_slug method.
# If it's an id, we need to use the find method.
# If the id is an array, we need to use the where method in order to return a collection.
if id.is_a?(Array)
id.first.to_i == 0 ? query.where(slug: id) : query.where(id: id)
else
id.to_i == 0 ? query.find_by_slug(id) : query.find(id)
end
}
end
```
```ruby [app/models/post.rb]
class Post < ApplicationRecord
before_save :update_slug
def to_param
slug || id
end
def update_slug
self.slug = name.parameterize
end
end
```
:::
#### Using the `friendly` gem
::: code-group
```ruby [app/avo/resources/user.rb]
class Avo::Resources::User < Avo::BaseResource
self.find_record_method = -> {
if id.is_a?(Array)
query.where(slug: id)
else
# We have to add .friendly to the query
query.friendly.find id
end
}
end
```
```ruby [app/models/user.rb]
class User < ApplicationRecord
extend FriendlyId
friendly_id :name, use: :slugged
end
```
:::
#### Using `prefixed_ids` gem
You really don't have to do anything on Avo's side for this to work. You only need to add the `has_prefix_id` the model as per the documentation. Avo will know how to search for the record.
```ruby
class Course < ApplicationRecord
has_prefix_id :course
end
```
## Customize profile name, photo, and title
You might see on the sidebar footer a small profile widget. The widget displays three types of information about the user; `name`, `photo`, and `title`.
### Customize the name of the user
Avo checks to see if the object returned by your `current_user_method` responds to a `name` method. If not, it will try the `email` method and then fall back to `Avo user`.
### Customize the profile photo
Similarly, it will check if that current user responds to `avatar` and use that as the `src` of the photo.
### Customize the title of the user
Lastly, it will check if it responds to the `avo_title` method and uses that to display it under the name.
### Customize the sign-out link
Please follow this guide in authentication.
## Skip show view
In the CRUD interface Avo adds the view by default. This means that when your users will see the view icon to go to that detail page and they will be redirected to the page when doing certain tasks (update a record, run an action, etc.).
You might not want that behavior and you might not use the view at all and prefer to skip that and just use the view.
Adding `config.skip_show_view = true` to your `avo.rb` configuration file will tell Avo to skip it and use the view as the default resource view.
```ruby{3}
# config/initializers/avo.rb
Avo.configure do |config|
config.skip_show_view = true
end
```
## Logger
You may want to set a different output stream for avo logs, you can do that by returning it on a `config.logger` Proc
```ruby
## == Logger ==
config.logger = -> {
file_logger = ActiveSupport::Logger.new(Rails.root.join("log", "avo.log"))
file_logger.datetime_format = "%Y-%m-%d %H:%M:%S"
file_logger.formatter = proc do |severity, time, progname, msg|
"[Avo] #{time}: #{msg}\n".tap do |i|
puts i
end
end
file_logger
}
```
`default_url_options` is a Rails [controller method](https://apidock.com/rails/ActionController/Base/default_url_options) that will append params automatically to the paths you generate through path helpers.
In order to implement some features like route-level Multitenancy we exposed an API to add to Avo's `default_url_options` method.
::: code-group
```ruby [config/initializers/avo.rb]{2}
Avo.configure do |config|
config.default_url_options = [:account_id]
end
```
```ruby [app/config/routes.rb]{3}
Rails.application.routes.draw do
# Use to test out route-based multitenancy
scope "/account/:account_id" do
mount_avo
end
end
```
:::
Now, when you visit `https://example.org/account/adrian/avo`, the `account_id` param is `adrian` and it will be appended to all path helpers.
You may want to configure how turbo behave on Avo.
You can configure it using `config.turbo` option on `avo.rb` initializer
Supported options with default values:
```ruby
config.turbo = -> do
{
instant_click: true
}
end
```
You can configure the default pagination settings key by key.
```ruby
config.pagination = {
type: :countless
}
# Or
config.pagination = -> do
{
type: :countless,
}
end
```
This will make all your application's tables countless keeping the size key / value as the default one.
Verify all possible options here.
This setting allows your users to click on a record to navigate to its view.
:::warning
This interaction (clicking a `tr` element to behave as a link) is not natively supported in HTML.
Avo enhances this functionality with JavaScript, which may lead to side effects. Please report any issues you encounter on our [issue queue](https://avo.cool/new-issue).
:::
Enable this setting by using the `click_row_to_view_record` configuration option.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.click_row_to_view_record = true
end
```
## Associations lookup list limit
By default, there is a limit of a 1000 records per query when listing the association options. This limit ensures that the page will not crash due to large collections.
Use `associations_lookup_list_limit` configuration to change the limit value.
```ruby{3}
# config/initializers/avo.rb
Avo.configure do |config|
config.associations_lookup_list_limit = 1000
end
```
The message `There are more records available.` is shown when the limit is reached. To localize the message you can use `I18n.translate("avo.more_records_available")`.
Using searchable is recommended for listing unlimited records with better performance and user experience.
### Persistent UI State Configuration
#### Overview
The `persistence` configuration enables retention of specific UI settings, such as pagination and static filters, across user interactions.
---
#### Configuration
By default, the `:driver` is `nil`, which means no persistence is applied. You can configure the `:driver` for persistence as follows:
```ruby
Avo.configure do |config|
config.persistence = {
driver: :session
}
# Or with a dynamic block
config.persistence = -> do
{
driver: :session
}
end
end
```
---
#### Behavior
When enabled, the `persistence` configuration ensures the following:
1. **Associations Pagination**
The pagination state (e.g., `page` and `per_page` settings) for association tables (e.g., `has_many` fields) is retained across requests.
2. **Static Filters**
Static filter selections applied by users are preserved during their session.
---
#### How It Works
Setting `:driver` to `:session` stores the UI state in the user session, enabling it to persist while the session remains active.
---
:::warning
**Important**:
To prevent issues with session storage limits, avoid relying solely on the default **cookie store** for session management. The **cookie store** in Rails has a size limit of 4096 bytes. Storing multiple pagination states and filter settings may exceed this limit, resulting in an `ActionDispatch::Cookies::CookieOverflow` error.
:::
#### Recommended Session Store
To mitigate potential storage overflow, it is advisable to use a more scalable session store, such as:
- **Redis Store**
- **MemCache Store**
For detailed guidance, refer to the [Rails session store configuration](https://guides.rubyonrails.org/v8.0/configuring.html#config-session-store).
---
By adopting the `persistence` configuration with a suitable session store, you can ensure a seamless user experience.
Specifies the duration (in milliseconds) for which alerts remain visible before automatically dismissing.
A lower value results in quicker dismissal, while a higher value keeps the alert on screen for longer.
### Default value
`5000`
### Example
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.alert_dismiss_time = 8000
end
```
Defines the default sorting option for the fields on the index view.
### Default value
`:desc`
### Possible values
- `:asc`
- `:desc`
### Example
```ruby{3}
# config/initializers/avo.rb
Avo.configure do |config|
config.first_sorting_option = :asc
end
```
Defines which status items to exclude from the status page (`/avo_private/status`). This is useful for hiding sensitive information like license keys from the status page.
### Default value
`[]`
### Possible values
An array of strings or a callable that returns an array of strings representing the status items to exclude.
### Example
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.exclude_from_status = ["license_key"]
# OR using a callable
config.exclude_from_status = -> do
["license_key"]
end
end
```
### Common use case
The most common use case is to exclude the `license_key` from being displayed on the status page for security reasons.
Controls whether Avo sends usage metadata to Avo HQ, such as fields count, resources count, and other relevant metrics. This helps the Avo team understand how the framework is being used.
:::info
This option only takes effect on the community tier. Any other paid tier always sends metadata.
:::
### Default value
`true`
### Example
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.send_metadata = false
end
```
---
# Eject
If you want to change one of Avo's built-in views, you can eject it, update it and use it in your admin panel.
:::warning
Once ejected, the views will not receive updates on new Avo releases. You must maintain them yourself.
:::
Utilize the `--partial` option when you intend to extract certain partial
## Prepared templates
We prepared a few templates to make it easier for you.
`bin/rails generate avo:eject --partial :head` will eject the `_head.html.erb` partial.
```
βΆ bin/rails generate avo:eject --partial :head
Running via Spring preloader in process 20947
create app/views/avo/partials/_head.html.erb
```
A list of prepared templates:
- `:head` β‘οΈ `app/views/avo/partials/_head.html.erb`
- `:header` β‘οΈ `app/views/avo/partials/_header.html.erb`
- `:scripts` β‘οΈ `app/views/avo/partials/_scripts.html.erb`
- `:sidebar_extra` β‘οΈ `app/views/avo/partials/_sidebar_extra.html.erb`
### Logo
In the `app/views/avo/partials` directory, you will find the `_logo.html.erb` partial, which you may customize however you want. It will be displayed in place of Avo's logo.
### Header
The `_header.html.erb` partial enables you to customize the name and link of your app.
### Scripts
The `_scripts.html.erb` partial enables you to insert scripts in the footer of your admin.
## Eject any template
You can eject any partial from Avo using the partial path.
```
βΆ bin/rails generate avo:eject --partial app/views/layouts/avo/application.html.erb
create app/views/layouts/avo/application.html.erb
```
You can eject any view component from Avo using the `--component` option.
```bash
$ bin/rails generate avo:eject --component Avo::Index::TableRowComponent
```
or
```bash
$ bin/rails generate avo:eject --component avo/index/table_row_component
```
Have the same output:
```bash
create app/components/avo/index/table_row_component.rb
create app/components/avo/index/table_row_component.html.erb
```
With `--field-components` option is easy to eject, one or multiple field components. Notice that without using the `--scope`, the ejected components will override the original components for that field everywhere on the project.
Check the `--scope` and the `components` field options for more details on how to override the components only on specific parts of the project.
```bash
$ rails g avo:eject --field-components text
create app/components/avo/fields/text_field
create app/components/avo/fields/text_field/edit_component.html.erb
create app/components/avo/fields/text_field/edit_component.rb
create app/components/avo/fields/text_field/index_component.html.erb
create app/components/avo/fields/text_field/index_component.rb
create app/components/avo/fields/text_field/show_component.html.erb
create app/components/avo/fields/text_field/show_component.rb
```
Let's say you want to override only the edit component of the `TextField`, that can be achieved with this simple command.
```bash
$ rails g avo:eject --field-components text --view edit
create app/components/avo/fields/text_field/edit_component.rb
create app/components/avo/fields/text_field/edit_component.html.erb
```
While utilizing the `--field-components` option, you can selectively extract a specific view using the `--view` parameter, as demonstrated in the example above. If this option is omitted, all components of the field will be ejected.
When you opt to eject a view component that exists under `Avo::Views` or a field component under `Avo::Fields` namespace, for example the `Avo::Views::ResourceIndexComponent` or `Avo::Fields::TextField::ShowComponent` you can employ the `--scope` option to specify the namespace that should be adopted by the ejected component, extending from `Avo::Views` / `Avo::Fields`.
```bash
$ rails g avo:eject --component Avo::Views::ResourceIndexComponent --scope admins
create app/components/avo/views/admins/resource_index_component.rb
create app/components/avo/views/admins/resource_index_component.html.erb
$ rails g avo:eject --field-components text --view show --scope admins
create app/components/avo/fields/admins/text_field/show_component.rb
create app/components/avo/fields/admins/text_field/show_component.html.erb
```
The ejected file have the same code that original `Avo::Views::ResourceIndexComponent` or `Avo::Fields::TextField::ShowComponent` but you can notice that the class name and the directory has changed
```ruby
class Avo::Views::Admins::ResourceIndexComponent < Avo::ResourceComponent
class Avo::Fields::Admins::TextField::ShowComponent < Avo::Fields::ShowComponent
```
:::info Scopes transformation
`--scope users_admins` -> `Avo::Views::UsersAdmins::ResourceIndexComponent`
`--scope users/admins` -> `Avo::Views::Users::Admins::ResourceIndexComponent`
:::
You can eject any Avo controller using the `--controller` option. Once ejected, you'll be responsible for maintaining the ejected controller.
```bash
$ rails g avo:eject --controller application_controller
```
The most common use case is ejecting the `application_controller`. The ejected application controller serves as an extendable layer that inherits from `Avo::BaseApplicationController`, where the core logic resides. All your Avo controllers for a specific resource inherit from this extendable layer, allowing you to customize behavior that applies to all resources' controllers.
This approach provides a clean way to add custom functionality or override default behaviors without directly modifying Avo's base controllers.
---
# Custom view types
Avo ships with three built-in view types for the resource index: **table**, **grid**, and **map**. You can restrict which ones are available per-resource, or create entirely new view types through plugins.
## Restricting available view types
By default, Avo displays all the configured view types on the view switcher. For example, if you have `map_view` and `grid_view` configured, both of them, along with the `table_view`, will be available on the view switcher.
However, there might be cases where you only want to make a specific view type available without removing the configurations for other view types. This can be achieved using the `view_types` class attribute on the resource. Note that when only one view type is available, the view switcher will not be displayed.
```ruby{3}
class Avo::Resources::City < Avo::BaseResource
# ...
self.view_types = :table
#...
end
```
If you want to make multiple view types available, you can use an array. The icons on the view switcher will follow the order in which they are declared in the configuration.
```ruby{3}
class Avo::Resources::City < Avo::BaseResource
# ...
self.view_types = [:table, :grid]
#...
end
```
You can also dynamically restrict the view types based on user roles, params, or other business logic. To do this, assign a block to the `view_types` attribute. Within the block, you'll have access to `resource`, `record`, `params`, `current_user`, and other default accessors provided by `ExecutionContext`.
```ruby{3-9}
class Avo::Resources::City < Avo::BaseResource
# ...
self.view_types = -> do
if current_user.is_admin?
[:table, :grid]
else
:table
end
end
#...
end
```
## Creating a custom view type through a plugin
You can register entirely new view types from a Rails Engine (Avo plugin). The view type will appear in the view switcher alongside the built-in ones and can be set as the default for any resource.
The process has three parts: **create the component**, **register the view type**, and **configure a resource to use it**.
### 1. Create the view type component
Every view type is a ViewComponent that inherits from `Avo::ViewTypes::BaseViewTypeComponent`. The base class provides these props automatically:
| Prop | Description |
| ----------------- | ------------------------------------------------------------- |
| `resources` | Array of Avo resource wrappers (call `.record` for the model) |
| `resource` | The Avo resource class |
| `pagy` | Pagination object |
| `query` | The current query |
| `turbo_frame` | The Turbo Frame ID |
| `index_params` | Current index parameters |
| `reflection` | Association reflection (if nested) |
| `parent_record` | Parent record (if nested) |
| `parent_resource` | Parent resource (if nested) |
| `actions` | Available actions |
Create your component class inside your engine's namespace:
```ruby
# app/components/my_plugin/view_types/timeline_view_type_component.rb
class MyPlugin::ViewTypes::TimelineViewTypeComponent < Avo::ViewTypes::BaseViewTypeComponent # [!code focus]
def grouped_resources
@resources.group_by { |r| r.record.created_at.to_date }
end
def empty?
@resources.blank?
end
end
```
Then create the template. You have full control over the HTML β render items however you like and include the paginator at the bottom:
```erb
<%# app/components/my_plugin/view_types/timeline_view_type_component.html.erb %>
<% if empty? %>
No records found.
<% else %>
<% grouped_resources.each do |date, resources| %>
<%= date.strftime("%B %d, %Y") %>
<% resources.each do |resource| %>
<%= resource.record.title %>
<% end %>
<% end %>
<% end %>
<%= render paginator_component %>
```
:::info
The `paginator_component` method is inherited from the base class. Always render it to keep pagination working.
:::
### 2. Register the view type
In your engine's initializer, register the view type with `Avo.plugin_manager.register_view_type`. This must happen inside the `ActiveSupport.on_load(:avo_boot)` hook so Avo core is loaded first.
```ruby
# lib/my_plugin/engine.rb
module MyPlugin
class Engine < ::Rails::Engine
initializer "my_plugin.init" do
ActiveSupport.on_load(:avo_boot) do
Avo.plugin_manager.register "my_plugin" # [!code focus:5]
Avo.plugin_manager.register_view_type :timeline,
component: "MyPlugin::ViewTypes::TimelineViewTypeComponent",
icon: "tabler/outline/timeline-event",
active_icon: "tabler/filled/timeline-event"
end
end
end
end
```
`register_view_type` accepts these options:
| Option | Required | Description |
| ----------------- | -------- | ----------------------------------------------------- |
| `component` | Yes | Component class or string (auto-constantized) |
| `icon` | Yes | Icon path for the inactive state in the view switcher |
| `active_icon` | Yes | Icon path for the active state in the view switcher |
| `translation_key` | No | I18n key for the view type name in tooltips |
:::info
The `component` can be passed as a string (`"MyPlugin::ViewTypes::TimelineViewTypeComponent"`) or as the class itself. Strings are constantized at render time, which avoids load-order issues during boot.
:::
### 3. Configure a resource to use it
Once registered, you can use your custom view type in any resource:
```ruby
class Avo::Resources::Event < Avo::BaseResource
self.default_view_type = :timeline # [!code focus:2]
self.view_types = [:table, :timeline]
# ... fields
end
```
Setting `default_view_type` makes your view type the one users see first. Including `:table` in `view_types` keeps the table view available as a fallback via the view switcher.
## How it works under the hood
When a user visits a resource index, Avo resolves the current view type through the `ViewTypeManager`:
1. The `ViewTypeManager` holds a registry of all view types (built-in + plugin-registered)
2. It looks up the component class for the current view type via `component_for(name)`
3. The `ResourceListingComponent` renders that component with all the standard props
4. The view switcher partial reads the registry for icons and renders toggle buttons for each available view type
The view type is persisted in the URL as the `view_type` query parameter, so it survives page reloads and can be bookmarked.
## Full example: avo-notifications
The `avo-notifications` gem ships a `:notification` view type as a real-world reference. Here's how it's wired up:
**Registration** in the engine:
```ruby
# lib/avo/notifications/engine_content.rb
Avo.plugin_manager.register_view_type :notification,
component: "Avo::Notifications::ViewTypes::NotificationViewTypeComponent",
icon: "tabler/outline/bell",
active_icon: "tabler/filled/bell"
```
**Component** inherits from the base and adds domain logic (time grouping, unread counts):
```ruby
# app/components/avo/notifications/view_types/notification_view_type_component.rb
class Avo::Notifications::ViewTypes::NotificationViewTypeComponent < Avo::ViewTypes::BaseViewTypeComponent
def grouped_resources
@resources.group_by { |resource| time_group(resource.record.created_at) }
end
def unread_count
@resources.count { |resource| user_unread?(resource.record) }
end
# ...
end
```
**Resource** sets it as the default:
```ruby
# app/avo/resources/avo_notification.rb
class Avo::Resources::AvoNotification < Avo::BaseResource
self.default_view_type = :notification
self.view_types = [:table, :notification]
end
```
## Adding styles
If your view type needs custom CSS, add it to your engine's stylesheet. Follow BEM methodology with Tailwind `@apply` directives:
```css
/* app/assets/stylesheets/my-plugin/application.css */
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);
@layer components {
.timeline-view__item {
@apply flex gap-3 px-5 py-3.5 transition-colors;
&:hover {
@apply bg-gray-50;
}
}
}
```
Then register the stylesheet in your engine initializer:
```ruby
Avo.asset_manager.add_stylesheet "my-plugin/application"
```
## Adding interactivity with Stimulus
For client-side behavior (filtering, toggling, etc.), create a Stimulus controller in your engine and register it:
```javascript
// app/javascript/controllers/my_filter_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["item"];
static values = { filter: { type: String, default: "all" } };
applyFilter() {
this.itemTargets.forEach((item) => {
item.toggleAttribute("hidden", !this.shouldShow(item));
});
}
shouldShow(item) {
if (this.filterValue === "all") return true;
return item.dataset.active === "true";
}
}
```
```javascript
// app/javascript/controllers/index.js
import MyFilterController from "./my_filter_controller";
const application = window.Stimulus;
application.register("my-filter", MyFilterController);
```
Then use it in your template with `data-controller="my-filter"` and `data-action` attributes. Use the `hidden` HTML attribute (not CSS classes) for toggling visibility.
---
# Menu editor
One common task you need to do is organize your sidebar resources into menus. You can easily do that using the menu editor in the initializer.
When you start with Avo, you'll get an auto-generated sidebar by default. That sidebar will contain all your resources, dashboards, and custom tools. To customize that menu, you have to add the `main_menu` key to your initializer.
```ruby{3-20}
# config/initializers/avo.rb
Avo.configure do |config|
config.main_menu = -> {
section "Resources", icon: "tabler/outline/building-store", collapsable: false do
group "Company", collapsable: true do
resource :projects, path: "/admin/resources/projects" do
link "First project", active: :inclusive, path: "/admin/resources/projects/1"
link "Second project", active: :inclusive, path: "/admin/resources/projects/2"
end
resource :team, icon: "heroicons/outline/user-group"
resource :team_membership
resource :reviews, icon: "heroicons/outline/star"
end
end
section I18n.t('avo.other'), icon: "heroicons/outline/finger-print", collapsable: true, collapsed: true do
link_to 'Avo HQ', path: 'https://avohq.io', target: :_blank
link_to 'Jumpstart Rails', path: 'https://jumpstartrails.com/', target: :_blank
end
}
end
```
For now, Avo supports editing only two menus, `main_menu` and `profile_menu`. However, that might change in the future by allowing you to write custom menus for other parts of your app.
## Menu item types
A few menu item types are supported: `link_to`, `section`, `group`, `resource`, `dashboard`, and `subitems`. There are a few helpers too, like `all_resources`, `all_dashboards`, and `all_tools`.
The recommended hierarchy is `section β group β resource β subitem`. Sections are the top-level containers rendered with an icon header in the sidebar.
`link_to` is the menu item that the user will probably interact with the most. It will generate a link on your menu. You can specify the `name`, `path` , and `target`.
```ruby
link_to "Google", path: "https://google.com", target: :_blank
```
When you add the `target: :_blank` option, a tiny external link icon will be displayed.
#### `link_to` options
#### `path`
This is the path of the item.
It may be ommited to make the API look like Rail's
```ruby
config.main_menu = -> {
# These two are equivalent
link_to "Home", path: main_app.root_path
link_to "Home", main_app.root_path
}
```
##### `data`
You may add arbitraty `data` attributes to your link.
You can make a link execute a `put`, `post`, or `delete` request similar to how you use the `data-turbo-method` attribute.
```ruby
config.main_menu = -> {
link_to "Sign out!", main_app.destroy_user_session_path, data: { turbo_method: :delete }
}
```
The `render` method will render renderable objects like partials or View Components.
You can even pass `locals` to partials.
The partials follow the same pattern as the regular `render` method.
```ruby
render "avo/sidebar/items/custom_tool"
render "avo/sidebar/items/custom_tool", locals: { something: :here }
render Super::Dooper::Component.new(something: :here)
```
To make it a bit easier, you can use `resource` to quickly generate a link to one of your resources. For example, you can pass a short symbol name `:user` or the full name `Avo::Resources::User`.
```ruby
resource :posts
resource "Avo::Resources::Comments"
```
You can also change the label for the `resource` items to something else.
```ruby
resource :posts, label: "News posts"
```
Additionally, you can pass the `params` option to the `resource` items to add query params to the link.
```ruby
resource :posts, params: { status: "published" }
resource :users, params: -> do
decoded_filter = {"Avo::Filters::IsAdmin"=>["non_admins"]}
{ encoded_filters: Avo::Filters::BaseFilter.encode_filters(decoded_filter)}
end
```
### Subitems
You can add sub-links beneath a resource by passing a block. These appear as child items under the resource link in the sidebar and are useful for linking to filtered views, specific records, or nested paths.
```ruby
resource :projects, path: "/admin/resources/projects" do
link "First project", active: :inclusive, path: "/admin/resources/projects/1"
link "Second project", active: :inclusive, path: "/admin/resources/projects/2"
end
```
The `active` option controls when the sub-link is highlighted as active:
- `:inclusive` β the link is active when the current path starts with the given `path` (useful for nested routes)
- `:exclusive` β the link is active only on an exact path match (default)
`subitems` is an optional wrapper you can use inside a `resource` block to make the sub-links more explicit and readable. It is functionally equivalent to writing links directly in the block. Note that `subitems` and the `link` items within it do not support the `icon` option.
```ruby
# These two are equivalent
resource :projects do
link "New project", path: "/admin/resources/projects/new"
link "All projects", path: "/admin/resources/projects"
end
resource :projects do
subitems do
link "New project", path: "/admin/resources/projects/new"
link "All projects", path: "/admin/resources/projects"
end
end
```
Similar to `resource`, this is a helper to make it easier to reference a dashboard. You pass in the `id` or the `name` of the dashboard.
```ruby
dashboard :dashy
dashboard "Sales"
```
You can also change the label for the `dashboard` items to something else.
```ruby
dashboard :dashy, label: "Dashy Dashboard"
```
Sections are the **top-level containers** in the sidebar. They are rendered with a prominent header that includes an `icon` and a `name`. Sections are intended to group related `group`s and items at the highest level of the menu.
```ruby
section "Resources", icon: "heroicons/outline/academic-cap" do
group "Academia", collapsable: true do
resource :course
resource :course_link
end
group "Blog", collapsable: true, collapsed: true do
resource :posts
resource :comments
end
end
```
You can also place items directly inside a section without a group:
```ruby
section "Tools", icon: "heroicons/outline/finger-print" do
all_tools
end
```
Groups are **sub-categories** nested inside sections. They render as a collapsable label and are used to cluster related items within a section. Groups support `collapsable` and `collapsed` options. Note that groups do not support the `icon` option.
```ruby
section "Resources", icon: "heroicons/outline/academic-cap" do
group "Blog", collapsable: true, collapsed: true do
resource :posts
resource :categories
resource :comments
end
end
```
Groups can also be placed at the top level without a parent section, but the recommended structure is to nest them inside sections.
Renders all resources, except those explicitly excluded.
#### Arguments:
- `except`: *(Array, optional)* β A list of resource names to be excluded.
#### Example:
```ruby
section "App", icon: "heroicons/outline/beaker" do
group "Resources" do
all_resources except: [:users, :orders]
end
end
```
In the example above, all resources will be rendered except `Avo::Resources::Users` and `Avo::Resources::Orders`.
Renders all dashboards, except those explicitly excluded.
#### Arguments:
- `except`: *(Array, optional)* β A list of dashboard names to be excluded.
#### Example:
```ruby
section "App", icon: "heroicons/outline/beaker" do
group "Dashboards" do
all_dashboards except: [:sales, :analytics]
end
end
```
In this example, all dashboards will be rendered except `Avo::Resources::Sales` and `Avo::Resources::Analytics`.
Renders all tools.
```ruby
section "App", icon: "heroicons/outline/beaker" do
group "All tools" do
all_tools
end
end
```
### `all_` helpers
```ruby
section "App", icon: "heroicons/outline/beaker" do
group "Dashboards" do
all_dashboards
end
group "Resources" do
all_resources
end
group "All tools" do
all_tools
end
end
```
:::warning
The `all_resources` helper is taking into account your authorization rules, so make sure you have `def index?` enabled in your resource policy.
:::
## Item visibility
The `visible` option is available on all menu items. It can be a boolean or a block that has access to a few things:
- the `current_user`. Given that you set a way for Avo to know who the current user is, that will be available in that block call
- the `context` object.
- the `params` object of that current request
- the [`view_context`](https://apidock.com/rails/AbstractController/Rendering/view_context) object. The `view_context` object lets you use the route helpers. eg: `view_context.main_app.posts_path`.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.main_menu = -> {
resource :user, visible: -> do
context[:something] == :something_else
end
}
end
```
## Add `data` attributes to items
You may want to add special data attributes to some items and you can do that using the `data` option. For example you may add `data: {turbo: false}` to make a regular request for a link.
```ruby{4}
# config/initializers/avo.rb
Avo.configure do |config|
config.main_menu = -> {
resource :user, data: {turbo: false}
}
end
```
## Using authorization rules
When you switch from a generated menu to a custom one, you might want to keep using the same authorization rules as before. To quickly do that, use the `authorize` method in the `visible` option.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.main_menu = -> {
resource :team, visible: -> do
# authorize current_user, MODEL_THAT_NEEDS_TO_BE_AUTHORIZED, METHOD_THAT_NEEDS_TO_BE_AUTHORIZED
authorize current_user, Team, "index?", raise_exception: false
end
}
end
```
## Icons
The `icon` option is supported on `section` and on individual menu items (`link_to`, `resource`, `dashboard`). It is not supported on `group` or `subitems` (including links within subitems).
You can use icons from [Heroicons](https://heroicons.com/) (both `outline` and `solid` variants) or from [Tabler Icons](https://tabler.io/icons) (preferred in Avo 4).
```ruby
section "Resources", icon: "heroicons/solid/academic-cap" do
group "Blog" do
resource :posts, icon: "heroicons/outline/academic-cap"
end
end
section "Resources", icon: "heroicons/solid/finger-print" do
resource :course, icon: "heroicons/outline/finger-print"
end
section "Resources", icon: "heroicons/solid/adjustments" do
resource :course, icon: "heroicons/outline/adjustments"
end
```
### Icons on resource, dashboard, and link_to
In addition to sections, you can add icons to `resource`, `dashboard`, and `link_to` items.
```ruby
link_to "Avo", "https://avohq.io", icon: "globe"
```
## Keyboard shortcuts on menu items
Any menu item β `resource`, `link`, or `dashboard` β accepts a `hotkey:` option. When set, Avo renders a `` badge next to the label and registers the key binding so users can jump straight to that item from anywhere in the admin panel.
```ruby
Avo.configure do |config|
config.main_menu = -> {
section "Content", icon: "tabler/outline/files" do
resource :post, hotkey: "g p"
resource :category, hotkey: "g c"
link "Analytics", path: "/avo/analytics", hotkey: "g a"
end
}
end
```
The hotkey string follows [@github/hotkey](https://github.com/github/hotkey) syntax. Use space-separated keys for sequences (e.g. `"g p"` means press g then p ).
For `resource` items you can also set the hotkey on the resource class itself, which acts as a fallback when no `hotkey:` is passed to the menu item:
```ruby
class Avo::Resources::Post < Avo::BaseResource
self.hotkey = "g p"
end
```
Keyboard shortcuts β full reference for built-in shortcuts and patterns
## Collapsable sections and groups
Both `section` and `group` support the `collapsable` option. When enabled, an arrow icon is added to indicate the item can be collapsed. The collapsed/expanded state is stored in the browser's Local Storage and remembered across page loads.
```ruby
section "Resources", icon: "heroicons/outline/academic-cap", collapsable: true do
group "Blog", collapsable: true do
resource :posts
resource :comments
end
end
```
### Default collapsed state
You can set a default collapsed state using the `collapsed` option. This only takes effect the first time a user visits β once they have a stored preference, that preference takes priority.
```ruby
section "Resources", icon: "heroicons/outline/academic-cap", collapsable: true, collapsed: true do
group "Blog", collapsable: true, collapsed: true do
resource :posts
resource :comments
end
end
```
You might want to allow your users to hide certain items from view.
## Authorization
If you use the authorization feature, you will need an easy way to authorize your items in the menu builder.
For that scenario, we added the `authorize` helper.
```ruby{3}
Avo.configure do |config|
config.main_menu = -> {
resource :team, visible: -> {
# authorize current_user, THE_RESOURCE_MODEL, THE_POLICY_METHOD, raise_exception: false
authorize current_user, Team, "index?", raise_exception: false
}
}
end
```
Use it in the `visible` block by giving it the `current_user` (which is available in that block), the class of the resource, the method that you'd like to authorize for (default is `index?`), and tell it not to throw an exception.
Now, the item visibility will use the `index?` method from the `TeamPolicy` class.
## Profile menu
The profile menu allows you to add items to the menu displayed in the profile component. **The sign-out link is automatically added for you.**
You may add the `icon` option to the `profile_menu` links.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.profile_menu = -> {
link_to "Profile", path: "/profile", icon: "user-circle"
}
end
```
## Forms in profile menu
It's common to have forms that `POST` to a path to do sign ut a user. For this scenario we added the `method` and `params` option to the profile item `link_to`, so if you have a custom sign out path you can do things like this.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.profile_menu = -> {
link_to "Sign out", path: main_app.destroy_user_session_path, icon: "user-circle", method: :post, params: {custom_param: :here}
}
end
```
## Custom content in the profile menu
You might, however, want to add a very custom form or more items to the profile menu. For that we prepared the `_profile_menu_extra.html.erb` partial for you.
```bash
bin/rails generate avo:eject --partial :profile_menu_extra
```
This will eject the partial and you can add whatever custom content you might need.
```erb
<%# Example link below %>
<%#= render Avo::ProfileItemComponent.new label: 'Profile', path: '/profile', icon: 'user-circle' %>
```
---
# Search
Finding what you're looking for fast is essential. That's why Avo recommends using [ransack's](https://github.com/activerecord-hackery/ransack) powerful query language. While we show you examples using `ransack`, you can use other search engines, `ransack` is **not mandatory**.
If you're choosing to use `ransack`, you need to add it as a dependency to your app.
```ruby
# Gemfile
gem "ransack"
```
## Enable search for a resource
To enable search for a resource, you need to configure the `search` class attribute to the resource file.
```ruby{2-4}
class Avo::Resources::User < Avo::BaseResource
self.search = {
query: -> { query.ransack(name_eq: q).result(distinct: false) }
}
end
```
The `query` block provides the `q` variable, which contains the stripped search query string, and the `query` variable on which you run the query. That ensures that the authorization scopes have been appropriately applied. If you need access to the unstripped query string, you can use `params[:q]` instead of `q`.
In this block, you may configure the search however strict or loose you need it. Check out [ransack's search matchers](https://github.com/activerecord-hackery/ransack#search-matchers) to compose the query better.
:::warning
If you're using ransack version 4 and up you must add `ransackable_attributes` and maybe more to your model in order for it to work. Read more about it [here](https://activerecord-hackery.github.io/ransack/going-further/other-notes/#authorization-allowlistingdenylisting).
:::
## Authorize search
Search is authorized in policy files using the `search?` method.
```ruby
class UserPolicy < ApplicationPolicy
def search?
true
end
end
```
If the `search?` method returns false, the search operation for that resource is not going to show up in the global search and the search box on index is not going to be displayed.
If you're using `search?` already in your policy file, you can alias it to some other method in you initializer using the `config.authorization_methods` config. More about that on the authorization page.
```ruby
Avo.configure do |config|
config.authorization_methods = {
search: 'avo_search?',
}
end
```
## Resource search
When a resource has the `search` attribute with a valid configuration, a new search input will be displayed on the `Index` view. When you perform a search, the current view (table, grid, map, or any other view type) will update to show only the matching results, maintaining the same visual format.
## Searching within associations
In some cases, you might need to search for records based on attributes of associated models. This can be achieved by adding a few things to the search query. Here's an example of how to do that:
Assuming you have two models, `Application` and `Client`, with the following associations:
```ruby{3,8}
# app/models/application.rb
class Application < ApplicationRecord
belongs_to :client
end
# app/models/client.rb
class Client < ApplicationRecord
has_many :applications
end
```
You can perform a search on `Application` records based on attributes of the associated `Client`. For example, searching by the client's email, name, or phone number:
```ruby{6,11-15}
# app/avo/resources/application.rb
class Avo::Resources::Application < Avo::BaseResource
self.search = {
query: -> {
query
.joins(:client)
.ransack(
id_eq: q,
name_cont: q,
workflow_name_cont: q,
client_id_eq: q,
client_first_name_cont: q,
client_last_name_cont: q,
client_email_cont: q,
client_phone_number_cont: q,
m: 'or'
).result(distinct: false)
}
}
end
```
In the above example, ransack is used to search for `Application` records based on various attributes of the associated `Client`, such as `client_email_cont` and `client_phone_number_cont`. The joins method is used to join the applications table with the clients table to perform the search efficiently.
This approach allows for flexible searching within associations, enabling you to find records based on related model attributes.
---
# Global Search
Avo has a powerful global search feature powered by Hotwire. It searches through all the resources that have the `search` attribute with a valid configuration.
The global search leverages the resource search configuration. Please refer to the resource search section for more information on how to configure the search for a resource.
You open the global search by clicking the trigger on the navbar or by using the Cmd + K keyboard shortcut (Ctrl + K on Windows). The search includes enhanced keyboard navigation:
- Ctrl + K or Cmd + K - Open global search
- Up and Down arrow keys - Navigate through search results
- Enter - Visit the selected record
- Esc - Close the search modal
The global search shows a limited number of quick results in the dropdown, with an option to view all matching results on a dedicated page without limits.
## Global configuration
Use the `global_search` configuration to enable/disable the feature and control related options.
```ruby{3-6}
# config/initializers/avo.rb
Avo.configure do |config|
config.global_search = {
enabled: true,
navigation_section: true,
}
end
```
Set `enabled: false` to hide the global search.
All configuration options can be set using a lambda. Within this block, you gain access to all attributes of `Avo::ExecutionContext`.
```ruby{3-6}
# config/initializers/avo.rb
Avo.configure do |config|
config.global_search = {
enabled: -> { current_user.is_admin? },
navigation_section: -> { current_user.is_admin? },
}
end
```
The `item` configuration is used to configure the item displayed in the search results. It is a hash with the following options:
| Option | Description | Default | Possible Values |
|--------|-------------|---------|-----------------|
| `title` | The title of the search result | Resource title | Any string |
| `description` | The description of the search result | `nil` | Any string |
| `image_url` | The URL of the image to display in the search result | `nil` | Any valid URL |
| `image_format` | The format of the image to display in the search result | `:square` | `:square`, `:rounded`, `:circle` |
| `path` | The path to redirect to when clicking the search result | Record's show page | Any valid path |
### Example with all configurations
```ruby{5-13}
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.search = {
query: -> { query.ransack(name_cont: q, body_cont: q, m: "or").result(distinct: false) },
item: -> do
{
title: "[#{record.id}] #{record.name}",
description: record.truncated_body,
image_url: main_app.url_for(record.cover_photo),
image_format: :rounded,
path: avo.resources_post_path(record, custom: "search")
}
end
}
end
```
You might have a resource that you'd like to be able to perform a search on when on its `Index` page but not have it present in the global search. You can hide it using `hide_on_global: true`.
```ruby{9}
class Avo::Resources::TeamMembership < Avo::BaseResource
self.search = {
query: -> { query.ransack(id_eq: q, m: "or").result(distinct: false) },
item: -> do
{
description: record.level,
}
end,
hide_on_global: true
}
end
```
By default, Avo displays 8 search results for each resource in the global search. You can change the number of results displayed by configuring the `search_results_count` option:
```ruby{3}
# config/initializers/avo.rb
Avo.configure do |config|
config.search_results_count = 16
end
```
You can also change the number of results displayed on individual resources:
```ruby{4}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
self.search = {
results_count: 5
query: -> {
# ...
},
}
end
```
You can also assign a lambda to dynamically set the value. Inside that block you have access to all attributes of the `Avo::ExecutionContext`.
```ruby{3}
class Avo::Resources::User < Avo::BaseResource
self.search = {
results_count: -> { user.admin? ? 30 : 10 }
}
end
```
If you configure `results_count` by specifying it in the resource file then that number takes precedence over the global [`search_results_count`](#search_results_count) for that resource.
By default, Avo displays the search results count for each resource in the global search. Example: "Users (8 of 21)". You can avoid counting the number of results by configuring the `display_count` option
This is useful if you have a custom search provider that doesn't return the number of results or if you want to avoid counting the number of results on large datasets.
```ruby{4}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
self.search = {
display_count: false
query: -> {
# ...
},
}
end
```
You can also assign a lambda to dynamically set the value. Inside that block you have access to all attributes of the `Avo::ExecutionContext`.
```ruby{4}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
self.search = {
display_count: -> { user.admin? }
}
end
```
## Scope out global or resource searches
You may want to perform different searches on the `global` search from the `resource` search. You may use the `params[:global]` flag to figure that out.
```ruby{5-11}
# app/avo/resources/order.rb
class Avo::Resources::Order < Avo::BaseResource
self.search = {
query: -> {
if params[:global]
# Perform global search
query.ransack(id_eq: q, m: "or").result(distinct: false)
else
# Perform resource search
query.ransack(id_eq: q, details_cont: q, m: "or").result(distinct: false)
end
}
}
end
```
## Custom search provider
You can use custom search providers like Elasticsearch.
In such cases, or when you want to have full control over the search results, the `query` block should return an array of hashes. Each hash should follow the structure below:
```ruby
{
_id: 1,
_label: "The label",
_url: "The URL",
_description: "Some description about the record", # only with Avo Pro and above
_avatar: "URL to an image that represents the record", # only with Avo Pro and above
_avatar_type: :rounded # or :circle or :square; only with Avo Pro and above
}
```
Example:
```ruby{2-10}
class Avo::Resources::Project < Avo::BaseResource
self.search = {
query: -> do
[
{ _id: 1, _label: "Record One", _url: "https://example.com/1" },
{ _id: 2, _label: "Record Two", _url: "https://example.com/2" },
{ _id: 3, _label: "Record Three", _url: "https://example.com/3" }
]
end
}
end
```
:::warning
Results count will not be available with custom search providers.
:::
---
# Localization (i18n)
Avo leverages Rails' powerful `I18n` translations module.
:::warning Multi-language URL Support
If you're serving Avo using multiple languages and you're using the locale in your routes (`/en/resources/users`, `/de/resources/users`), check out this guide.
:::
When you run `bin/rails avo:install`, Rails will not generate for you the `avo.en.yml` translation file. This file is already loaded will automatically be injected into the I18n translations module.
## Localizing resources
Let's say you want to localize a resource. All you need to do is add a `self.translation_key` class attribute in the `Resource` file. That will tell Avo to use that translation key to localize this resource. That will change the labels of that resource everywhere in Avo.
```ruby{4}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
self.title = :name
self.translation_key = 'avo.resource_translations.user'
end
```
```yaml{6-10}
# avo.es.yml
es:
avo:
dashboard: 'Dashboard'
# ... other translation keys
resource_translations:
user:
zero: 'usuarios'
one: 'usuario'
other: 'usuarios'
```
## Localizing fields
Similarly, you can even localize fields. All you need to do is add a `translation_key:` option on the field declaration.
```ruby{8}
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id
# ... other fields
field :files, as: :files, translation_key: 'avo.field_translations.file'
end
end
```
```yaml{6-10}
# avo.es.yml
es:
avo:
dashboard: 'Dashboard'
# ... other translation keys
field_translations:
file:
zero: 'archivos'
one: 'archivo'
other: 'archivos'
```
## Localizing buttons label
The `avo.save` configuration applies to all save buttons. If you wish to customize the localization for a specific resource, such as `Avo::Resources::Product`, you can achieve this by:
```yml
---
en:
avo:
resource_translations:
product:
save: "Save the product!"
```
## Setting the locale
Setting the locale for Avo is pretty simple. Just use the `config.locale = :en` config attribute. Default is `nil` and will fall back to whatever you have configured in as `config.i18n.default_locale` in `application.rb`.
```ruby{2}
Avo.configure do |config|
config.locale = :en # default is nil
end
```
That will change the locale only for Avo requests. The rest of your app will still use your locale set in `application.rb`. If you wish to change the locale for Avo, you can use the `set_locale=pt-BR` param. That will set the default locale for Avo until you restart your server.
Suppose you wish to change the locale only for one request using the `force_locale=pt-BR` param. That will set the locale for that request and keep the `force_locale` param in all links while you navigate Avo. Remove that param when you want to go back to your configured `default_locale`.
Related:
- Check out our guide for multilingual records.
## Customize the locale
If there's anything in the locale files that you would like to change, run `bin/rails generate avo:locales` to generate the locale files.
These provide a guide for you for when you want to add more languages.
If you do translate Avo in a new language please consider contributing it to the [main repo](https://github.com/avo-hq/avo). Thank you
## FAQ
If you try to localize your resources and fields and it doesn't seem to work, please be aware of the following.
### The I18n.t method defaults to the name of that field/resource
Internally the localization works like so `I18n.t(translation_key, count: 1, default: default)` where the `default` is the computed field/resource name. So check the structure of your translation keys.
```yaml
# config/locales/avo.pt-BR.yml
pt-BR:
avo:
field_translations:
file:
zero: 'arquivos'
one: 'arquivo'
other: 'arquivos'
resource_translations:
user:
zero: 'usuΓ‘rios'
one: 'usuΓ‘rio'
other: 'usuΓ‘rios'
```
### Using a Route Scope for Localization
To implement a route scope for localization within Avo, refer to this guide. It provides step-by-step instructions on configuring your routes to include a locale scope, enabling seamless localization handling across your application.
---
# Branding
Avo's branding feature lets you customize the look and feel of your admin panel β logos, colors, color scheme, and chart colors. It supports two modes: **static** (you choose the theme once) and **dynamic** (users can switch themes on the fly).
## Configuration
All branding options are configured through `config.branding` as a hash in your `config/initializers/avo.rb` file.
```ruby
Avo.configure do |config|
config.branding = {
logo: "my_company/logo.png",
logomark: "my_company/logomark.png",
favicon: "my_company/favicon.ico",
neutral: :slate,
accent: :blue
}
end
```
## Logos
### Desktop logo
The `logo` option sets the main logo displayed in the sidebar on desktop screens.
```ruby
config.branding = {
logo: "my_company/logo.png"
}
```
### Dark mode logo
Provide a `logo_dark` variant to display a different logo when the user is in dark mode.
```ruby
config.branding = {
logo: "my_company/logo.png",
logo_dark: "my_company/logo_dark.png"
}
```
### Mobile logomark
The `logomark` should be a square image used on smaller screens where the sidebar collapses.
```ruby
config.branding = {
logomark: "my_company/logomark.png",
logomark_dark: "my_company/logomark_dark.png"
}
```
### Favicon
Override the default favicon with your own `.ico` file. You can also provide a dark mode variant.
```ruby
config.branding = {
favicon: "my_company/favicon.ico",
favicon_dark: "my_company/favicon_dark.ico"
}
```
### Placeholder image
When a record doesn't have a cover image (e.g. in grid view), Avo shows a placeholder. You can customize it.
```ruby
config.branding = {
placeholder: "my_company/placeholder.svg"
}
```
## Neutral theme
The neutral theme controls the base surface and border colors throughout the UI. You can set it using a predefined palette name or a custom color hash.
### Predefined palettes
Choose from one of the built-in neutral palettes:
```ruby
config.branding = {
neutral: :slate
}
```
| Palette | Description |
| ---------- | ------------------------------------- |
| `:slate` | Cool blue-gray tones |
| `:stone` | Warm gray with a slight brown tint |
| `:gray` | Pure neutral gray |
| `:zinc` | Cool gray with a hint of blue |
| `:neutral` | Perfectly balanced gray |
| `:taupe` | Warm gray with earthy undertones |
| `:mauve` | Gray with a subtle purple cast |
| `:mist` | Light, airy blue-gray |
| `:olive` | Gray with green-yellow undertones |
### Custom neutral colors
Pass a hash of shade values (using oklch or any CSS color format) for full control:
```ruby
config.branding = {
neutral: {
25 => "oklch(0.99 0.01 240)",
50 => "oklch(0.97 0.01 240)",
100 => "oklch(0.94 0.01 240)",
200 => "oklch(0.88 0.02 240)",
300 => "oklch(0.80 0.02 240)",
400 => "oklch(0.68 0.02 240)",
500 => "oklch(0.55 0.02 240)",
600 => "oklch(0.45 0.02 240)",
700 => "oklch(0.37 0.02 240)",
800 => "oklch(0.27 0.02 240)",
900 => "oklch(0.20 0.02 240)",
950 => "oklch(0.14 0.02 240)"
}
}
```
You can also provide separate light and dark scales:
```ruby
config.branding = {
neutral: {
light: {
25 => "oklch(0.99 0.01 240)",
50 => "oklch(0.97 0.01 240)",
# ...
},
dark: {
25 => "oklch(0.14 0.01 240)",
50 => "oklch(0.18 0.01 240)",
# ...
}
}
}
```
## Accent color
The accent color is used for interactive elements like buttons, links, and highlights. Like neutrals, you can use a predefined color or a custom hash.
### Predefined accent colors
```ruby
config.branding = {
accent: :blue
}
```
Available accent colors: `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`.
### Custom accent colors
Provide three tokens β `color` (the main accent), `content` (text/icons on accent backgrounds), and `foreground` (alternative foreground):
```ruby
config.branding = {
accent: {
color: "oklch(0.6 0.2 260)",
content: "oklch(0.9 0.05 260)",
foreground: "oklch(1.0 0 0)"
}
}
```
With light/dark variants:
```ruby
config.branding = {
accent: {
light: {
color: "oklch(0.6 0.2 260)",
content: "oklch(0.9 0.05 260)",
foreground: "oklch(1.0 0 0)"
},
dark: {
color: "oklch(0.7 0.2 260)",
content: "oklch(0.3 0.05 260)",
foreground: "oklch(0.1 0 0)"
}
}
}
```
## Color scheme
Set the color scheme with `scheme`:
```ruby
config.branding = {
scheme: :auto # :auto | :light | :dark
}
```
| Value | Behavior |
| -------- | ------------------------------------------------------------ |
| `:auto` | Follows the user's system preference (default) |
| `:light` | Always starts in light mode |
| `:dark` | Always starts in dark mode |
Users can always toggle between light, dark, and auto using the color scheme switcher in the sidebar.
## Static vs. dynamic mode
### Static mode (default)
In static mode, you lock the neutral theme and accent color in the initializer. Users can still switch between light/dark/auto, but they cannot change the color palette.
```ruby
config.branding = {
mode: :static,
neutral: :stone,
accent: :emerald
}
```
### Dynamic mode
In dynamic mode, users get a theme picker in the sidebar that lets them choose their own neutral theme and accent color. Their preferences are persisted either in localStorage or in the database.
```ruby
config.branding = {
mode: :dynamic
}
```
#### Persistence
By default, theme preferences are stored in the browser's localStorage. To persist them in the database instead (so preferences follow users across devices), configure the `persistence`, `load_settings`, and `save_settings` options:
```ruby
config.branding = {
mode: :dynamic,
persistence: :database, # [!code focus]
load_settings: ->(current_user) { # [!code focus]
current_user.theme_settings || {} # [!code focus]
}, # [!code focus]
save_settings: ->(settings:, current_user:) { # [!code focus]
current_user.update!(theme_settings: settings) # [!code focus]
} # [!code focus]
}
```
The `settings` hash contains up to three keys: `color_scheme` (light/dark/auto), `neutral` (theme name), and `accent` (accent color name).
:::info
When using database persistence, add a `theme_settings` JSON column (or similar) to your users table.
:::
## Chart colors
Customize the colors used in dashboard charts by passing an array of hex colors:
```ruby
config.branding = {
chart_colors: ["#0B8AE2", "#34C683", "#FFBE4F", "#FF7676", "#2AB1EE"]
}
```
:::warning
Chart colors must be hex values. They are forwarded directly to Chart.js.
:::
## Full example
```ruby
Avo.configure do |config|
config.branding = {
# Logos
logo: "my_company/logo.png",
logo_dark: "my_company/logo_dark.png",
logomark: "my_company/logomark.png",
logomark_dark: "my_company/logomark_dark.png",
favicon: "my_company/favicon.ico",
placeholder: "my_company/placeholder.svg",
# Theme
mode: :dynamic,
neutral: :slate,
accent: :blue,
scheme: :auto,
# Chart colors
chart_colors: ["#0B8AE2", "#34C683", "#FFBE4F", "#FF7676"],
# Database persistence
persistence: :database,
load_settings: ->(current_user) {
current_user.theme_settings || {}
},
save_settings: ->(settings:, current_user:) {
current_user.update!(theme_settings: settings)
}
}
end
```
## Options reference
| Option | Type | Default | Description |
| ---------------- | --------------------------- | ----------------------- | ---------------------------------------------------- |
| `mode` | `:static` `:dynamic` | `:static` | Whether users can switch themes |
| `scheme` | `:auto` `:light` `:dark` | `:auto` | Color scheme |
| `neutral` | Symbol or Hash | `nil` | Neutral palette β predefined name or custom colors |
| `accent` | Symbol or Hash | `nil` | Accent color β predefined name or custom tokens |
| `persistence` | `:localstorage` `:database` | `:localstorage` | Where to store user theme preferences |
| `logo` | String | `"avo/logo.png"` | Desktop logo path |
| `logo_dark` | String | `nil` | Desktop logo for dark mode |
| `logomark` | String | `"avo/logomark.png"` | Mobile logo path |
| `logomark_dark` | String | `nil` | Mobile logo for dark mode |
| `favicon` | String | `"avo/favicon.ico"` | Favicon path |
| `favicon_dark` | String | `nil` | Favicon for dark mode |
| `placeholder` | String | `"avo/placeholder.svg"` | Missing image placeholder |
| `chart_colors` | Array | 10 default hex colors | Colors used in dashboard charts |
| `load_settings` | Proc | `nil` | Lambda to load theme settings from database |
| `save_settings` | Proc | `nil` | Lambda to save theme settings to database |
---
# Routing
We stick to Rails defaults in terms of routing just to make working with Avo as straightforward as possible.
Avo's functionality is distributed across multiple gems, each encapsulating its own engine. By default, these engines are mounted under Avo's scope within your Rails application.
Each engine registers itself with Avo.
### Default Mounting Behavior
When the `mount_avo` method is invoked, Avo and all the associated engines are mounted at a common entry point. By default, this mounting point corresponds to `Avo.configuration.root_path`, but you can customize it using the `at` argument:
```ruby{4,7}
# config/routes.rb
Rails.application.routes.draw do
# Mounts Avo at Avo.configuration.root_path
mount_avo
# Mounts Avo at `/custom_path` instead of the default
mount_avo at: "custom_path"
end
```
If no custom path is specified, Avo is mounted at the default configuration root path.
## Mount Avo under a scope
In this example, we'll demonstrate how to add a `:locale` scope to your routes.
The `:locale` scope is just an example. If your objective is to implement a route scope for localization within Avo, there's a detailed recipe available. Check out this guide for comprehensive instructions.
```ruby{4-6}
# config/routes.rb
Rails.application.routes.draw do
scope ":locale" do
mount_avo
end
end
```
:::info
To guarantee that the `locale` scope is included in the `default_url_options`, you must explicitly add it to the Avo configuration.
Check this documentation section for details on how to configure `default_url_options` setting.
:::
## Add your own routes
You may want to add your own routes inside Avo so you can access different custom actions that you might have set in the Avo resource controllers.
You can do that in your app's `routes.rb` file by opening up the Avo routes block and append your own.
```ruby
# routes.rb
Rails.application.routes.draw do
mount_avo
# your other app routes
end
if defined? ::Avo
Avo::Engine.routes.draw do
# new route in new controller
put "switch_accounts/:id", to: "switch_accounts#update", as: :switch_account
scope :resources do
# append a route to a resource controller
get "courses/cities", to: "courses#cities"
end
end
end
# app/controllers/avo/switch_accounts_controller.rb
class Avo::SwitchAccountsController < Avo::ApplicationController
def update
session[:tenant_id] = params[:id]
redirect_back fallback_location: root_path
end
end
```
---
# Multitenancy
Multitenancy is a very talked-about subject. We're not going to go very deep into how to achieve it on the database level, but will talk a little bit about how it's supported in Avo.
## Breakdown
Usually, with multitenancy you add a new layer just one level below authentication. You don't have just a user to think about, but now that user might act on the behalf of a tenant. That tenant can be an `Account` or a `Team`, or any other model you design in your database.
So now, the mission is to pinpoint which tenant is the user acting for. Because Avo has such an integrated experience and we use our own `ApplicationController`, you might think it's difficult to add that layer, when in fact it's really not. There are a couple of steps to do.
:::info
We'll use the `foo` tenant id from now on.
:::
## Route-based tenancy
There are a couple of strategies here, but the a common one is to use route-based tenancy. That means that your user uses a URL like `https://example.com/foo/` and the app should know to scope everything to that `foo` tenant.
We need to do a few things:
#### 1. Set the proper routing pattern
Mount Avo under the `tenant_id` scope
```ruby
# config/routes.rb
Rails.application.routes.draw do
scope "/:tenant_id" do
mount_avo
end
end
```
#### 2. Set the tenant for each request
:::code-group
```ruby [config/initializers/avo.rb]{6}
Avo.configure do |config|
# configuration values
end
Rails.configuration.to_prepare do
Avo::ApplicationController.include Multitenancy
end
```
```ruby [app/controllers/concerns/multitenancy.rb]
module Multitenancy
extend ActiveSupport::Concern
included do
prepend_before_action :set_tenant
end
def set_tenant
Avo::Current.tenant_id = params[:tenant_id]
Avo::Current.tenant = Account.find params[:tenant_id]
end
end
```
:::
Now, whenever you navigate to `https://example.com/lol` the tenant the `tenant_id` will be set to `lol`.
## Session-based tenancy
Using a session-based tenancy strategy is a bit simpler as we don't meddle with the routing.
:::warning
The code below shows how it's possible to do session-based multitenancy but your use-case or model names may vary a bit.
:::
We need to do a few things:
#### 1. Set the tenant for each request
:::code-group
```ruby [config/initializers/avo.rb]{6}
Avo.configure do |config|
# configuration values
end
Rails.configuration.to_prepare do
Avo::ApplicationController.include Multitenancy
end
```
```ruby [app/controllers/concerns/multitenancy.rb]
module Multitenancy
extend ActiveSupport::Concern
included do
prepend_before_action :set_tenant
end
def set_tenant
Avo::Current.tenant = Account.find session[:tenant_id] || current_user.accounts.first
end
end
```
:::
#### 2. Add an account switcher
Somewhere in a view on a navbar or sidebar add an account switcher.
:::code-group
```erb [app/views/avo/session_switcher.html.erb]
<% current_user.accounts.each do |account| %>
<%= link_to account.name, switch_account_path(account.id), class: class_names({"underline": session[:tenant_id].to_s == account.id.to_s}), data: {turbo_method: :put} %>
<% end %>
```
```ruby [app/controllers/avo/switch_accounts_controller.rb]
class Avo::SwitchAccountsController < Avo::ApplicationController
def update
# set the new tenant in session
session[:tenant_id] = params[:id]
redirect_back fallback_location: root_path
end
end
```
:::
---
# Custom pages (custom tools)
You may use custom tools to create custom sections or views to add to your app.
## Generate tools
`bin/rails generate avo:tool dashboard` will generate the necessary files to show the new custom tool.
```bash{2-6}
βΆ bin/rails generate avo:tool dashboard
create app/views/avo/sidebar/items/_dashboard.html.erb
insert app/controllers/avo/tools_controller.rb
create app/views/avo/tools/dashboard.html.erb
route namespace :avo do
get "dashboard", to: "tools#dashboard"
end
```
### Controller
If this is your first custom tool, a new `ToolsController` will be generated for you. Within this controller, Avo created a new method.
```ruby
class Avo::ToolsController < Avo::ApplicationController
def dashboard
end
end
```
You can keep this action in this controller or move it to another controller and organize it differently.
### Route
```ruby{2-4}
Rails.application.routes.draw do
namespace :avo do
get "dashboard", to: "tools#dashboard"
end
authenticate :user, ->(user) { user.admin? } do
mount_avo
end
end
```
The route generated is wrapped inside a namespace with the `Avo.configuration.root_path` name. Therefore, you may move it inside your authentication block next to the Avo mounting call.
### Sidebar item
The `_dashboard.html.erb` partial will be added to the `app/views/avo/sidebar/items` directory. All the files in this directory will be loaded by Avo and displayed in the sidebar. They are displayed alphabetically, so you may change their names to reorder the items.
### Customize the sidebar
If you want to customize the sidebar partial further, you can eject and update it to your liking. We're planning on creating a better sidebar customization experience later this year.
## Add assets
You might want to import assets (javascript and stylesheets files) when creating custom tools or fields. You can do that so easily from v1.3. Please follow this guide to bring your assets with your asset pipeline.
## Using helpers from your app
You'll probably want to use some of your helpers in your custom tools. To have them available inside your custom controllers inherited from Avo's `ApplicationController`, you need to include them using the `helper` method.
```ruby{3-5,10}
# app/helpers/home_helper.rb
module HomeHelper
def custom_helper
'hey from custom helper'
end
end
# app/controllers/avo/tools_controller.rb
class Avo::ToolsController < Avo::ApplicationController
helper HomeHelper
def dashboard
@page_title = "Dashboard"
end
end
```
```erb{13}
# app/views/avo/tools/dashboard.html.erb
<%= render Avo::PanelComponent.new title: 'Dashboard', display_breadcrumbs: true do |c| %>
<% c.with_tools do %>
This is the panels tools section.
<% end %>
<% c.with_body do %>
What a nice new tool π
<%= custom_helper %>
<% end %>
<% end %>
```
### Using path helpers
Because you're in a Rails engine, you will have to prepend the engine object to the path.
#### For Avo paths
Instead of writing `resources_posts_path(1)` you have to write `avo.resources_posts_path(1)`.
#### For the main app paths
When you want to reference paths from your main app, instead of writing `posts_path(1)`, you have to write `main_app.posts_path`.
---
# Custom fields
Avo ships with 20+ well polished and ready to be used, fields out of the box.
When you need a field that is not provided by default, Avo makes it easy to add it.
## Generate a new field
Every new field comes with three [view components](https://viewcomponent.org/), `Edit` (which is also used in the `New` view), and `Show` and `Index`. There's also a `Field` configuration file.
`bin/rails generate avo:field progress_bar` generates the files for you.
:::info
Please restart your rails server after adding a new custom field.
:::
```bash{2-9}
βΆ bin/rails generate avo:field progress_bar
create app/components/avo/fields/progress_bar_field
create app/components/avo/fields/progress_bar_field/edit_component.html.erb
create app/components/avo/fields/progress_bar_field/edit_component.rb
create app/components/avo/fields/progress_bar_field/index_component.html.erb
create app/components/avo/fields/progress_bar_field/index_component.rb
create app/components/avo/fields/progress_bar_field/show_component.html.erb
create app/components/avo/fields/progress_bar_field/show_component.rb
create app/avo/fields/progress_bar_field.rb
```
The `ProgressBarField` file is what registers the field in your admin.
```ruby
class Avo::Fields::ProgressBarField < Avo::Fields::BaseField
def initialize(name, **args, &block)
super(name, **args, &block)
end
end
```
Now you can use your field like so:
```ruby{7}
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id, link_to_record: true
field :progress, as: :progress_bar
end
end
```
The generated view components are basic text fields for now.
```erb{1,9,14}
# app/components/avo/fields/progress_bar_field/edit_component.html.erb
<%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal do %>
<%= @form.text_field @field.id,
class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)),
placeholder: @field.placeholder,
disabled: @field.readonly %>
<% end %>
# app/components/avo/fields/progress_bar_field/index_component.html.erb
<%= index_field_wrapper field: @field do %>
<%= @field.value %>
<% end %>
# app/components/avo/fields/progress_bar_field/show_component.html.erb
<%= show_field_wrapper field: @field, index: @index do %>
<%= @field.value %>
<% end %>
```
You can customize them and add as much or as little content as needed. More on customization [below](#customize-the-views).
There may be times when you want to duplicate an existing field and start from there.
To achieve this behavior, use the `--field_template` argument and pass the original field as a value.
Now, all components will have the exact same code (except the name) as the original field.
```bash
$ bin/rails generate avo:field super_text --field_template text
create app/components/avo/fields/super_text_field
create app/components/avo/fields/super_text_field/edit_component.html.erb
create app/components/avo/fields/super_text_field/edit_component.rb
create app/components/avo/fields/super_text_field/index_component.html.erb
create app/components/avo/fields/super_text_field/index_component.rb
create app/components/avo/fields/super_text_field/show_component.html.erb
create app/components/avo/fields/super_text_field/show_component.rb
create app/avo/fields/super_text_field.rb
```
We can verify that all components have the text field code. From here there are endless possibilities to extend the original field features.
```ruby
# app/avo/fields/super_text_field.rb
module Avo
module Fields
class SuperTextField < BaseField
attr_reader :link_to_record
attr_reader :as_html
attr_reader :protocol
def initialize(id, **args, &block)
super(id, **args, &block)
add_boolean_prop args, :link_to_record
add_boolean_prop args, :as_html
add_string_prop args, :protocol
end
end
end
end
# lib/avo/fields/text_field.rb
module Avo
module Fields
class TextField < BaseField
attr_reader :link_to_record
attr_reader :as_html
attr_reader :protocol
def initialize(id, **args, &block)
super(id, **args, &block)
add_boolean_prop args, :link_to_record
add_boolean_prop args, :as_html
add_string_prop args, :protocol
end
end
end
end
```
## Field options
This file is where you may add field-specific options.
```ruby{3-6,11-14}
# app/avo/fields/progress_bar_field.rb
class Avo::Fields::ProgressBarField < Avo::Fields::BaseField
attr_reader :max
attr_reader :step
attr_reader :display_value
attr_reader :value_suffix
def initialize(name, **args, &block)
super(name, **args, &block)
@max = 100
@step = 1
@display_value = false
@value_suffix = nil
end
end
```
The field-specific options can come from the field declaration as well.
```ruby{11-14,24}
# app/avo/fields/progress_bar_field.rb
class Avo::Fields::ProgressBarField < Avo::Fields::BaseField
attr_reader :max
attr_reader :step
attr_reader :display_value
attr_reader :value_suffix
def initialize(name, **args, &block)
super(name, **args, &block)
@max = args[:max] || 100
@step = args[:step] || 1
@display_value = args[:display_value] || false
@value_suffix = args[:value_suffix] || nil
end
end
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id, link_to_record: true
field :progress, as: :progress_bar, step: 10, display_value: true, value_suffix: "%"
end
end
```
## Field Visibility
If you need to hide the field in some view, you can use the visibility helpers.
```ruby{16}
# app/avo/fields/progress_bar_field.rb
class Avo::Fields::ProgressBarField < Avo::Fields::BaseField
attr_reader :max
attr_reader :step
attr_reader :display_value
attr_reader :value_suffix
def initialize(name, **args, &block)
super(name, **args, &block)
@max = args[:max] || 100
@step = args[:step] || 1
@display_value = args[:display_value] || false
@value_suffix = args[:value_suffix] || nil
hide_on :forms
end
end
```
## Customize the views
No let's do something about those views. Let's add a progress bar to the `Index` and `Show` views.
```erb{1,15}
# app/components/avo/fields/progress_bar_field/show_component.html.erb
<%= show_field_wrapper field: @field, index: @index do %>
<% if @field.display_value %>
<%= @field.value %><%= @field.value_suffix if @field.value_suffix.present? %>
<% end %>
<% end %>
# app/components/avo/fields/progress_bar_field/index_component.html.erb
<%= index_field_wrapper field: @field do %>
<% if @field.display_value %>
<%= @field.value %><%= @field.value_suffix if @field.value_suffix.present? %>
<% end %>
<% end %>
```
For the `Edit` view, we're going to do something different. We'll implement a `range` input.
```erb{1}
# app/components/avo/fields/progress_bar_field/edit_component.html.erb
<%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal do %>
<% if @field.display_value %>
<%= @field.value %> <%= @field.value_suffix if @field.value_suffix.present? %>
<% end %>
<%= @form.range_field @field.id,
class: 'w-full',
placeholder: @field.placeholder,
disabled: @field.readonly,
min: 0,
# add the field-specific options
max: @field.max,
step: @field.step,
%>
<% end %>
```
## Field assets
Because there isn't just one standardized way of handling assets in Rails, we decided we won't provide **asset loading** support for custom fields for now. That doesn't mean that you can't use custom assets (javascript or CSS files), but you will have to load them in your own pipeline in dedicated Avo files.
In the example above, we added javascript on the page just to demonstrate the functionality. In reality, you might add that to a stimulus controller inside your own Avo dedicated pipeline (webpacker or sprockets).
Some styles were added in the asset pipeline directly.
```css
progress {
@apply h-2 bg-white border border-gray-400 rounded shadow-inner;
}
progress[value]::-webkit-progress-bar {
@apply bg-white border border-gray-500 rounded shadow-inner;
}
progress[value]::-webkit-progress-value {
@apply bg-green-600 rounded;
}
progress[value]::-moz-progress-bar {
@apply bg-green-600 rounded appearance-none;
}
```
## Use pre-built Stimulus controllers
Avo ships with a few Stimulus controllers that help you build more dynamic fields.
### Hidden input controller
This controller allows you to hide your content and add a trigger to show it. You'll find it in the Trix field.
You should add the `:always_show` `attr_reader` and `@always_show` instance variables to your field.
```ruby{3,8}
# app/avo/fields/color_picker_field.rb
class Avo::Fields::ColorPickerField < Avo::Fields::BaseField
attr_reader :always_show
def initialize(id, **args, &block)
super(id, **args, &block)
@always_show = args[:always_show] || false
@allow_non_colors = args[:allow_non_colors]
end
end
```
Next, in your fields `Show` component, you need to do a few things.
1. Wrap the field inside a controller tag
1. Add the trigger that will show the content.
1. Wrap the value in a div with the `hidden` class applied if the condition `@field.always_show` is `false`.
1. Add the `content` target (`data-hidden-input-target="content"`) to that div.
```erb{4-7,8}
# app/components/avo/fields/color_picker_field/show_component.html.erb
<%= show_field_wrapper field: @field, index: @index do %>
<% unless @field.always_show %>
<%= link_to t('avo.show_content'), 'javascript:void(0);', class: 'font-bold inline-block', data: { action: 'click->hidden-input#showContent' } %>
<% end %>
class="hidden" <% end %> data-hidden-input-target="content">
<%= @field.value %>
<% end %>
```
### Non existing model field
To ensure proper rendering of a custom field that lacks getters and setters at the model level, you must implement these methods within the model.
```ruby
def custom_field
end
def custom_field=(value)
end
```
## Field methods
We won't be able to list all the methods available for a field here, but we've added a few methods to help you build better fields.
This adds a class to the `th` element of the table header.
We added it when we needed to force a certain column to be a certain size, but you can use it for any purpose.
It defaults to `nil`
```ruby
def table_header_class
"w-32"
end
```
---
# Custom errors
Actions such as create, update, attach, etc... will not be completed if the record contains any errors. This ensures that only valid data is processed and saved, maintaining the integrity of your application. Custom validations can be added to your models to enforce specific rules and provide meaningful error messages to users.
## Adding Custom Errors
To add custom errors, you can define a validation method in your model. If the validation fails it adds an error to the record. These errors will prevent the action from completing and will be displayed as notifications to the user.
## In a Simple Record
Consider a simple `User` model where you want to enforce a custom validation rule, such as ensuring that the user's age is over a certain value.
```ruby
# app/models/user.rb
class User < ApplicationRecord
validate :age_must_be_over_18
private
def age_must_be_over_18
# Add a custom error to the record if age is less than 18.
if age < 18
errors.add(:age, "must be over 18.")
end
end
end
```
In this example, the `age_must_be_over_18` method checks if the user's age is less than 18. If so, it adds an error to the `age` attribute with a custom message. This error prevents any further Avo action on the record and notifies the user of the issue.
## In a Join Table
Consider a join table `TeamMembership` which links `Team` and `User` models. You might want to add a custom validation to ensure some business logic is enforced.
```ruby
# app/models/team_membership.rb
class TeamMembership < ApplicationRecord
belongs_to :team
belongs_to :user
validate :custom_validation
private
def custom_validation
if user.banned?
errors.add(:user, "is banned.")
end
end
end
```
In this example, the `custom_validation` method is called whenever a `TeamMembership` record is validated. If the conditions in this method are not met, an error is added to the `user` attribute with a custom message. This error prevents any further Avo action on the record and notifies the user of the issue.
---
# Resource tools
Similar to adding custom fields to a resource, you can add custom tools. A custom tool is a partial added to your resource's `Show` and `Edit` views.
## Generate a resource tool
Run `bin/rails generate avo:resource_tool post_info`. That will create two files. The configuration file `app/avo/resource_tools/post_info.rb` and the partial file `app/views/avo/resource_tools/_post_info.html.erb`.
The configuration file holds the tool's name and the partial path if you want to override it.
```ruby
class Avo::ResourceTools::PostInfo < Avo::BaseResourceTool
self.name = "Post info"
# self.partial = "avo/resource_tools/post_info"
end
```
The partial is ready for you to customize further.
```erb
<%= render Avo::PanelComponent.new title: "Post info" do |c| %>
<% c.with_tools do %>
<%= a_link('/avo', icon: 'heroicons/solid/academic-cap', style: :primary) do %>
Dummy link
<% end %>
<% end %>
<% c.with_body do %>
πͺ§ This partial is waiting to be updated
You can edit this file here app/views/avo/resource_tools/post_info.html.erb.
The resource tool configuration file should be here app/avo/resource_tools/post_info.rb.
<%
# In this partial, you have access to the following variables:
# tool
# @resource
# @resource.model
# form (on create & edit pages. please check for presence first)
# params
# Avo::Current.context
# current_user
%>
<% end %>
<% end %>
```
## Partial context
You might need access to a few things in the partial.
You have access to the `tool`, which is an instance of your tool `PostInfo`, and the `@resource`, which holds all the information about that particular resource (`view`, `model`, `params`, and others), the `params` of the request, the `Avo::Current.context` and the `current_user`.
That should give you all the necessary data to scope out the partial content.
## Tool visibility
The resource tool is default visible on the `Show` view of a resource. You can change that using the visibility options (`show_on`, `only_on`).
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
def fields
tool Avo::ResourceTools::PostInfo, show_on: :edit
end
end
```
### Using path helpers
Because you're in a Rails engine, you will have to prepend the engine object to the path.
#### For Avo paths
Instead of writing `resources_posts_path(1)` you have to write `avo.resources_posts_path(1)`.
#### For the main app paths
When you want to reference paths from your main app, instead of writing `posts_path(1)`, you have to write `main_app.posts_path`.
## Add custom fields on forms
**From Avo 2.12**
You might want to add a few more fields or pieces of functionality besides the CRUD-generated fields on your forms. Of course, you can already create new custom fields to do it in a more structured way, but you can also use a resource tool to achieve more custom behavior.
You have access to the `form` object that is available on the new/edit pages on which you can attach inputs of your choosing. You can even achieve nested form functionality.
You have to follow three steps to enable this functionality:
1. Add the inputs in a resource tool and enable the tool on the form pages
2. Tell Avo which `params` it should permit to write to the model
3. Make sure the model is equipped to receive the params
In the example below, we'll use the `Avo::Resources::Fish`, add a few input fields (they will be a bit unstyled because this is not the scope of the exercise), and do some actions with some of them.
We first need to generate the tool with `bin/rails g avo:resource_tool fish_information` and add the tool to the resource file.
```ruby{3}
class Avo::ResourcesFish < Avo::BaseResource
def fields
tool Avo::ResourceTools::FishInformation, show_on: :forms
end
end
```
In the `_fish_information.html.erb` partial, we'll add a few input fields. Some are directly on the `form`, and some are nested with `form.fields_for`.
The fields are:
- `fish_type` as a text input
- `properties` as a multiple text input which will produce an array in the back-end
- `information` as nested inputs which will produce a `Hash` in the back-end
```erb{13-36}
<%= render Avo::PanelComponent.new(title: @resource.model.name) do |c| %>
<% c.with_tools do %>
<%= a_link('/admin', icon: 'heroicons/solid/academic-cap', style: :primary) do %>
Primary
<% end %>
<% end %>
<% c.with_body do %>
<% if form.present? %>
<%= form.label :fish_type %>
<%= form.text_field :fish_type, value: 'default type of fish', class: input_classes %>
<%= form.label :properties %>
<%= form.text_field :properties, multiple: true, value: 'property 1', class: input_classes %>
<%= form.text_field :properties, multiple: true, value: 'property 2', class: input_classes %>
<% form.fields_for :information do |information_form| %>
<%= form.label :information_name %>
<%= information_form.text_field :name, value: 'information name', class: input_classes %>
This is going to be passed to the model
<%= form.label :information_history %>
<%= information_form.text_field :history, value: 'information history', class: input_classes %>
This is going to be passed to the model
<%= form.label :information_age %>
<%= information_form.text_field :age, value: 'information age', class: input_classes %>
This is NOT going to be passed to the model
<% end %>
<% end %>
<% end %>
<% end %>
```
Next, we need to tell Avo and Rails which params are welcomed in the `create`/`update` request. We do that using the `extra_params` option on the `Avo::Resources::Fish`. Avo's internal implementation is to assign the attributes you specify here to the underlying model (`model.assign_attributes params.permit(extra_params)`).
```ruby{2}
class Avo::Resources::Fish < Avo::BaseResource
self.extra_params = [:fish_type, :something_else, properties: [], information: [:name, :history]]
def fields
tool Avo::ResourceTools::FishInformation, show_on: :forms
end
end
```
The third step is optional. You must ensure your model responds to the params you're sending. Our example should have the `fish_type`, `properties`, and `information` attributes or setter methods on the model class. We chose to add setters to demonstrate the params are called to the model.
```ruby
class Fish < ApplicationRecord
self.inheritance_column = nil # required in order to use the type DB attribute
def fish_type=(value)
self.type = value
end
def properties=(value)
# properties should be an array
puts ["properties in the Fish model->", value].inspect
end
def information=(value)
# properties should be a hash
puts ["information in the Fish model->", value].inspect
end
end
```
If you run this code, you'll notice that the `information.information_age` param will not reach the `information=` method because we haven't allowed it in the `extra_params` option.
## Where to add logic
It's a good practice not to keep login in view files (partials).
You can hide that logic inside the tool using instance variables and methods, and access it in the partial using the `tool` variable.
[Here's an example](https://github.com/avo-hq/main.avodemo.com/commit/c8ecb9b53a770103a993df4c2b3acec0a1faf737) on how you could do that.
```ruby{8,10}
class Avo::ResourceTools::PostInfo < Avo::BaseResourceTool
self.name = "Post info"
# self.partial = "avo/resource_tools/post_info"
attr_reader :foo
def initialize(**kwargs)
super **kwargs # It's important to call super with the same keyword arguments
# You'll have access to the following objects:
# resource - when attached to a resource
# parent - which is the object it's attached to (resource if attached to a resource)
# view
@foo = :bar # Add your variables
end
def custom_method_call
:called
end
end
```
```erb{7,12}
<%= render Avo::PanelComponent.new title: "Post info" do |c| %>
<% c.with_body do %>
This variable was declared in the initializer:
<%= tool.foo %>
This is a method called on the tool:
<%= tool.custom_method_call %>
<% end %>
<% end %>
```
---
# Stimulus JS & HTML attributes
:::warning
This feature is in the **beta** phase. The API might change while seeing how the community uses it to build their apps.
This is not the **dependable fields** feature but a placeholder so we can observe and see what we need to ship to make it helpful to you.
:::
_What we'll be able to do at the end of reading these docs_
:::info
**Please note** that in order to have the JS code from your controllers loaded in Avo you'll need to add your asset pipeline using these instructions. It's really easier than it sounds. It's like you'd add a new JS file to your regular Rails app.
:::
One of the most requested features is the ability to make the forms more dynamic. We want to bring the first iteration of this feature through Stimulus JS integration.
This light layer will allow you to hook into the views and inject your functionality with Stimulus JS.
You'll be able to add your Stimulus controllers to the resource views (`Index`, `Show`, `Edit`, and `New`), attach `classes`, `style`, and `data` attributes to the fields and inputs in different views.
## Assign Stimulus controllers to resource views
To enable a stimulus controller to resource view, you can use the `stimulus_controllers` option on the resource file.
```ruby
class Avo::Resources::Course < Avo::BaseResource
self.stimulus_controllers = "course-resource"
end
```
You can add more and separate them by a space character.
```ruby
class Avo::Resources::Course < Avo::BaseResource
self.stimulus_controllers = "course-resource select-field association-fields"
end
```
Avo will add a `resource-[VIEW]` (`resource-edit`, `resource-show`, or `resource-index`) controller for each view.
### Field wrappers as targets
By default, Avo will add stimulus target data attributes to all field wrappers. The notation scheme uses the name and field type `[FIELD_NAME][FIELD_TYPE]WrapperTarget`.
```ruby
# Wrappers get the `data-[CONTROLLER]-target="nameTextWrapper"` attribute and can be targeted using nameTextWrapperTarget
field :name, as: :text
# Wrappers get the `data-[CONTROLLER]-target="createdAtDateTimeWrapper"` attribute and can be targeted using createdAtDateTimeWrapperTarget
field :created_at, as: :date_time
# Wrappers get the `data-[CONTROLLER]-target="hasSkillsTagsWrapper"` attribute and can be targeted using hasSkillsTagsWrapperTarget
field :has_skills, as: :tags
```
For example for the following stimulus controllers `self.stimulus_controllers = "course-resource select-field association-fields"` Avo will generate the following markup for the `has_skills` field above on the `edit` view.
```html{4-7}
```
You can add those targets to your controllers and use them in your JS code.
### Field inputs as targets
Similar to the wrapper element, inputs in the `Edit` and `New` views get the `[FIELD_NAME][FIELD_TYPE]InputTarget`. On more complex fields like the searchable, polymorphic `belongs_to` field, where there is more than one input, the target attributes are attached to all `input`, `select`, and `button` elements.
```ruby
# Inputs get the `data-[CONTROLLER]-target="nameTextInput"` attribute and can be targeted using nameTextInputTarget
field :name, as: :text
# Inputs get the `data-[CONTROLLER]-target="createdAtDateTimeInput"` attribute and can be targeted using createdAtDateTimeInputTarget
field :created_at, as: :date_time
# Inputs get the `data-[CONTROLLER]-target="hasSkillsTagsInput"` attribute and can be targeted using hasSkillsTagsInputTarget
field :has_skills, as: :tags
```
### All controllers receive the `view` value
All stimulus controllers receive the `view` attribute in the DOM.
```html{4-5}
```
Now you can use that inside your Stimulus JS controller like so:
```js{5,9}
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static values = {
view: String,
}
async connect() {
console.log('view ->', this.viewValue)
}
}
```
The possible values are `index`, `show`, `edit`, or `new`
## Assign Stimulus controllers to actions
Similarly as to resource, you can assign stimulus controller to an action. To do that you can use the `stimulus_controllers` option on the action file.
```ruby
class Avo::Actions::ShowCurrentTime < Avo::BaseAction
self.stimulus_controllers = "city-in-country"
end
```
You can add more and separate them by a space character.
```ruby
class Avo::Actions::ShowCurrentTime < Avo::BaseAction
self.stimulus_controllers = "course-resource select-field association-fields"
end
```
The same way as for the resources, Avo will add stimulus target data attributes to [all field wrappers](#field-wrappers-as-targets) and [all input fields](#field-inputs-as-targets).
Unlike with the resource, Avo will not add a specific default controller for each type of the view (`index`, `show`, `edit`).
Same way, the controllers will not receive the `view` attribute in the DOM, [as in case of resources](#all-controllers-receive-the-view-value).
## Attach HTML attributes
This section has moved.
## Composing the attributes together
You can use the attributes together to make your fields more dynamic.
```ruby{3-9}
field :has_skills, as: :boolean, html: {
edit: {
input: {
data: {
# On click run the toggleSkills method on the toggle-fields controller
action: "input->toggle-fields#toggleSkills",
}
}
}
}
field :skills, as: :tags, html: {
edit: {
wrapper: {
# hide this field by default
classes: "hidden"
}
}
}
```
```js
// toggle_fields_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["skillsTagsWrapper"]; // use the target Avo prepared for you
toggleSkills() {
this.skillsTagsWrapperTarget.classList.toggle("hidden");
}
}
```
## Pre-made stimulus methods
Avo ships with a few JS methods you may use on your resources.
### `resource-edit#toggle`
On your `Edit` views, you can use the `resource-edit#toggle` method to toggle the field visibility from another field.
```ruby{5-7}
field :has_country, as: :boolean, html: {
edit: {
input: {
data: {
action: "input->resource-edit#toggle", # use the pre-made stimulus method on input
resource_edit_toggle_target_param: "countrySelectWrapper", # target to be toggled
# resource_edit_toggle_targets_param: ["countrySelectWrapper"] # add more than one target
}
}
}
}
field :country, as: :select, options: Course.countries.map { |country| [country, country] }.to_h
```
### `resource-edit#disable`
Disable works similarly to toggle, with the difference that it disables the field instead of hiding it.
```ruby{5-7,16}
field :has_skills, as: :boolean, html: {
edit: {
input: {
data: {
action: "input->resource-edit#disable", # use the pre-made stimulus method on input
resource_edit_disable_target_param: "countrySelectInput", # target to be disabled
# resource_edit_disable_targets_param: ["countrySelectWrapper"] # add more than one target to disable
}
}
}
}
field :country, as: :select, options: Course.countries.map { |country| [country, country] }.to_h
```
You may also target the `wrapper` element for that field if the target field has more than one input like the searchable polymorphic `belongs_to` field.
```ruby{6}
field :has_skills, as: :boolean, html: {
edit: {
input: {
data: {
action: "input->resource-edit#disable", # use the pre-made stimulus method on input
resource_edit_disable_target_param: "countrySelectWrapper", # target the wrapper so all inputs are disabled
# resource_edit_disable_targets_param: ["countrySelectWrapper"] # add more than one target to disable
}
}
}
}
field :country, as: :select, options: Course.countries.map { |country| [country, country] }.to_h
```
### `resource-edit#debugOnInput`
For debugging purposes only, the `resource_edit` Stimulus JS controller provides the `debugOnInput` method that outputs the event and value for an action to the console. Use this just to make sure you targeted your fields correctly. It doesn't have any real use.
## Custom Stimulus controllers
:::info Check the source code
If you visit our demo website on the [course edit page](https://main.avodemo.com/avo/resources/courses/1/edit) you can see this in action.
- Demo of the feature in action
https://main.avodemo.com/avo/resources/courses/1/edit
- JS controller that does that change
https://github.com/avo-hq/main.avodemo.com/blob/main/app/javascript/controllers/course_controller.js
- Rails controller that returns the results
https://github.com/avo-hq/main.avodemo.com/blob/main/app/controllers/avo/courses_controller.rb#L3
- Stimulus action that triggers the update
https://github.com/avo-hq/main.avodemo.com/blob/main/app/avo/resources/course.rb#L68
:::
The bigger purpose of this feature is to create your own Stimulus JS controllers to bring the functionality you need to the CRUD interface.
Below is an example of how you could implement a city & country select feature where the city select will have its options changed when the user selects a country:
1. Add an action to the country select to trigger a change.
1. The stimulus method `onCountryChange` will be triggered when the user changes the country.
1. That will trigger a fetch from the server where Rails will return an array of cities for the provided country.
1. The city field will have a `loading` state while we fetch the results.
1. The cities will be added to the `city` select field
1. If the initial value is present in the returned results, it will be selected.
1. All of this will happen only on the `New` and `Edit` views because of the condition we added to the `connect` method.
::: code-group
```ruby [app/avo/resources/course.rb]
# app/avo/resources/course.rb
class Avo::Resources::Course < Avo::BaseResource
self.stimulus_controllers = "course-resource"
def fields
field :id, as: :id
field :name, as: :text
field :country, as: :select, options: Course.countries.map { |country| [country, country] }.to_h, html: {
edit: {
input: {
data: {
course_resource_target: "countryFieldInput", # Make the input a target
action: "input->course-resource#onCountryChange" # Add an action on change
}
}
}
}
field :city, as: :select, options: Course.cities.values.flatten.map { |city| [city, city] }.to_h, html: {
edit: {
input: {
data: {
course_resource_target: "cityFieldInput" # Make the input a target
}
}
}
}
end
end
```
```ruby{4-6} [config/routes.rb]
Rails.application.routes.draw do
if defined? ::Avo
Avo::Engine.routes.draw do
scope :resources do
get "courses/cities", to: "courses#cities"
end
end
end
end
```
```ruby{3} [app/controllers/avo/courses_controller.rb]
class Avo::CoursesController < Avo::ResourcesController
def cities
render json: get_cities(params[:country]) # return an array of cities based on the country we received
end
private
def get_cities(country)
return [] unless Course.countries.include?(country)
Course.cities[country.to_sym]
end
end
```
```ruby [app/models/course.rb]
class Course < ApplicationRecord
def self.countries
["USA", "Japan", "Spain", "Thailand"]
end
def self.cities
{
USA: ["New York", "Los Angeles", "San Francisco", "Boston", "Philadelphia"],
Japan: ["Tokyo", "Osaka", "Kyoto", "Hiroshima", "Yokohama", "Nagoya", "Kobe"],
Spain: ["Madrid", "Valencia", "Barcelona"],
Thailand: ["Chiang Mai", "Bangkok", "Phuket"]
}
end
end
```
```js [course_resource_controller.js]
import { Controller } from "@hotwired/stimulus";
const LOADER_CLASSES = "absolute bg-gray-100 opacity-10 w-full h-full";
export default class extends Controller {
static targets = ["countryFieldInput", "cityFieldInput", "citySelectWrapper"];
static values = {
view: String,
};
// Te fields initial value
static initialValue;
get placeholder() {
return this.cityFieldInputTarget.ariaPlaceholder;
}
set loading(isLoading) {
if (isLoading) {
// create a loader overlay
const loadingDiv = document.createElement("div");
loadingDiv.className = LOADER_CLASSES;
loadingDiv.dataset.target = "city-loader";
// add the loader overlay
this.citySelectWrapperTarget.prepend(loadingDiv);
this.citySelectWrapperTarget.classList.add("opacity-50");
} else {
// remove the loader overlay
this.citySelectWrapperTarget
.querySelector('[data-target="city-loader"]')
.remove();
this.citySelectWrapperTarget.classList.remove("opacity-50");
}
}
async connect() {
// Add the controller functionality only on forms
if (["edit", "new"].includes(this.viewValue)) {
this.captureTheInitialValue();
// Trigger the change on load
await this.onCountryChange();
}
}
// Read the country select.
// If there's any value selected show the cities and prefill them.
async onCountryChange() {
if (this.hasCountryFieldInputTarget && this.countryFieldInputTarget) {
// Get the country
const country = this.countryFieldInputTarget.value;
// Dynamically fetch the cities for this country
const cities = await this.fetchCitiesForCountry(country);
// Clear the select of options
Object.keys(this.cityFieldInputTarget.options).forEach(() => {
this.cityFieldInputTarget.options.remove(0);
});
// Add blank option
this.cityFieldInputTarget.add(new Option(this.placeholder));
// Add the new cities
cities.forEach((city) => {
this.cityFieldInputTarget.add(new Option(city, city));
});
// Check if the initial value is present in the cities array and select it.
// If not, select the first item
const currentOptions = Array.from(this.cityFieldInputTarget.options).map(
(item) => item.value
);
if (currentOptions.includes(this.initialValue)) {
this.cityFieldInputTarget.value = this.initialValue;
} else {
// Select the first item
this.cityFieldInputTarget.value =
this.cityFieldInputTarget.options[0].value;
}
}
}
// Private
captureTheInitialValue() {
this.initialValue = this.cityFieldInputTarget.value;
}
async fetchCitiesForCountry(country) {
if (!country) {
return [];
}
this.loading = true;
const response = await fetch(
`${window.Avo.configuration.root_path}/resources/courses/cities?country=${country}`
);
const data = await response.json();
this.loading = false;
return data;
}
}
```
:::
This is how the fields behave with this Stimulus JS controller.
## Use Stimulus JS in a tool
There are a few steps you need to take in order to register the Stimulus JS controller in the current app context.
First, you need to have a JS entrypoint (ex: `avo.custom.js`) and have that loaded in the `_head` partial. For instructions on that please follow these steps to add it to your app (`importmaps` or `esbuild`).
### Set up a controller
```js
// app/javascript/controllers/sample_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
connect() {
console.log("Hey from sample controller π");
}
}
```
### Register that controller with the current Stimulus app
```js
// app/javascript/avo.custom.js
import SampleController from "controllers/sample_controller";
// Hook into the stimulus instance provided by Avo
const application = window.Stimulus;
application.register("course-resource", SampleController);
// eslint-disable-next-line no-console
console.log("Hi from Avo custom JS π");
```
### Use the controller in the Avo tool
```erb
```
Done π Now you have a controller connecting to a custom Resource tool or Avo tool (or Avo views).
---
# Custom asset pipeline
Avo plays well with most Rails asset pipelines.
| Asset pipeline | Avo compatibility |
|---------------|------------|
| [importmap](https://github.com/rails/importmap-rails) | β
Fully supported |
| [Propshaft](https://github.com/rails/propshaft) | β
Fully supported |
| [Sprockets](https://github.com/rails/sprockets) | β
Fully supported |
| [Webpacker](https://github.com/rails/webpacker) | π» Only with Sprockets or Propshaft |
There are two things we need to mention when communicating about assets.
1. Avo's assets
2. You custom assets
## Avo's assets
We chose to impact your app, and your deploy processes as little as possible. That's why we bundle up Avo's assets when we publish on [rubygems](https://rubygems.org/gems/avo), so you don't have to do anything else when you deploy your app. Avo doesn't require a NodeJS, or any kind of any other special environment in your deploy process.
Under the hood Avo uses TailwindCSS 3.0 with the JIT engine and bundles the assets using [`jsbundling`](https://github.com/rails/jsbundling-rails) with `esbuild`.
## Exclude servings Avo assets from a CDN?
If you utilize a Content Delivery Network (CDN) for serving assets and you want to exclude Avo paths from the default asset host you may use the following code snippet.
```ruby
config.action_controller.asset_host = Proc.new do |source|
# Exclude assets under the "/avo" path from CDN
next nil if source.start_with?("/avo")
# Set the general asset host (CDN) using an environment variable
ENV.fetch("ASSET_HOST")
end
```
This configuration ensures that assets are served through the specified CDN, except for those under the `/avo` path. Adjust the paths and environment variable as needed for your application.
## Your custom assets
Avo makes it easy to use your own styles and javascript through your already set up asset pipeline. It just hooks on to it to inject the new assets to be used in Avo.
## Use TailwindCSS utility classes
Please follow the dedicated TailwindCSS integration guide.
## Add custom JS code and Stimulus controllers
There are more ways of dealing with JS assets, and Avo handles that well.
## Use Importmap to add your assets
Importmap has become the default way of dealing with assets in Rails 7. For you to start using custom JS assets with Avo and importmap you should run this install command `bin/rails generate avo:js:install`. That will:
- create your `avo.custom.js` file as your JS entrypoint;
- add it to the `app/views/avo/partials/_head.html.erb` partial so Avo knows to load it;
- pin it in your `importmap.rb` file so `importmap-rails` knows to pick it up.
## Use `js-bundling` with `esbuild`
`js-bundling` gives you a bit more flexibility and power when it comes to assets. We use that under the hood and we'll use it to expose your custom JS assets.
When you install `js-bundling` with `esbuild` you get this npm script `"build": esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets`. That script will take all your JS entrypoint files under `app/javascript` and bundle them under `assets/builds`.
```bash
bin/rails generate avo:js:install --bundler esbuild
```
That command will:
- eject the `_head.html.erb` file;
- add the `avo.custom.js` asset to it;
- create the `avo.custom.js` file under `app/javascript` which will be your entrypoint.
That will be picked up by the `build` script and create it's own `assets/builds/avo.custom.js` file that will, in turn, be picked up by sprockets or propshaft and loaded into your app.
## Use `js-bundling` with `rollup` or `webpack`
Avo supports the other bundlers too but we just don't have a generator command to configure them for you. If you use the other bundlers and have configured them to use custom assets, then please [open up a PR](https://github.com/avo-hq/avo) and help the community get started faster.
## Manually add your CSS and JS assets
In order to manually add your assets you have to eject the `_pre_head.html.erb` partial (`bin/rails generate avo:eject --partial :pre_head`), create the asset files (examples below), and add the asset files from your pipeline to the `_pre_head` partial. Then, your asset pipeline will pick up those assets and use add them to your app.
:::warning
You should add your custom styles to `_pre_head.html.erb`, versus `_head.html.erb` to avoid overriding Avo's default styles. This
The order in which Avo loads the partials and asset files is this one:
1. `_pre_head.html.erb`
2. Avo's CSS and JS assets
3. `_head.html.erb`
:::
### Sprockets and Propshaft
Create `avo.custom.js` to the `app/javascripts` directory and `avo.custom.css` to `app/assets/stylesheets` with the desired scripts and styles.
Then add them to Avo using the `_pre_head.html.erb` partial (`rails generate avo:eject --partial :pre_head`).
```erb
# app/views/avo/partials/_pre_head.html.erb
<%= javascript_include_tag 'avo.custom', defer: true %>
<%= stylesheet_link_tag 'avo.custom', media: 'all' %>
```
:::warning
Please ensure that when using `javascript_include_tag` you add the `defer: true` option so the browser will use the same loading strategy as Avo's and the javascript files are loaded in the right order.
:::
### Webpacker
:::warning
We removed support for webpacker. In order to use Avo with your assets you must install Sprockets or Propshaft in order to serve assets like SVG, CSS, or JS files.
:::
:::info
Instructions below are for Webpacker version 6. Version 5 has different paths (`app/javascript/packs`).
:::
Create `avo.custom.js` and `avo.custom.css` inside `app/packs/entrypoints` with the desired scripts and styles.
Then add them to Avo using the `_pre_head.html.erb` partial (`rails generate avo:eject --partial :pre_head`).
```erb
# app/views/avo/partials/_pre_head.html.erb
<%= javascript_pack_tag 'avo.custom', defer: true %>
<%= stylesheet_pack_tag 'avo.custom', media: 'all' %>
```
---
# TailwindCSS integration
:::info
This integration is especially useful when you style UI extension points with custom CSS or Tailwind classes, like custom pages and custom fields, where classes may not exist in the precompiled Avo bundle.
This integration is built for Tailwind CSS 4.
:::
Avo ships with precompiled styles that are easy to use and work out of the box, so most apps do not need to care about extra build steps.
When Avo detects `tailwindcss-ruby`, it automatically enables this integration and builds an app-level Avo stylesheet with zero (or close to zero) configuration.
If you have `tailwindcss-ruby` installed (or `tailwindcss-rails`, which depends on `tailwindcss-ruby`) but you do **not** use custom Tailwind/CSS classes in custom tools, ejected components, custom fields, or other extended UI areas, you can opt out and keep using only Avo's precompiled styles.
When enabled, this integration compiles a stylesheet that includes:
- Avo core styles
- loaded Avo plugin styles
- your host app Avo styles from `app/assets/stylesheets/avo/**/*.css`
- utility classes discovered under your Rails `app/` directory (configurable; see below), plus Avo and plugin sources
The output file is:
`app/assets/builds/avo/application.css`
This integration writes to the same logical Avo stylesheet path (`avo/application`) so your app always loads one stylesheet entrypoint.
## Auto-enable behavior
Add `tailwindcss-ruby` to your app:
```ruby
gem "tailwindcss-ruby"
```
Once present, Avo will:
- auto-build in development
- hook into `assets:precompile` in production
:::tip
We highly recommend running this integration through `bin/dev` with a `Procfile.dev` watcher process:
```bash
web: bin/rails server -p 3000
avo_css: bin/rails avo:tailwindcss:watch
```
This keeps your Avo Tailwind build running in parallel with the Rails server, so style changes are compiled automatically while you work without extra manual steps.
:::
## Add your custom Avo styles
Add your Avo-specific stylesheets under `app/assets/stylesheets/avo/`.
Every CSS file in this path is included in the final `avo/application.css` build.
For example:
```css
/* app/assets/stylesheets/avo/buttons.css */
@layer components {
.avo-btn-highlight {
@apply px-3 py-2 rounded-md bg-indigo-600 text-white;
}
}
```
## Disable integration (opt-out)
If your app has `tailwindcss-ruby` (directly or via `tailwindcss-rails`) but you do not need Avo custom utility coverage, disable the integration in your initializer:
```ruby
Avo.configure do |config|
config.tailwindcss_integration_enabled = false
end
```
Default is `true`.
The default is intentionally `true` to preserve the zero-configuration experience: if your app has `tailwindcss-ruby` and you start adding classes/styles in custom Avo UI extension points, this integration should just work without you needing to think about setup details.
## Host content scan paths (`tailwindcss_content_sources`)
Avo writes Tailwind v4 `@source` directives into `tmp/avo/avo.tailwind.input.css` so the compiler can see utility classes used in templates and Ruby/HTML. **By default**, the host-side scan is limited to `Rails.root.join("app")` (the usual Rails tree: views, components, `app/avo` resources, and similar).
That keeps discovery focused on application code and avoids scanning unrelated directories under the project root.
Configure extra roots (or override the list entirely) in `config/initializers/avo.rb`:
```ruby
Avo.configure do |config|
config.tailwindcss_content_sources = [
Rails.root.join("app"),
Rails.root.join("lib", "components")
]
end
```
Each entry may be an **absolute** path or a path **relative to** `Rails.root`. Directories that do not exist are skipped.
To include the **entire project root** in the host scan (everything under `Rails.root`), set:
```ruby
config.tailwindcss_content_sources = [Rails.root]
```
## Debugging
If classes are missing from `avo/application.css`, check these files first:
- input file generated by Avo: `tmp/avo/avo.tailwind.input.css`
- output file generated by Tailwind: `app/assets/builds/avo/application.css`
How it works, in short:
- Avo generates `tmp/avo/avo.tailwind.input.css` on each build/watch cycle.
- That input file imports Avo/plugin `application.css` files and your host styles from `app/assets/stylesheets/avo/**/*.css`.
- It also adds `@source` entries for Avo, loaded plugins, and your configured host directories (default: `Rails.root.join("app")`) so Tailwind can discover utility classes from templates and code.
- Tailwind then compiles everything into `app/assets/builds/avo/application.css`.
Quick checks:
- confirm the integration is enabled (`config.tailwindcss_integration_enabled = true`);
- ensure `tailwindcss-ruby` is installed (directly or via `tailwindcss-rails`);
- verify your custom styles are under `app/assets/stylesheets/avo/`;
- run with the watcher (`bin/dev`) so changes are rebuilt continuously.
---
# Dashboards
:::warning
You must manually require the `chartkick` gem in your `Gemfile`.
```ruby
# Create beautiful JavaScript charts with one line of Ruby
gem "chartkick"
```
:::
There comes the point in your app's life when you need to display the data in an aggregated form like a metric or chart. That's what Avo's Dashboards are all about.
## Generate a dashboard
Run `bin/rails g avo:dashboard my_dashboard` to get a shiny new dashboard.
```ruby
class Avo::Dashboards::MyDashboard < Avo::Dashboards::BaseDashboard
self.id = 'my_dashboard'
self.name = 'Dashy'
self.description = 'The first dashbaord'
self.grid_cols = 3
def cards
card Avo::Cards::ExampleMetric
card Avo::Cards::ExampleAreaChart
card Avo::Cards::ExampleScatterChart
card Avo::Cards::PercentDone
card Avo::Cards::AmountRaised
card Avo::Cards::ExampleLineChart
card Avo::Cards::ExampleColumnChart
card Avo::Cards::ExamplePieChart
card Avo::Cards::ExampleBarChart
divider label: "Custom partials"
card Avo::Cards::ExampleCustomPartial
card Avo::Cards::MapCard
end
end
```
## Settings
Each dashboard is a file. It holds information about itself like the `id`, `name`, `description`, and how many columns its grid has.
The `id` field has to be unique. The `name` is what the user sees in big letters on top of the page, and the `description` is some text you pass to give the user more details regarding the dashboard.
Using the ' grid_cols ' parameter, you may organize the cards in a grid with `3`, `4`, `5`, or `6` columns using the `grid_cols` parameter. The default is `3`.
## Cards
This section has moved.
### Override card arguments from the dashboard
We found ourselves in the position to add a few cards that were the same card but with a slight difference. Ex: Have one `Users count` card and another `Active users count` card. They both count users, but the latter has an `active: true` condition applied.
Before, we'd have to duplicate that card and modify the `query` method slightly but end up with duplicated boilerplate code.
For those scenarios, we created the `arguments` attribute. It allows you to send arbitrary arguments to the card from the parent.
```ruby{7-9}
class Avo::Dashboards::Dashy < Avo::Dashboards::BaseDashboard
self.id = "dashy"
self.name = "Dashy"
def cards
card Avo::Cards::UsersCount
card Avo::Cards::UsersCount, arguments: {
active_users: true
}
end
end
```
Now we can pick up that option in the card and update the query accordingly.
```ruby{9-11}
class Avo::Cards::UsersCount < Avo::Cards::MetricCard
self.id = "users_metric"
self.label = "Users count"
# You have access to context, params, range, current parent, and current card
def query
scope = User
if arguments[:active_users].present?
scope = scope.active
end
result scope.count
end
end
```
That gives you an extra layer of control without code duplication and the best developer experience.
#### Control the base settings from the parent
Evidently, you don't want to show the same `label`, `description`, and other details for that second card from the first card.
Therefore, you can control the `label`, `description`, `cols`, `rows`, `visible`, and `refresh_every` arguments from the parent declaration.
```ruby{8-16}
class Avo::Dashboards::Dashy < Avo::Dashboards::BaseDashboard
self.id = "dashy"
self.name = "Dashy"
def cards
card Avo::Cards::UsersCount
card Avo::Cards::UsersCount,
label: "Active users",
description: "Active users count",
cols: 2,
rows: 2,
visible: -> { true }
refresh_every: 2.minutes,
arguments: {
active_users: true
}
end
end
```
## Dashboards visibility
You might want to hide specific dashboards from certain users. You can do that using the `visible` option. The option can be a boolean `true`/`false` or a block where you have access to the `params`, `current_user`, `context`, and `dashboard`.
If you don't pass anything to `visible`, the dashboard will be available for anyone.
```ruby{5-11}
class Avo::Dashboards::ComplexDash < Avo::Dashboards::BaseDashboard
self.id = "complex_dash"
self.name = "Complex dash"
self.description = "Complex dash description"
self.visible = -> do
current_user.is_admin?
# or
params[:something] == 'something else'
# or
context[:your_param] == params[:something_else]
end
def cards
card Avo::Cards::UsersCount
end
end
```
## Dashboards authorization
You can set authorization rules for dashboards using the `authorize` block.
```ruby{3-6}
class Avo::Dashboards::Dashy < Avo::Dashboards::BaseDashboard
self.id = 'dashy'
self.authorize = -> do
# You have access to current_user, params, request, context, adn view_context.
current_user.is_admin?
end
end
```
`self.name` is what is going to be displayed to the user as the dashboard name.
```ruby
self.name = "Dashy"
```
`self.name` can be configured using a Proc.
```ruby
self.name = -> { I18n.t("avo.dashboards.dashy.name") }
```
Within this block, you gain access to all attributes of `Avo::ExecutionContext` along with the `dashboard`.
---
# Cards
Cards are one way of quickly adding custom content for your users.
Cards can be used on dashboards or resources, we'll refer to both of them as "parent" since they're hosting the cards.
You can add three types of cards to your parent: `partial`, `metric`, and `chartkick`.
## Base settings
All cards have some standard settings like `id`, which must be unique, `label` and `description`. The `label` will be the title of your card, and `description` will show a tiny question mark icon on the bottom right with a tooltip with that description.
Each card has its own `cols` and `rows` settings to control the width and height of the card inside the parent's grid. They can have values from `1` to `6`.
All this settings can be called as an lambda.
The lambda will be executed using `Avo::ExecutionContext`. Within this blocks, you gain access to all attributes of `Avo::ExecutionContext` along with the `parent`, `resource`, `dashboard` and `card`.
```ruby{2-7}
class Avo::Cards::UsersMetric < Avo::Cards::MetricCard
self.id = "users_metric"
self.label = -> { "Users count" }
self.description = -> { "Users description" }
self.cols = 1
self.rows = 1
self.display_header = true
end
```
## Ranges
#### Control the aggregation using ranges
You may also want to give the user the ability to query data in different ranges. You can control what's passed in the dropdown using the' ranges' attribute. The array passed here will be parsed and displayed on the card. All integers are transformed to days, and other string variables will be passed as they are.
You can also set a default range using the `initial_range` attribute.
The ranges have been changed a bit since **version 2.8**. The parameter you pass to the `range` option will be directly passed to the [`options_for_select`](https://apidock.com/rails/v5.2.3/ActionView/Helpers/FormOptionsHelper/options_for_select) helper, so it behaves more like a regular `select_tag`.
```ruby{4-15}
class Avo::Cards::UsersMetric < Avo::Cards::MetricCard
self.id = 'users_metric'
self.label = 'Users count'
self.initial_range = 30
self.ranges = {
"7 days": 7,
"30 days": 30,
"60 days": 60,
"365 days": 365,
Today: "TODAY",
"Month to date": "MTD",
"Quarter to date": "QTD",
"Year to date": "YTD",
All: "ALL"
}
end
```
## Keep the data fresh
If the parent is something that you keep on the big screen, you need to keep the data fresh at all times. That's easy using `refresh_every`. You pass the number of seconds you need to be refreshed and forget about it. Avo will do it for you.
```ruby{3}
class Avo::Cards::UsersMetric < Avo::Cards::MetricCard
self.id = 'users_metric'
self.refresh_every = 10.minutes
end
```
## Hide the header
In cases where you need to embed some content that should fill the whole card (like a map, for example), you can choose to hide the label and ranges dropdown.
```ruby{3}
class Avo::Cards::UsersMetric < Avo::Cards::MetricCard
self.id = 'users_metric'
self.display_header = false
end
```
## Format
Option `self.format` is useful when you want to format the data that `result` returns from `query`.
Example without format:
```ruby
class Avo::Cards::AmountRaised < Avo::Cards::MetricCard
self.id = "amount_raised"
self.label = "Amount raised"
self.prefix = "$"
def query
result 9001
end
end
```
Example with format:
```ruby
class Avo::Cards::AmountRaised < Avo::Cards::MetricCard
self.id = "amount_raised"
self.label = "Amount raised"
self.prefix = "$"
self.format = -> {
number_to_social value, start_at: 1_000
}
def query
result 9001
end
end
```
## Metric card
The metric card is your friend when you only need to display a simple big number. To generate one run `bin/rails g avo:card users_metric --type metric`.
#### Calculate results
To calculate your result, you may use the `query` method. After you make the query, use the `result` method to store the value displayed on the card.
In the `query` method you have access to a few variables like `context` (the App context), `params` (the request params), `range` (the range that was requested), `dashboard`, `resource` or `parent` (the current dashboard or resource the card is on), and current `card`.
```ruby{23-47,36}
class Avo::Cards::UsersMetric < Avo::Cards::MetricCard
self.id = 'users_metric'
self.label = 'Users count'
self.description = 'Some tiny description'
self.cols = 1
# self.rows = 1
# self.initial_range = 30
# self.ranges = {
# "7 days": 7,
# "30 days": 30,
# "60 days": 60,
# "365 days": 365,
# Today: "TODAY",
# "Month to date": "MTD",
# "Quarter to date": "QTD",
# "Year to date": "YTD",
# All: "ALL",
# }
# self.prefix = '$'
# self.suffix = '%'
# self.refresh_every = 10.minutes
def query
from = Date.today.midnight - 1.week
to = DateTime.current
if range.present?
if range.to_s == range.to_i.to_s
from = DateTime.current - range.to_i.days
else
case range
when 'TODAY'
from = DateTime.current.beginning_of_day
when 'MTD'
from = DateTime.current.beginning_of_month
when 'QTD'
from = DateTime.current.beginning_of_quarter
when 'YTD'
from = DateTime.current.beginning_of_year
when 'ALL'
from = Time.at(0)
end
end
end
result User.where(created_at: from..to).count
end
end
```
### Decorate the data using `prefix` and `suffix`
Some metrics might want to add a `prefix` or a `suffix` to display the data better.
```ruby{3,4}
class Avo::Cards::UsersMetric < Avo::Cards::MetricCard
self.id = 'users_metric'
self.prefix = '$'
self.suffix = '%'
end
```
`prefix` and `suffix` became callable options.
The blocks are executed using `Avo::ExecutionContext`. Within this blocks, you gain access to all attributes of `Avo::ExecutionContext` along with the `parent`.
```ruby{3,4}
class Avo::Cards::UsersMetric < Avo::Cards::MetricCard
self.id = 'users_metric'
self.prefix = -> { params[:prefix] || parent.prefix }
self.suffix = -> { params[:suffix] || parent.suffix }
end
```
## Chartkick card
A picture is worth a thousand words. So maybe a chart a hundred? Who knows? But creating charts in Avo is very easy with the help of the [chartkick](https://github.com/ankane/chartkick) gem.
You start by running `bin/rails g avo:card users_chart --type chartkick`.
```ruby
class Avo::Cards::UserSignups < Avo::Cards::ChartkickCard
self.id = 'user_signups'
self.label = 'User signups'
self.chart_type = :area_chart
self.description = 'Some tiny description'
self.cols = 2
# self.rows = 1
# self.chart_options = { library: { plugins: { legend: { display: true } } } }
# self.flush = true
# self.legend = false
# self.scale = false
# self.legend_on_left = false
# self.legend_on_right = false
def query
points = 16
i = Time.new.year.to_i - points
base_data =
Array
.new(points)
.map do
i += 1
[i.to_s, rand(0..20)]
end
.to_h
data = [
{ name: 'batch 1', data: base_data.map { |k, v| [k, rand(0..20)] }.to_h },
{ name: 'batch 2', data: base_data.map { |k, v| [k, rand(0..40)] }.to_h },
{ name: 'batch 3', data: base_data.map { |k, v| [k, rand(0..10)] }.to_h }
]
result data
end
end
```
### Chart types
Using the `self.chart_type` class attribute you can change the type of the chart. Supported types are `line_chart`, `pie_chart`, `column_chart`, `bar_chart`, `area_chart`, and `scatter_chart`.
### Customize chart
Because the charts are being rendered with padding initially, we offset that before rendering to make the chart look good on the card. To disable that, you can set `self.flush = false`. That will set the chart loose for you to customize further.
After you set `flush` to `false`, you can add/remove the `scale` and `legend`. You can also place the legend on the left or right using `legend_on_left` and `legend_on_right`.
These are just some of the predefined options we provide out of the box, but you can send different [chartkick options](https://github.com/ankane/chartkick#options) to the chart using `chart_options`.
If you'd like to use [Groupdate](https://github.com/ankane/groupdate), [Hightop](https://github.com/ankane/hightop), and [ActiveMedian](https://github.com/ankane/active_median) you should require them in your `Gemfile`. Only `chartkick` is required by default.
`chart.js` is supported for the time being. So if you need support for other types, please reach out or post a PR (π PRs are much appreciated).
`self.chartkick_options` accepts callable blocks:
```ruby
class Avo::Cards::ExampleAreaChart < Avo::Cards::ChartkickCard
self.chart_options: -> do
{
library: {
plugins: {
legend: {display: true}
}
}
}
end
end
```
`chartkick_options` can also be declared when registering the card:
```ruby
class Avo::Dashboards::Dashy < Avo::Dashboards::BaseDashboard
def cards
card Avo::Cards::ExampleAreaChart,
chart_options: {
library: {
plugins: {
legend: {display: true}
}
}
}
# OR
card Avo::Cards::ExampleAreaChart,
chart_options: -> do
{
library: {
plugins: {
legend: {display: true}
}
}
}
end
end
end
```
The blocks are executed using `Avo::ExecutionContext`. Within this blocks, you gain access to all attributes of `Avo::ExecutionContext` along with the `parent`, `arguments` and `result_data`.
## Partial card
You can use a partial card to add custom content to a card. Generate one by running `bin/rails g avo:card custom_card --type partial`. That will create the card class and the partial for it.
```ruby{5}
class Avo::Cards::ExampleCustomPartial < Avo::Cards::PartialCard
self.id = "users_custom_card"
self.cols = 1
self.rows = 4
self.partial = "avo/cards/custom_card"
# self.display_header = true
end
```
You can embed a piece of content from another app using an iframe. You can hide the header using the `self.display_header = false` option. That will render the embedded content flush to the container.
```ruby{5}
# app/avo/cards/map_card.rb
class Avo::Cards::MapCard < Avo::Cards::PartialCard
self.id = "map_card"
self.label = "Map card"
self.partial = "avo/cards/map_card"
self.display_header = false
self.cols = 2
self.rows = 4
end
```
```html
```
## Cards visibility
It's common to show the same card to multiple types of users (admins, regular users). In that scenario you might want to hide some cards for the regular users and show them just to the admins.
You can use the `visible` option to do that. It can be a `boolean` or a `block` where you can access the `params`, `current_user`, `context`, `parent`, and `card` object.
```ruby{4-11}
class Avo::Cards::UsersCount < Avo::Cards::MetricCard
self.id = "users_metric"
self.label = "Users count"
self.visible = -> do
# You have access to:
# context
# params
# parent (the current dashboard or resource)
# dashboard (will be nil when parent is resource)
# resource (will be nil when parent is dashboard)
# current card
true
end
def query
result User.count
end
end
```
You may also control the visibility from the parent class.
:::code-group
```ruby [On Dashboards]
class Avo::Dashboards::Dashy < Avo::Dashboards::BaseDashboard
def cards
card Avo::Cards::UsersCount, visible: -> { true }
end
end
```
```ruby [On Resources]
class Avo::Resources::User < Avo::BaseResource
def cards
card Avo::Cards::UsersCount, visible: -> { true }
end
end
```
:::
## Dividers
You may want to separate the cards. You can use dividers to do that.
```ruby [On Dashboards]
class Avo::Dashboards::Dashy < Avo::Dashboards::BaseDashboard
def cards
card Avo::Cards::ExampleColumnChart
card Avo::Cards::ExamplePieChart
card Avo::Cards::ExampleBarChart
divider label: "Custom partials"
card Avo::Cards::ExampleCustomPartial
card Avo::Cards::MapCard
end
end
```
Dividers can be a simple line between your cards or have some text on them that you control using the `label` option.
When you don't want to show the line, you can enable the `invisible` option, which adds the divider but does not display a border or label.
## Dividers visibility
You might want to conditionally show/hide a divider based on a few factors. You can do that using the `visible` option.
```ruby
divider label: "Custom partials", visible: -> {
# You have access to:
# context
# params
# parent (the current dashboard or resource)
# dashboard (will be nil when parent is resource)
# resource (will be nil when parent is dashboard)
true
}
```
## View-specific card methods
Similar to view-specific field methods like `index_fields` and `show_fields`, resources can define view-specific card methods to control which cards render on each page.
### Resolution order by view
| View | Specific method | Context fallback | Final fallback |
| --- | --- | --- | --- |
| Index | `index_cards` | `display_cards` | `cards` |
| Show | `show_cards` | `display_cards` | `cards` |
| New | `new_cards` | `form_cards` | `cards` |
| Edit | `edit_cards` | `form_cards` | `cards` |
Avo picks the first method available in the order listed above for the current view.
### Example
Assume this card class:
```ruby
class Avo::Cards::AmountRaised < Avo::Cards::MetricCard
self.id = "amount_raised"
self.label = "Amount raised"
self.prefix = "$"
self.format = -> {
number_to_social value, start_at: 1_000
}
def query
result 9001
end
end
```
Define where it should appear on the `Project` resource:
```ruby
class Avo::Resources::Project < Avo::BaseResource
# Show page uses `show_cards` first
def show_cards
card Avo::Cards::AmountRaised
end
# Index and show pages fall back to `display_cards` only when
# their specific method (`index_cards`/`show_cards`) is not defined
def display_cards
card Avo::Cards::AmountRaised
end
# New and edit pages fall back to `form_cards` when `new_cards`/`edit_cards` are not defined
def form_cards
card Avo::Cards::AmountRaised
end
end
```
With the setup above, the card will render on the Project show page via `show_cards`. If you remove `show_cards`, Avo will use `display_cards` for the show page. For new/edit pages, Avo will use `form_cards` unless you define `new_cards` or `edit_cards` respectively.
---
# Kanban boards
:::warning
The feature and docs are both work in progress. Please read the `info` sections below.
:::
Having a kanban board is a great way to organize your work and keep track of your records.
## Overview
The Kanban Board feature is a way to create a kanban board for your resources. They support multiple resources. Think about GitHub's Projects. You can have Issues, PRs, and simple tasks on them.
The boards and columns and items are database backed and you can create them on the fly.
## Requirements
Some of these requirements might change over time.
- We tested this on an app with Avo Advanced license
- [`acts_as_list`](https://github.com/brendon/acts_as_list) gem (comes automatically as a requirement)
- [`hotwire_combobox`](https://github.com/josefarias/hotwire_combobox) gem (comes automatically as a requirement)
## Installation
To install the `avo-kanban` gem, follow the steps below:
1. Add the following line to your Gemfile:
```ruby
gem "avo-kanban", source: "https://packager.dev/avo-hq/"
```
2. Run the `bundle install` command to install the gem:
```bash
bundle install
```
3. Generate the necessary resources and controllers by running:
```bash
rails generate avo:kanban install
```
This command will create pre-configured resources and controllers for managing boards, columns, and items in your application. You can further customize the generated code to suit your needs.
This command will also generate the item's partial and a migration.
4. Run the migration to apply the database changes:
```bash
rails db:migrate
```
## DB schema
`Avo::Kanban::Board` -> has_many `Avo::Kanban::Column` -> has_many `Avo::Kanban::Item`
The `Avo::Kanban::Column` has a polymorphic `belongs_to` association with any other model you might have in your app.
## Create a kanban board
We can create a kanban board by going to the Boards resource and clicking on the `Create board` button.
Once you create the board, add it to the menu using the `link_to` option (for now. we'll add `board` soon).
## Create columns
For now you can create the columns from the resource view.
By default, each column will have a `name` and `value` assigned to it. It will also have a `position` that you can use to sort the columns.
The `value` is what is being used to update the record when it's dropped into a new column.
## Configure the board
Each board has a configuration attached to it.
We can configure what kind of resources can be added to the board.
Similar we can change the column names and the value from the settings screen.
## Adding items to the board
This is best done on the board. Under each column you'll find the new field. This will search throught the resources that you've selected in the configuration.
It will use the `self.search[:query]` block to search for the records. It will send two `for_kanban_board` and `board_id` arguments to the block so you can customize the query.
When an item is added to the a column, it will have an `Avo::Kanban::Item` record created for it. This `Item` record is responsible for keeping track of the board, column, position properties and more.
When an item is added to the a column it will update the property on the record to the column's `value`. More on what this means in the next section.
## How does it work?
Each board updates one `property` on the `record`, and each column represents a `value`.
The record is the actual record from the database (User, Project, To Do, etc.).
Let's say we are replicating the GitHub Projects boards.
### `Board` and `Column`s
We should have a `Board` record with the following columns:
- `No status` with an empty string as value
- `Backlog` with the value `backlog`
- `In progress` with the value `in_progress`
- `Done` with the value `done`
The board has the `property` option set to `status` so we ensure that the `status` property of the record is updated when we move the item to a new column.
### `Resource`s and `Item`s
We should have `Issue`, `PullRequest`, and `ToDo` models and resources. The resources should have the `self.search[:query]` block configured.
Each resource must have the `self.title` method configured. This title will be used as a label to identify records throughout the kanban board, including in the search box and on individual entries.
Next in our board we should select these resources as allowed from the board settings.
### Add items to the board
At the bottom of the `No status` column we can search for an `Issue`. When we select that issue, an `Avo::Kanban::Item` record will be created for it with references to the board, column, and record (that issue).
This automatically triggers the issue to change the status to an empty string because we added it to the `No status` column which has the `value` set to an empty string.
If we were to add it to the `Backlog` column, it would change the status to `backlog`.
### Move items between columns
Now, if we move the item to the `In progress` column, it will change the status to `in_progress`.
### Items without that property
Some models might belong on the same board but have different properties to show the status.
Some models might use a timestamp like `published_at` to show the status.
Or some models might belong to a a status but that isn't dictated by a single property but a collection of properties.
In order to mitigate that we can create virtual properties on the model.
Let's imagine that a new board that displays the posts in columns based on their "published" status. the board uses the `status` property to but the `Post` model doesn't have the `status` property as a column in the database.
We can create a virtual property on the model.
```ruby
class Post < ApplicationRecord
def status
if published_at.present?
"published"
elsif published_status == "draft"
"draft"
else
"private"
end
end
def status=(value)
if value == "published"
published_at = Time.now
published_status = "draft"
elsif value == "draft"
published_at = nil
published_status = "draft"
elsif value == "draft"
published_at = nil
published_status = nil
end
save!
end
end
```
## Customize the card
:::warning
This might change in the future.
:::
In order to customize the card, you can eject the `Avo::Kanban::Items::ItemComponent` component.
```bash
rails generate avo:eject --component Avo::Kanban::Items::ItemComponent
```
Then customize it at `app/components/avo/kanban/items/item_component.html.erb`
```erb
<%= item.record.name %>
```
The `item` is the `Avo::Kanban::Item` and the `record` is the actual record from the database.
## Authorization
This section assumes that you have already set up authorization in your application using Pundit.
1. Generate a policy for the `Board` resource by running:
```bash
rails generate pundit:policy board
```
### Authorization Methods
You can control access to various parts of the Kanban board by defining the following methods in your `BoardPolicy`:
- `manage_column?`
Controls the visibility of the three-dot menu on each column (used for column management).
- `edit?`
Controls the "Edit board" button on the board itself.
:::warning
Also controls the ability to edit the board in the resource view.
:::
- `add_item?`
Controls the visibility of the "Add item" button on the board, which allows users to add new items to a column.
:::warning
Doesn't impact the ability to add items via the bottom of each column.
:::
## Customizing kanban models for your business logic
Sometimes, the default kanban models aren't quite enough for your specific use case. Let's say you're building a project management system where kanban boards need to belong to specific teams, and each column represents a workflow stage that needs to track additional metadata like SLA targets or approval requirements.
In this scenario, you might need to extend the kanban models to add custom associations, validations, or callbacks that align with your business logic. Here's how you can safely extend the `Avo::Kanban::Board`, `Avo::Kanban::Column`, and `Avo::Kanban::Item` models:
```ruby{7-54}
# config/initializers/avo.rb
Avo.configure do |config|
config.root_path = '/admin'
# ... other config options ...
end
Rails.configuration.to_prepare do
Avo::Kanban::Board.class_eval do
belongs_to :team, optional: true
has_many :board_watchers, dependent: :destroy
validates :name, presence: true, uniqueness: { scope: :team_id }
end
Avo::Kanban::Column.class_eval do
belongs_to :workflow_stage, optional: true
has_one :sla_config, dependent: :destroy
after_update :notify_stage_change
private
def notify_stage_change
# Custom logic to notify team members of stage changes
BoardNotificationService.new(self).notify_stage_update
end
end
Avo::Kanban::Item.class_eval do
has_many :item_comments, dependent: :destroy
belongs_to :assignee, class_name: 'User', optional: true
after_destroy :cleanup_item_data
before_update :track_movement_history
private
def cleanup_item_data
# Clear any business-specific property when item is removed
record.update!("#{board.property}": nil)
end
def track_movement_history
if column_id_changed?
ItemMovementTracker.create!(
item: self,
from_column_id: column_id_was,
to_column_id: column_id,
moved_at: Time.current
)
end
end
end
end
```
This approach allows you to seamlessly integrate the kanban functionality with your existing domain models while maintaining the flexibility to add custom business logic as your application grows.
---
# Forms & Pages
Build structured configuration and settings interfaces in your Avo admin using standalone forms and organized page hierarchies β no database models required.
Forms handle custom data processing, settings management, and workflows. Pages organize those forms into a sidebar-navigable interface with a main page and sub-pages.
## Requirements
- Avo >= 4.0
- An active Avo license with the Forms add-on enabled
## Installation
### 1. Add the gem
Add `avo-forms` to your `Gemfile`:
```ruby
gem "avo-forms", source: "https://packager.dev/avo-hq/"
```
Then install it:
```bash
bundle install
```
### 2. Verify the engine loads
The engine registers itself automatically via `Avo.plugin_manager`. No initializer changes are required β start creating forms and pages right away.
## Quick start
### Create your first form
Generate a form with:
```bash
rails generate avo:form general_settings
```
This creates `app/avo/forms/general_settings.rb`. Edit it to add fields and handle submission:
```ruby
# app/avo/forms/general_settings.rb
class Avo::Forms::GeneralSettings < Avo::Forms::Core::Form
self.title = "General Settings"
self.description = "Configure your application"
def fields
field :app_name, as: :text, required: true
field :maintenance_mode, as: :boolean, default: false
end
def handle
# params contains submitted form data
flash[:notice] = "Settings saved"
default_response
end
end
```
### Create a page to host your form
Generate a page:
```bash
rails generate avo:page settings
```
Edit `app/avo/pages/settings.rb` to wire in your form:
```ruby
# app/avo/pages/settings.rb
class Avo::Pages::Settings < Avo::Forms::Core::Page
self.title = "Settings"
self.description = "Manage your application settings"
def navigation
page Avo::Pages::Settings::General, default: true
end
end
```
Create the sub-page `app/avo/pages/settings/general.rb`:
```ruby
# app/avo/pages/settings/general.rb
class Avo::Pages::Settings::General < Avo::Forms::Core::Page
self.title = "General"
def content
form Avo::Forms::GeneralSettings
end
end
```
### Add the page to your Avo menu
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.main_menu = -> {
section "Configuration", icon: "cog" do
page Avo::Pages::Settings, icon: "adjustments"
end
}
end
```
Users can now navigate to **Settings β General** in the sidebar and submit the form.
## How it works
- **Forms** inherit from `Avo::Forms::Core::Form`. They define fields via `def fields` and handle submissions via `def handle`. The `handle` method runs in the controller context, so `params`, `current_user`, `flash`, and `cookies` are all available directly.
- **Pages** inherit from `Avo::Forms::Core::Page`. A **main page** (one namespace level deep, e.g. `Avo::Pages::Settings`) acts as a container and defines navigation. **Sub-pages** (deeper, e.g. `Avo::Pages::Settings::General`) define the actual content via `def content`.
- **Routes** are generated dynamically at boot time from your form and page class definitions β no manual route declarations needed.
- **Rendering**: Pages render with a sidebar navigation listing all sub-pages. Each sub-page shows its title, description, and all registered forms in order.
## Next steps
- Forms β field types, panels, record binding, flash messages, and full examples
- Pages β hierarchy, virtual pages, navigation configuration, menu integration
- Generators β all generator commands and generated file templates
- Guides & Tutorials β form organization strategies and page patterns
---
# Generators
Avo Forms provides generators to help you quickly create forms and pages.
## Form Generator
```bash
rails generate avo:form your_form_name
```
This will create a new form file at `app/avo/forms/your_form_name.rb` with the following structure:
```ruby
# app/avo/forms/your_form_name.rb
class Avo::Forms::YourFormName < Avo::Forms::Core::Form
self.title = "Your Form Name"
self.description = "Manage your your form name"
def fields
field :example, as: :text, default: "Hello World"
end
def handle
flash[:success] = { body: "Form submitted successfully", timeout: :forever }
flash[:notice] = params[:example]
default_response
end
end
```
## Page Generator
The page generator creates a new page class.
```bash
rails generate avo:page your_page_name
```
This will create a new page file at `app/avo/pages/your_page_name.rb` with the following structure:
```ruby
class Avo::Pages::YourPageName < Avo::Forms::Core::Page
self.title = "Your Page Name"
self.description = "A page for your page name"
# self.navigation_label = "Your Page Name"
def content
# form Avo::Forms::AnyFormClass
end
def navigation
# Class-based page
# page Avo::Pages::AnySubPageClass
# Virtual page with form, this page will be displayed in the navigation menu and when the user clicks on it, it will display the form.
# page form: Avo::Forms::AnyFormClass
# Virtual page with custom content, this page will be displayed in the navigation menu and when the user clicks on it, it will display the custom content.
# page "Custom Page",
# description: "A page for custom page",
# content: -> do
# form Avo::Forms::SomeForm
# end
end
end
```
:::warning Boot-time Parsing
The `def content` and `def navigation` methods are parsed only once during application boot. Do not use conditional logic (if/else statements) or dynamic content inside these methods, as they will not be re-evaluated during runtime.
:::
:::tip
To create a sub-page, you need to create a page first. The sub-page need to be namespaced under the parent page.
Read more about the Page Hierarchy.
Example:
```ruby
# app/avo/pages/parent_page.rb
class Avo::Pages::ParentPage < Avo::Forms::Core::Page
self.title = "Parent Page"
self.description = "A page for parent page"
def navigation
page Avo::Pages::ParentPage::SubPage
end
end
```
```ruby
# app/avo/pages/parent_page/sub_page.rb
class Avo::Pages::ParentPage::SubPage < Avo::Forms::Core::Page
self.title = "Sub Page"
self.description = "A page for sub page"
end
```
:::
## Best Practices
1. Use descriptive names for your forms and pages
2. Keep form fields focused and relevant to their purpose
3. Organize related forms and pages together
4. Use sub-pages to create a logical navigation structure
5. Add appropriate descriptions to help users understand the purpose of each form and page
---
# Pages
Avo provides a powerful page system that enables you to build structured interfaces with nested content organization. Pages in Avo work similarly to resources but are designed for displaying and managing forms and sub-pages rather than database records.
## Hierarchy
Pages are organized in a hierarchical structure that follows a specific pattern:
### Page Structure
1. **Main Pages**
Example: `Avo::Pages::Settings`
- Always 1 level deep in the namespace
- Act as containers for related sub-pages
- Have a navigation entry on the left sidebar menu (if `self.menu_entry` is `true`)
- When accessed directly, redirect to the default sub-page if one is configured
2. **Sub-Pages**
Example: `Avo::Pages::Settings::General`, `Avo::Pages::Settings::Notifications`
- Always 2 or more levels deep in the namespace
- Contain the actual forms and content
- Are accessible through the parent page's navigation
This hierarchical organization allows you to create structured interfaces where users can navigate through different sections and manage various settings or configurations.
### Navigation Behavior
When a user visits a main page that has sub-pages:
1. If a default sub-page is configured, the user is automatically redirected to it
2. If no default sub-page is configured, the main page is displayed with its own forms (if any)
3. Sub-pages are displayed in a sidebar navigation for easy access
## Generating Pages
The generator usage is documented in the Generators page.
## Page Configuration Options
Pages have several class attributes that you can configure to customize their behavior and appearance.
Sets the display title for the page.
```ruby{3}
# app/avo/pages/settings.rb
class Avo::Pages::Settings < Avo::Forms::Core::Page
self.title = "Application Settings"
end
```
Provides a description that appears below the page title.
```ruby{3}
# app/avo/pages/settings.rb
class Avo::Pages::Settings < Avo::Forms::Core::Page
self.description = "Manage your application settings and preferences"
end
```
Customizes the label displayed in the Avo menu entry and page sidebar menu entry.
**Default behavior:**
- If `navigation_label` is not set, it defaults to the `title`
- If `title` is not set, it takes the last namespace from the class and humanizes it
```ruby{3}
# app/avo/pages/settings.rb
class Avo::Pages::Settings < Avo::Forms::Core::Page
self.navigation_label = "App Configuration"
end
```
This is particularly useful when you want different text in navigation menus than what's displayed as the page title, or when you want to shorten long titles for better menu presentation.
## Page Methods
Define sub-pages that belong to this page. This method is used to register child pages and configure their relationship to the parent.
:::warning Boot-time Parsing
The `def navigation` method is parsed only once during application boot. Do not use conditional logic (if/else statements) or dynamic content inside this method, as it will not be re-evaluated during runtime.
:::
```ruby{3-5}
# app/avo/pages/settings.rb
class Avo::Pages::Settings < Avo::Forms::Core::Page
def navigation
# ...
end
end
```
This method is used to register sub pages that belong to this page. The `page` method supports three different types of declarations:
#### 1. Class-based pages (backed by a file)
The traditional approach where you reference an existing page class.
```ruby
# app/avo/pages/settings.rb
class Avo::Pages::Settings < Avo::Forms::Core::Page
def navigation
page Avo::Pages::Settings::General # [!code focus]
end
end
```
#### 2. Virtual pages that render a form
Create a "virtual" page that directly renders a form without requiring a separate page file. If title and description are not provided, the page will use the form's title and description.
```ruby
# app/avo/pages/settings.rb
class Avo::Pages::Settings < Avo::Forms::Core::Page
def navigation
page form: Avo::Forms::Profiles # [!code focus]
end
end
```
#### 3. Virtual pages with custom content
Create a "virtual" page with a custom title and content block that can contain multiple forms.
```ruby
# app/avo/pages/settings.rb
class Avo::Pages::Settings < Avo::Forms::Core::Page
def navigation
page "Settings", # [!code focus]
description: "Manage your application settings and preferences", # [!code focus]
content: -> do # [!code focus]
form Avo::Forms::General # [!code focus]
form Avo::Forms::Integrations # [!code focus]
end # [!code focus]
end
end
```
### Page Options
When defining pages, you can pass additional options:
#### `default`
Marks a page as the default one to display when the main page is accessed. Available for all page types.
**Default value**: `false`
**Possible values**: `true` or `false`
```ruby
# app/avo/pages/settings.rb
class Avo::Pages::Settings < Avo::Forms::Core::Page
def navigation
page Avo::Pages::Settings::General, default: true # [!code focus]
page form: Avo::Forms::Profiles
page "Custom Settings",
content: -> { form Avo::Forms::Custom },
default: false
end
end
```
Define forms that should be displayed on this page. Forms are the primary way users interact with your page's functionality.
:::warning Boot-time Parsing
The `def content` method is parsed only once during application boot. Do not use conditional logic (if/else statements) or dynamic content inside this method, as it will not be re-evaluated during runtime.
:::
```ruby{3-5}
# app/avo/pages/settings/general.rb
class Avo::Pages::Settings::General < Avo::Forms::Core::Page
def content
# ...
end
end
```
You can define multiple forms on a single page, and they will be displayed in the order you declare them.
This method is used to register forms that should be displayed on this page.
It expects the form class as the first argument.
```ruby
# app/avo/pages/settings/general.rb
class Avo::Pages::Settings::General < Avo::Forms::Core::Page
def content
form Avo::Forms::Settings::AppSettings # [!code focus]
form Avo::Forms::Settings::ProfileSettings # [!code focus]
end
end
```
## Complete Example
Here's a complete example showing a settings page with multiple sub-pages:
```ruby
# Main settings page
# app/avo/pages/settings.rb
class Avo::Pages::Settings < Avo::Forms::Core::Page
self.title = "Settings"
self.description = "Manage your application settings"
self.navigation_label = "App Settings"
def navigation
page Avo::Pages::Settings::General, default: true
page form: Avo::Forms::UserProfiles
page "Integrations",
content: -> do
form Avo::Forms::Settings::SlackIntegration
form Avo::Forms::Settings::EmailIntegration
end
page Avo::Pages::Settings::Security
end
end
```
```ruby
# General settings sub-page
# app/avo/pages/settings/general.rb
class Avo::Pages::Settings::General < Avo::Forms::Core::Page
self.title = "General Settings"
self.description = "Basic application configuration"
def content
form Avo::Forms::Settings::AppSettings
form Avo::Forms::Settings::CompanyInfo
end
end
```
```ruby
# Notifications sub-page
# app/avo/pages/settings/notifications.rb
class Avo::Pages::Settings::Notifications < Avo::Forms::Core::Page
self.title = "Notifications"
self.description = "Configure notification preferences"
def content
form Avo::Forms::Settings::EmailNotifications
form Avo::Forms::Settings::SlackIntegration
end
end
```
## Adding Pages to the Menu
To make your pages accessible through Avo's main navigation, add them to your Avo configuration:
```ruby{9-10,12,14}
# config/initializers/avo.rb
Avo.configure do |config|
config.main_menu = -> {
section "Resources", icon: "avo/resources" do
all_resources
end
section "Configuration", icon: "cog" do
page Avo::Pages::Settings, icon: "adjustments"
page Avo::Pages::SystemHealth, icon: "heart"
# Or
all_pages
end
}
end
```
## Best Practices
**Keep the hierarchy shallow**: While you can nest pages deeply, it's best to keep the structure simple with main pages and one level of sub-pages.
**Set default sub-pages**: If your main page primarily serves as a container, always set a default sub-page to improve user experience.
**Use descriptive titles and descriptions**: Help users understand what each page contains and what actions they can perform.
**Customize navigation labels**: Use `navigation_label` to provide concise, menu-friendly names that may differ from your page titles, especially for long or technical titles.
**Choose the right page type**:
- Use **class-based pages** for complex pages with custom logic or multiple forms
- Use **virtual pages with forms** for simple, single-form pages to reduce file clutter
- Use **virtual pages with custom content** for organizing multiple related forms without creating separate page files
**Group related functionality**: Use the page hierarchy to logically group related forms and settings.
---
# Forms
Avo provide a powerful way to build custom forms for your interface. Unlike resources that are tied to database models, forms are standalone components that can handle any kind of data processing, settings management, or custom workflows.
## Overview
Forms in Avo are designed to:
- Handle custom data processing and workflows
- Manage application settings and configurations
- Provide standalone forms not tied to specific models
- Integrate seamlessly with pages for organized interfaces
- Support all Avo field types and layout components
- Be rendered anywhere in the interface
Forms are typically displayed on Pages and can be used for various purposes like user preferences, system settings, data imports, or any custom functionality your application requires.
Forms can also be rendered as a standalone component anywhere in the interface. For example, you can render the general settings form in a tool by using the following code:
```erb
<%= render Avo::Forms::Settings::General.component %>
```
## Generating Forms
The generator usage is documented in the Generators page.
## Form Structure
Every form inherits from `Avo::Forms::Core::Form` and requires two main methods:
1. **`def fields`** - Define the form structure and fields
2. **`def handle`** - Process form submission and define response
```ruby
# app/avo/forms/app_settings.rb
class Avo::Forms::AppSettings < Avo::Forms::Core::Form
self.title = "Application Settings"
self.description = "Manage your application configuration"
def fields
field :app_name, as: :text
field :maintenance_mode, as: :boolean
end
def handle
# Process form data
flash[:notice] = "Settings updated successfully"
default_response
end
end
```
## Form Configuration Options
Forms have several class attributes that customize their behavior and appearance.
Sets the display title for the form.
```ruby{3}
# app/avo/forms/app_settings.rb
class Avo::Forms::AppSettings < Avo::Forms::Core::Form
self.title = "Application Settings"
end
```
Provides a description that appears below the form title.
```ruby{3}
# app/avo/forms/app_settings.rb
class Avo::Forms::AppSettings < Avo::Forms::Core::Form
self.description = "Manage your application configurations"
end
```
## Form Methods
Define the structure and fields of your form. This method uses the same field syntax as Avo resources and actions, supporting all field types, panels, clusters, and layout components.
:::tip
When using `panel` to group fields, you **must** use the `main_panel` for one of the panels where you want to display the save button.
:::
```ruby{3-5}
# app/avo/forms/user_preferences.rb
class Avo::Forms::UserPreferences < Avo::Forms::Core::Form
def fields
# Define form fields here
end
end
```
Add individual fields to your form. Supports all Avo field types and options.
```ruby
# app/avo/forms/user_preferences.rb
class Avo::Forms::UserPreferences < Avo::Forms::Core::Form
def fields
field :email, as: :text, required: true # [!code focus]
field :notifications, as: :boolean, default: true # [!code focus]
field :theme, as: :select, options: { light: "Light", dark: "Dark" } # [!code focus]
end
end
```
**Field Options**
All standard Avo field options are supported
Process form submission and define the response. This method is called when the form is submitted and receives form data through the `params` object.
Currently, the `handle` method is executed in the context of the controller method that receives the form request. This means that you can use any of the methods available in the controller to process the form data. **This is something experimental and might change in the future.**
This experiment is to see if by not building a heavy DSL for forms, we can make it easier to use and maintain.
The main point is that since it is the controller method, everything is available and possible to the developer by using rails syntax.
If we build a heavy DSL for the `handle` method like we do for actions, it and might feel restrictive to the developer in some cases.
If you have any feedback, please share it with us.
Right now the only pre-defined methods available in the controller are:
- `default_response` - Standard redirect back turbo stream response
```ruby{3-8}
# app/avo/forms/user_preferences.rb
class Avo::Forms::UserPreferences < Avo::Forms::Core::Form
def handle
# Process form data
# Access form data via params
# Set flash messages and redirect
default_response
end
end
```
## Field Types and Layout
Forms support all Avo field types and layout components:
### Basic Fields
```ruby
def fields
field :name, as: :text
field :email, as: :text, required: true
field :age, as: :number
field :active, as: :boolean
field :bio, as: :textarea
field :role, as: :select, options: { admin: "Admin", user: "User" }
end
```
### Panels and Organization
```ruby
def fields
main_panel "Personal Information" do
field :first_name, as: :text
field :last_name, as: :text
field :email, as: :text
end
panel "Preferences", description: "Customize your experience" do
field :theme, as: :select, options: { light: "Light", dark: "Dark" }
field :notifications, as: :boolean
end
end
```
### Clusters for Inline Layout
```ruby
def fields
main_panel do
cluster do
field :first_name, as: :text
field :last_name, as: :text
end
end
end
```
### Working with Records
You can bind form fields to existing records:
```ruby
def fields
field :first_name, record: Avo::Current.user
field :last_name, record: Avo::Current.user
field :email, record: Avo::Current.user
end
```
## Form Submission Handling
### Processing Form Data
```ruby
def handle
# Access form parameters
app_name = params[:app_name]
maintenance_mode = params[:maintenance_mode]
# Update application settings
Rails.application.config.app_name = app_name
cookies[:maintenance_mode] = maintenance_mode
# Set success message
flash[:notice] = "Settings updated successfully"
# Return standard response
default_response
end
```
### Flash Messages
```ruby
def handle
# Informative message
flash[:notice] = "Operation completed successfully"
# Error message
flash[:error] = "Something went wrong"
# Success with timeout
flash[:success] = { body: "Saved successfully", timeout: 3000 }
# Warning message without dismissing
flash[:warning] = { body: "Something went wrong", timeout: :forever }
default_response
end
```
### Working with Models
```ruby
def handle
# Update current user
current_user.update(params.permit(:first_name, :last_name, :email))
# Create new records
Post.create(title: params[:title], body: params[:body])
# Complex data processing
if params[:import_data]
ImportService.new(params[:file]).process
end
flash[:notice] = "Data processed successfully"
default_response
end
```
## Complete Examples
### User Profile Settings Form
```ruby
# app/avo/forms/profile_settings.rb
class Avo::Forms::ProfileSettings < Avo::Forms::Core::Form
self.title = "Profile Settings"
self.description = "Update your personal information"
def fields
main_panel do
cluster do
with_options stacked: true, record: Avo::Current.user do
field :first_name, as: :text, required: true
field :last_name, as: :text, required: true
end
end
field :email, as: :text, required: true, record: Avo::Current.user
field :phone, as: :text, record: Avo::Current.user
end
panel "Preferences" do
field :theme, as: :select,
options: { light: "Light", dark: "Dark", auto: "Auto" },
default: "auto"
field :email_notifications, as: :boolean, default: true
field :timezone, as: :select, options: ActiveSupport::TimeZone.all.map { |tz| [tz.name, tz.name] }
end
end
def handle
# Update user profile
current_user.update(params.permit(:first_name, :last_name, :email, :phone))
# Update preferences (assuming a preferences model)
current_user.preferences.update(
theme: params[:theme],
email_notifications: params[:email_notifications],
timezone: params[:timezone]
)
flash[:notice] = "Profile updated successfully"
default_response
end
end
```
### Application Settings Form
```ruby
# app/avo/forms/app_settings.rb
class Avo::Forms::AppSettings < Avo::Forms::Core::Form
self.title = "Application Settings"
self.description = "Configure global application settings"
def fields
main_panel do
field :app_name, as: :text,
default: -> { Rails.application.class.module_parent_name },
required: true
field :app_url, as: :text,
default: -> { request.base_url },
placeholder: "https://yourapp.com"
field :maintenance_mode, as: :boolean, default: false
end
panel "Email Configuration" do
field :support_email, as: :text,
default: "support@yourapp.com",
required: true
field :from_email, as: :text,
default: "noreply@yourapp.com",
required: true
end
panel "Feature Flags" do
field :enable_registrations, as: :boolean, default: true
field :enable_api_access, as: :boolean, default: false
field :max_file_upload_size, as: :number,
default: 10,
help_text: "Maximum file size in MB"
end
end
def handle
# Store in application configuration or settings model
settings = {
app_name: params[:app_name],
app_url: params[:app_url],
maintenance_mode: params[:maintenance_mode],
support_email: params[:support_email],
from_email: params[:from_email],
enable_registrations: params[:enable_registrations],
enable_api_access: params[:enable_api_access],
max_file_upload_size: params[:max_file_upload_size]
}
# Update application settings (your implementation)
ApplicationSettings.update_all(settings)
# Or store in Rails credentials
# Rails.application.credentials.update(settings)
flash[:success] = {
body: "Application settings updated successfully",
timeout: 5000
}
default_response
end
end
```
### Data Import Form
```ruby
# app/avo/forms/data_import.rb
class Avo::Forms::DataImport < Avo::Forms::Core::Form
self.title = "Import Data"
self.description = "Upload and import data from CSV files"
def fields
main_panel do
field :import_type, as: :select,
options: {
users: "Users",
products: "Products",
orders: "Orders"
},
required: true
field :csv_file, as: :file,
required: true,
help_text: "Select a CSV file to import"
field :skip_header_row, as: :boolean,
default: true,
help_text: "Skip the first row if it contains headers"
end
panel "Import Options" do
field :update_existing, as: :boolean,
default: false,
help_text: "Update existing records if found"
field :send_notification, as: :boolean,
default: true,
help_text: "Send email notification when import completes"
end
end
def handle
import_type = params[:import_type]
csv_file = params[:csv_file]
options = {
skip_header_row: params[:skip_header_row],
update_existing: params[:update_existing]
}
# Process the import
begin
importer = DataImporter.new(import_type, csv_file, options)
result = importer.process
if params[:send_notification]
ImportNotificationMailer.import_completed(current_user, result).deliver_later
end
flash[:success] = {
body: "Import completed: #{result[:imported]} records imported, #{result[:skipped]} skipped",
timeout: 10000
}
rescue => e
flash[:error] = "Import failed: #{e.message}"
end
default_response
end
end
```
## Best Practices
**Keep forms focused**: Each form should handle a specific set of related functionality rather than trying to do everything.
**Use descriptive titles and descriptions**: Help users understand what the form does and what data is expected.
**Organize with panels**: Group related fields together using panels for better user experience.
**Validate input**: Always validate and sanitize form input in your handle method.
**Provide feedback**: Use flash messages to inform users about the results of their actions.
**Handle errors gracefully**: Wrap potentially failing operations in begin/rescue blocks.
**Use default values**: Provide sensible defaults for form fields when possible.
**Consider async processing**: For long-running operations, consider using background jobs and provide appropriate feedback to users.
---
# Guides and Tutorials
This section provides practical guidance and best practices for common scenarios when working with Avo Forms and Pages.
## Form Definition Approaches
When building forms in Avo, there are several approaches for organizing your form code. The recommended approach is to use the default generator approach. But in this guide we'll cover other approach as well.
### Approach 1: Default Generator Approach
Use the generator to create standalone form files in the `app/avo/forms/` directory.
```bash
rails generate avo:form user_profiles
```
This creates:
```ruby
# app/avo/forms/user_profiles.rb
class Avo::Forms::UserProfiles < Avo::Forms::Core::Form
self.title = "User Profiles"
self.description = "Manage your user profiles"
def fields
field :example, as: :text, default: "Hello World"
end
def handle
flash[:success] = { body: "Form submitted successfully", timeout: :forever }
flash[:notice] = params[:example]
default_response
end
end
```
Then reference it in your page:
```ruby
# app/avo/pages/settings/profiles.rb
class Avo::Pages::Settings::Profiles < Avo::Forms::Core::Page
def content
form Avo::Forms::UserProfiles
end
end
```
Or render it in a custom component:
```ruby
<%= render Avo::Forms::UserProfiles.component %>
```
### Approach 2: Inline Form Definitions
You can define forms directly within page classes using the page's namespace.
```ruby
# app/avo/pages/settings/integrations.rb
class Avo::Pages::Settings::Integrations < Avo::Forms::Core::Page
self.title = "Integrations"
self.description = "Manage your integrations"
def content
form ApiConfiguration
end
# Form defined inline within the page class
class ApiConfiguration < Avo::Forms::Core::Form
self.title = "API Configuration"
self.description = "Configure your API settings"
def fields
field :api_key, as: :text, required: true
field :api_endpoint, as: :text
field :webhook_url, as: :text
end
def handle
flash[:success] = "API Configuration updated successfully"
default_response
end
end
end
```
:::warning Namespace Confusion
When defining forms inline within page classes, it's not immediately clear that `Avo::Pages::Settings::Integrations::ApiConfiguration` is a form class rather than page. This can lead to confusion about the class's purpose and make code navigation more difficult. **This strategy is not recommended when you want to reuse the form in other pages or components.**
:::
## When to Use Each Approach
**Approach 1 (Default Generator)**: Good starting point for most forms. Easy to generate and maintain.
**Approach 2 (Inline)**: Best for simple, page-specific forms that won't be reused elsewhere.
## Page Organization Approaches
In addition to form definition approaches, you can also choose how to organize your pages:
### Virtual Pages
Instead of creating separate page files for every form, you can use virtual pages:
```ruby
# app/avo/pages/settings.rb
class Avo::Pages::Settings < Avo::Forms::Core::Page
def navigation
# Virtual page that renders a single form
page form: Avo::Forms::UserProfiles
# Virtual page with multiple forms
page "Integrations",
content: -> do
form Avo::Forms::SlackIntegration
form Avo::Forms::EmailIntegration
end
end
end
```
**Virtual pages are ideal when:**
- You have simple forms that don't need dedicated page files
- You want to group related forms together without file proliferation
- You're building settings or configuration interfaces with many small forms
---
# Mount Avo API
This document explains how to mount and configure the Avo API in your Rails application.
## Overview
The `mount_avo_api` method is a convenient Rails route helper that mounts the Avo API engine into your application's routing system. It provides a RESTful API for all your Avo resources, allowing external applications to interact with your data programmatically.
:::warning IMPORTANT
There is a caveat when mounting the API that requires attention:
If you have `mount_avo` inside an `authenticate` block (like `authenticate :user`), you **must** mount the API outside and before that authentication block.
**Why?** When the API is mounted inside an authentication block, all API endpoints will require the same authentication as your web interface, which breaks API functionality for external clients using API tokens.
**This do not mean that API can't use authentication, check the Authentication page for more information.**
**Correct setup:**
```ruby
# config/routes.rb
Rails.application.routes.draw do
# Mount Avo API FIRST - outside any authentication blocks
mount_avo_api
# Mount Avo web interface with authentication
authenticate :user do
mount_avo
end
end
```
**Incorrect setups:**
```ruby
# config/routes.rb
Rails.application.routes.draw do
# β Don't do this - API will require web authentication
authenticate :user do
mount_avo_api # This breaks API token authentication
mount_avo
end
end
```
```ruby
# config/routes.rb
Rails.application.routes.draw do
# β Don't do this - API will require web authentication
authenticate :user do
mount_avo
end
mount_avo_api # This breaks API token authentication
end
```
:::
## Basic Usage
### Simple Mount
Add this to your `config/routes.rb` file:
```ruby
# config/routes.rb
Rails.application.routes.draw do
mount_avo_api
end
```
This will mount the API at the default path: `/api`
### Using Avo's Root Path
If you want to mount the API under Avo's configured root path (like the previous default behavior), you can specify it explicitly:
```ruby
Rails.application.routes.draw do
mount_avo_api at: "#{Avo.configuration.root_path}/api"
end
```
For example, if your Avo is configured with `root_path = "/admin"`, this will make the API available at `/admin/api`.
### Custom Mount Path
You can specify a custom mount path:
```ruby
Rails.application.routes.draw do
mount_avo_api at: "/avo_api"
end
```
This makes the API available at `/avo_api` instead of the default path.
## Configuration Options
### Mount Path Option
```ruby
mount_avo_api at: "/custom/api/path"
```
**Parameters:**
- `at:` - String specifying where to mount the API (default: `"api"`)
### Additional Mount Options
You can pass any options that Rails' `mount` method accepts:
```ruby
mount_avo_api at: "/api", via: [:get, :post], constraints: { subdomain: "api" }
```
**Common options:**
- `via:` - Restrict HTTP methods
- `constraints:` - Add routing constraints
- `defaults:` - Set default parameters
### Custom Routes Block
You can provide a block to define custom routes within the API engine:
```ruby
Rails.application.routes.draw do
mount_avo_api do
# Custom routes within the API engine
get 'health', to: 'health#check'
get 'version', to: 'version#show'
# Custom namespaces
namespace :custom do
resources :reports, only: [:index, :show]
end
end
end
```
## Generated API Endpoints
When you mount the API, it automatically generates RESTful endpoints for all your Avo resources:
### Standard Endpoints Pattern
For each resource, the following endpoints are created:
```
GET /api/resources/v1/{resource_name} # List resources
POST /api/resources/v1/{resource_name} # Create resource
GET /api/resources/v1/{resource_name}/:id # Show resource
PATCH /api/resources/v1/{resource_name}/:id # Update resource
PUT /api/resources/v1/{resource_name}/:id # Update resource
DELETE /api/resources/v1/{resource_name}/:id # Delete resource
```
### Example for User Resource
If you have an `Avo::Resources::User` resource:
```
GET /api/resources/v1/users # List users
POST /api/resources/v1/users # Create user
GET /api/resources/v1/users/1 # Show user
PATCH /api/resources/v1/users/1 # Update user
PUT /api/resources/v1/users/1 # Update user
DELETE /api/resources/v1/users/1 # Delete user
```
## Complete Examples
### Basic Setup
```ruby
# config/routes.rb
Rails.application.routes.draw do
devise_for :users
# Mount Avo API
mount_avo_api
# Mount Avo
authenticate :user do
mount_avo do
get "tool_with_form", to: "tools#tool_with_form", as: :tool_with_form
end
end
# Redirect to Avo root path
root to: redirect(Avo.configuration.root_path)
end
```
### API with Custom Constraints
```ruby
# config/routes.rb
Rails.application.routes.draw do
# API only accessible from api subdomain
mount_avo_api at: "/api", constraints: { subdomain: "api" }
# API with IP restrictions (for internal use)
mount_avo_api at: "/internal/api", constraints: lambda { |request|
%w[127.0.0.1 10.0.0.0/8 192.168.0.0/16].any? { |ip|
IPAddr.new(ip).include?(request.remote_ip)
}
}
end
```
### Development vs Production Setup
```ruby
# config/routes.rb
Rails.application.routes.draw do
mount_avo
if Rails.env.development?
# Development: Mount API with debugging routes
mount_avo_api at: "/api" do
get 'debug/info', to: proc { |env|
info = {
version: Avo::Api::VERSION,
environment: Rails.env,
timestamp: Time.current.iso8601
}
[200, { 'Content-Type' => 'application/json' }, [info.to_json]]
}
end
else
# Production: Simple mount
mount_avo_api at: "/api"
end
end
```
---
# Avo API Controller Generators
This document explains how to use the Rails generators to create individual controllers for your Avo API resources.
## Overview
The `avo-api` gem provides two generators to help you create individual controllers for each resource. This allows for resource-specific customization while maintaining all the base functionality.
## Available Generators
### 1. Bulk Controller Generator
Creates controllers for **all existing Avo resources** at once.
```bash
rails generate avo_api:controllers
```
**What it does:**
- Automatically discovers all existing Avo resources in your application
- Creates individual controllers for each resource
- Places them in `app/controllers/avo/api/resources/v1/`
- Each controller inherits from `BaseResourcesController`
**Example output:**
```
β Created UsersController
β Created PostsController
β Created CommentsController
β Created ProfilesController
β Created TagsController
Generated 5 controllers successfully!
All controllers inherit from BaseResourcesController and can be customized as needed.
```
### 2. Single Controller Generator
Creates a controller for a **specific resource**.
```bash
rails generate avo_api:controller ResourceName
```
**Parameters:**
- `ResourceName` - The name of the resource (e.g., User, Post, BlogPost, ProductCategory)
**Examples:**
```bash
rails generate avo_api:controller User
rails generate avo_api:controller Post
rails generate avo_api:controller BlogPost
rails generate avo_api:controller ProductCategory
```
**Example output:**
```
create app/controllers/avo/api/resources/v1/users_controller.rb
Created UsersController at app/controllers/avo/api/resources/v1/users_controller.rb
You can now customize this controller by adding methods or overriding the base functionality.
```
## Generated Controller Structure
Each generated controller follows this structure:
```ruby
module Avo
module Api
module Resources
module V1
class UsersController < BaseResourcesController
# Add any custom logic for User resources here
#
# Example: Override methods to customize behavior
# def index
# super
# # Add custom logic after calling super
# end
#
# def show
# super
# # Add custom logic for show action
# end
end
end
end
end
end
```
## Resource to Controller Mapping
The generators follow Rails naming conventions:
| Avo Resource | Generated Controller |
|---------------|---------------------|
| `Avo::Resources::User` | `Avo::Api::Resources::V1::UsersController` |
| `Avo::Resources::Post` | `Avo::Api::Resources::V1::PostsController` |
| `Avo::Resources::Comment` | `Avo::Api::Resources::V1::CommentsController` |
| `Avo::Resources::BlogPost` | `Avo::Api::Resources::V1::BlogPostsController` |
| `Avo::Resources::ProductCategory` | `Avo::Api::Resources::V1::ProductCategoriesController` |
## File Locations
Generated controllers are placed in:
```
app/controllers/avo/api/resources/v1/
βββ users_controller.rb
βββ posts_controller.rb
βββ comments_controller.rb
βββ profiles_controller.rb
βββ tags_controller.rb
```
## Customizing Generated Controllers
Since each controller inherits from `BaseResourcesController`, you get all the standard CRUD functionality automatically. You can customize behavior by overriding methods:
### Example: Custom Index Logic
```ruby
class UsersController < BaseResourcesController
def index
super
# Add custom logic after the base index action
# The @resources variable contains the paginated records
# The @pagy variable contains pagination info
end
end
```
### Example: Custom Show Logic
```ruby
class UsersController < BaseResourcesController
def show
super
# Add custom logic after the base show action
# The @resource variable contains the Avo resource instance
# The @record variable contains the actual model record
end
end
```
### Example: Custom Create Logic
```ruby
class UsersController < BaseResourcesController
private
def create_success_action
# Custom success response for user creation
render json: {
record: serialize_record(@resource, :show),
message: "Welcome! Your account has been created successfully."
}, status: :created
end
def create_fail_action
# Custom error response for user creation
render json: {
errors: @record.errors,
message: "Account creation failed. Please check the errors below."
}, status: :unprocessable_entity
end
end
```
### Example: Adding Before Actions
```ruby
class UsersController < BaseResourcesController
before_action :require_admin, only: [:destroy]
before_action :log_user_access, only: [:show, :index]
private
def require_admin
# Custom authorization logic
head :forbidden unless current_user&.admin?
end
def log_user_access
# Custom logging logic
Rails.logger.info "User #{current_user&.id} accessed users API"
end
end
```
## Available Methods to Override
The `BaseResourcesController` provides these methods that you can override:
### CRUD Actions
- `index` - List resources
- `show` - Show single resource
- `create` - Create new resource
- `update` - Update existing resource
- `destroy` - Delete resource
### Success/Failure Callbacks
- `create_success_action` - Called after successful creation
- `create_fail_action` - Called after failed creation
- `update_success_action` - Called after successful update
- `update_fail_action` - Called after failed update
- `destroy_success_action` - Called after successful deletion
- `destroy_fail_action` - Called after failed deletion
### Serialization Methods
- `serialize_records(resources, view)` - Serialize multiple records
- `serialize_record(resource, view)` - Serialize single record
- `serialize_field_value(field)` - Serialize individual field
## Workflow
1. **Create your Avo resources** as usual in `app/avo/resources/`
2. **Update your routes** to use individual controllers (this is already done)
3. **Generate controllers** using one of the generators:
- `rails generate avo_api:controllers` (for all resources)
- `rails generate avo_api:controller ResourceName` (for specific resource)
4. **Customize as needed** by overriding methods in the generated controllers
## Notes
- Generated controllers automatically inherit all functionality from `BaseResourcesController`
- You don't need to implement basic CRUD operations unless you want to customize them
- The routing system automatically maps to the correct controller based on the resource name
- All existing Avo features (authorization, field visibility, etc.) continue to work
- Controllers are generated with helpful comments showing common customization patterns
---
# CSRF Protection in Avo API
## Overview
Cross-Site Request Forgery (CSRF) protection is a security measure that prevents malicious websites from making unauthorized requests on behalf of authenticated users. The Avo API implements CSRF protection using Rails' built-in mechanisms.
## Implementation
The Avo API implements CSRF protection through a customizable class method hook in the `Avo::Api::Resources::V1::ResourcesController`:
```ruby{10-12}
# app/controllers/avo/api/resources/v1/resources_controller.rb
module Avo
module Api
module Resources
module V1
class ResourcesController < Avo::BaseController
delegate :setup_csrf_protection, to: :class
before_action :setup_csrf_protection, prepend: true
def self.setup_csrf_protection
protect_from_forgery with: :null_session
end
end
end
end
end
end
```
This approach makes the CSRF protection easily configurable and overridable.
## Customizing CSRF Protection
You can override the `setup_csrf_protection` method in your controllers that inherit from `Avo::Api::Resources::V1::ResourcesController` to customize CSRF handling:
### Example 1: Change CSRF protection method
```ruby{7-9}
# app/controllers/avo/api/resources/v1/users_controller.rb
module Avo
module Api
module Resources
module V1
class UsersController < BaseResourcesController
def self.setup_csrf_protection
protect_from_forgery with: :exception
end
end
end
end
end
end
```
### Example 2: Disable CSRF protection entirely
```ruby{7-9}
# app/controllers/avo/api/resources/v1/users_controller.rb
module Avo
module Api
module Resources
module V1
class UsersController < BaseResourcesController
def self.setup_csrf_protection
# No CSRF protection - leave empty
end
end
end
end
end
end
```
## What is `:null_session`?
The `:null_session` strategy is specifically designed for API endpoints and works as follows:
1. **For requests with valid CSRF tokens**: Normal session handling continues
2. **For requests without valid CSRF tokens**: A new, empty session is created for the duration of the request
3. **No exceptions are raised**: Unlike other strategies, this doesn't raise `ActionController::InvalidAuthenticityToken`
## Why `:null_session` for APIs?
This strategy is ideal for REST APIs because:
- **Stateless Nature**: APIs are typically stateless and don't rely on browser sessions
- **Token-based Authentication**: APIs usually use tokens (JWT, API keys) rather than session-based authentication
- **Cross-Origin Requests**: APIs are designed to be consumed by various clients (mobile apps, SPAs, other services)
- **No CSRF Token Distribution**: API clients don't typically have access to CSRF tokens like HTML forms do
## Best Practices for API Consumers
When consuming the Avo API:
1. **Use Token-based Authentication**: Implement proper API token authentication
2. **HTTPS Only**: Always use HTTPS to prevent token interception
3. **Token Rotation**: Implement token rotation for long-lived applications
## Testing CSRF Protection
To test that CSRF protection is working:
```bash
# This should work (with null_session, no exception is raised)
curl -X POST http://localhost:3000/api/resources/v1/users \
-H "Content-Type: application/json" \
-d '{"user": {"first_name": "Test User"}}'
```
## Related Security Considerations
- Implement proper authentication and authorization
- Use CORS headers appropriately for browser-based clients
- Validate all input data
- Use HTTPS in production
- Implement rate limiting for API endpoints
## References
- [Rails Security Guide - CSRF](https://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf)
- [ActionController CSRF Protection](https://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html)
---
# Authentication
Avo API provides a flexible authentication system that can be customized for your specific needs. By default, the API requires authentication for all requests, but you can override this behavior in your controllers.
## How it works
The API uses a delegated authentication pattern where:
1. **Default Behavior**: All requests are rejected with a 401 Unauthorized error by default
2. **Override Pattern**: Individual controllers can override the `setup_authentication` method to implement custom authentication logic
3. **Error Handling**: Authentication failures are handled gracefully with JSON error responses
4. **Flexibility**: You can disable authentication entirely, implement token-based auth, session-based auth, or any custom solution
## Default Authentication
By default, Avo API controllers inherit from `Avo::Api::Resources::V1::ResourcesController`, which implements strict authentication:
```ruby{12-14}
# app/controllers/avo/api/resources/v1/resources_controller.rb
module Avo
module Api
module Resources
module V1
class ResourcesController < Avo::BaseController
rescue_from Avo::Api::AuthenticationError do |exception|
render json: { error: 'Unauthorized' }, status: :unauthorized
end
before_action :setup_authentication, prepend: true
def setup_authentication
raise Avo::Api::AuthenticationError
end
end
end
end
end
end
```
This means all API requests will return a 401 Unauthorized response unless you override the authentication behavior.
## Overriding Authentication
You can customize authentication by overriding the `setup_authentication` class method in your individual controllers:
### Example 1: Disable Authentication
```ruby{7-9,13}
# app/controllers/avo/api/resources/v1/users_controller.rb
module Avo
module Api
module Resources
module V1
class UsersController < BaseResourcesController
def setup_authentication
# Leave empty to disable authentication
end
# OR
skip_before_action :setup_authentication
end
end
end
end
end
```
### Example 2: API Key Authentication
Most suitable for server-to-server communication:
```ruby{7-12}
# app/controllers/avo/api/resources/v1/users_controller.rb
module Avo
module Api
module Resources
module V1
class UsersController < BaseResourcesController
def setup_authentication
api_key = request.headers['Authorization']&.sub(/^ApiKey /, '')
unless ApiKey.active.exists?(key: api_key)
raise Avo::Api::AuthenticationError
end
end
end
end
end
end
end
```
### Example 3: HTTP Basic Authentication
For more sophisticated token-based auth:
```ruby{7-16}
# app/controllers/avo/api/resources/v1/users_controller.rb
module Avo
module Api
module Resources
module V1
class UsersController < BaseResourcesController
def setup_authentication
raise Avo::Api::AuthenticationError unless authenticate_with_http_basic do |email, password|
user = User.find_by(email: email)
if user&.valid_password?(password)
sign_in(user, store: false)
else
false
end
end
end
end
end
end
end
end
```
## Error Responses
When authentication fails, the API returns a standardized JSON error response:
```json
{
"error": "Unauthorized"
}
```
The response includes:
- **Status Code**: 401 Unauthorized
- **Content-Type**: application/json
- **Body**: JSON object with error message
## Best Practices
1. **Override at the Controller Level**: Each resource controller can have its own authentication strategy
2. **Use Strong Tokens**: If implementing token authentication, use cryptographically secure random tokens
3. **Rate Limiting**: Consider implementing rate limiting for API endpoints
4. **HTTPS Only**: Always use HTTPS in production for token-based authentication
5. **Token Rotation**: Implement token expiration and rotation for better security
## Security Considerations
- The default behavior (rejecting all requests) is secure by default
- Authentication errors are handled without exposing sensitive information
- Each controller can implement the authentication strategy that best fits its needs
## Testing Authentication
You can test your authentication implementations by making requests with and without proper credentials:
```bash{1-2,4-6}
# Should return 401
curl -X GET "http://localhost:3000/api/resources/v1/users"
# Should return 200 (if properly authenticated)
curl -X GET "http://localhost:3000/api/resources/v1/users"
-H "Authorization: Basic YXZvQGF2b2hxLmlvOldIWV9BUkVfWU9VX1NPX0NVUklPVVM/"
```
---
# Collaboration
Keep your team in sync with built-in comments and status updates. No more scattered communication across multiple tools.
## Installation
1. **Add gem:** Add the following to your Gemfile:
```ruby
gem "avo-collaboration", source: "https://packager.dev/avo-hq"
```
2. **Bundle:** Run bundle install:
```bash
bundle
```
3. **Install migrations:** Generate the required database migrations:
```bash
rails avo_collaboration:install:migrations
```
4. **Run migrations:** Apply the migrations to your database:
```bash
rails db:migrate
```
5. **Configure resources:** Enable collaboration on your resources by adding `self.collaboration` to your resource configuration.
6. **Add timeline:** Include the collaboration timeline in your resource using `collaboration_timeline`.
7. **Configure permissions:** Add the [required authorization methods](https://docs.avohq.io/3.0/collaborate/authorization.html) to your resource policies.
Configure collaboration settings for your resource. This hash contains author and watchers configuration.
```ruby{3-21}
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
self.collaboration = {
author: {
current_author: -> { current_user },
name_property: :name,
},
watchers: [
{
property: :name,
message: -> { "This property has been updated #{property}: #{old_value} -> #{new_value}" }
},
{
property: :status,
i18n_message_key: "avo.collaboration.custom_property_changed_html",
},
{
property: :stage,
}
]
}
def fields
# ...
end
end
```
- **`current_author`**: A lambda that returns the current user/author object
- **`name_property`**: The property on the author object that contains their display name
Watchers monitor changes to specific properties and can generate automatic timeline entries when those properties change.
Each watcher can have:
- **`property`**: The property name to watch for changes (required)
- **`message`**: A lambda that generates a custom message when the property changes. Available variables: `property`, `old_value`, `new_value`
- **`i18n_message_key`**: An internationalization key for the message instead of a custom lambda
If neither `message` nor `i18n_message_key` is provided, a default message will be generated.
#### I18n Example
When using `i18n_message_key`, define your translations in your locale files:
```yaml
en:
hello: "Hello world"
avo:
collaboration:
custom_property_changed_html: changed %{property} to %{new_value} [Custom]
```
The message can use interpolation variables like `%{property}`, `%{old_value}`, and `%{new_value}`.
##### Safe HTML Translations
When your translation messages contain HTML markup (as shown in the example above), Rails automatically treats them as safe HTML when the translation key ends with `_html`. This follows Rails' [safe HTML translations](https://guides.rubyonrails.org/i18n.html#using-safe-html-translations) convention, allowing you to include styling and formatting in your collaboration timeline messages without additional escaping.
The `collaboration_timeline` method renders the collaboration timeline component at the specific position where it's defined within your resource's fields.
```ruby{13}
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
self.collaboration = {
# ...
}
def fields
field :id, as: :id
field :name
field :status
field :stage, as: :select, options: ["Not Started", "In Progress", "Completed"]
collaboration_timeline
end
end
```
This DSL method will display the collaboration timeline (showing comments, status updates, and property changes) wherever you place it in your resource definition. You can position it among your other fields to control where the timeline appears in your resource's layout.
## Customizing collaboration models for your business logic
Sometimes, the default collaboration models aren't quite enough for your specific use case. You might want to add custom associations, validations, or callbacks to align with your application's domain logic.
You can safely extend `Avo::Collaboration::Action`, `Avo::Collaboration::Comment`, and `Avo::Collaboration::Entry` in an initializer, similar to how you would extend other Avo provided models:
```ruby{7-86}
# config/initializers/avo.rb
Avo.configure do |config|
# ... other config options ...
end
Rails.configuration.to_prepare do
# For actions generated by watchers (property changes, status updates, etc.)
Avo::Collaboration::Action.class_eval do
after_create :slack_notification
private
def slack_notification
SlackNotificationService.notify(message: "New action created: #{body}")
end
end
# For user-authored comments in the collaboration timeline
Avo::Collaboration::Comment.class_eval do
validates :body, presence: true, length: { maximum: 5000 }
before_validation :sanitize_body
private
def sanitize_body
self.body = HtmlSanitizer.sanitize(body)
end
end
# Wrapper entry that unifies both comments and actions via delegated_type :entryable
Avo::Collaboration::Entry.class_eval do
after_create :discord_notification
private
def discord_notification
DiscordNotificationService.notify(message: "New action created: #{body}")
end
end
end
```
This approach lets you integrate collaboration events with your domain models while keeping the core Avo functionality intact. Add only the associations and callbacks that make sense for your application.
---
# Authorization
Control who can view, create, and manage collaboration timeline entries using Avo's authorization system. The collaboration feature provides specific authorization methods to fine-tune access to different aspects of the collaboration timeline.
## Authorization Methods
Controls whether a user can view the collaboration timeline on a resource.
```ruby{3-6}
# app/policies/project_policy.rb
class ProjectPolicy < ApplicationPolicy
def collaboration_view_timeline?
# Only allow users who can view the record to see the timeline
show?
end
end
```
Controls whether a user can create new timeline entries (write messages and comments).
```ruby{3-6}
# app/policies/project_policy.rb
class ProjectPolicy < ApplicationPolicy
def collaboration_create_entry?
# Only allow team members to create timeline entries
current_user.team_member? && show?
end
end
```
Controls whether a user can destroy timeline entries. The `record` parameter can be either an action entry (automatically generated when watched attributes change) or a message entry (manually created by users).
```ruby{3-10}
# app/policies/project_policy.rb
class ProjectPolicy < ApplicationPolicy
def collaboration_destroy_entry?
# Users can only destroy their own message entries
# Admins can destroy any entry
return true if current_user.admin?
# Only allow destroying message entries, not action entries
record.is_a?(Avo::Collaboration::Comment) && record.author == current_user
end
end
```
---
# Notifications
Avo Notifications is an in-app notification system for your Avo admin panel. It lets you send targeted or global notifications to your admin users, complete with action buttons, severity levels, and optional real-time delivery via ActionCable.
Notifications appear in a bell icon dropdown in the navbar and can also be browsed through a full Avo resource page with scopes, filters, and bulk actions.
## Requirements
- `avo` (core)
- ActionCable (optional, for real-time delivery)
## Installation
:::info
Follow these steps in order. The installer generates migrations, an initializer, and an Avo resource for you.
:::
### 1. Install the gem
Add the following to your `Gemfile`:
```ruby
gem "avo-notifications", source: "https://packager.dev/avo-hq/"
```
Then
```bash
bundle install
```
### 2. Run the installer
```bash
bin/rails generate avo:notifications install
```
This creates:
- A migration for the `avo_notifications` table
- A migration to add `avo_notifications_last_read_at` to your users table
- An initializer at `config/initializers/avo_notifications.rb`
- An Avo resource for managing notifications
### 3. Run migrations
```bash
bin/rails db:migrate
```
### 4. Include the concern in your User model
```ruby
class User < ApplicationRecord
include Avo::Notifications::HasNotifications # [!code ++] [!code focus]
end
```
This adds helper methods for reading notification state on the user (covered in [Reading notifications](#reading-notifications)).
## Configuration
After installation, configure the gem in `config/initializers/avo_notifications.rb`:
```ruby
Avo::Notifications.configure do |config|
# How long notifications are kept before cleanup deletes them
config.ttl = 30.days
# Enable real-time delivery via ActionCable
config.realtime = true
# Max notifications shown in the bell dropdown
config.dropdown_limit = 10
# The model class used for notification recipients
config.user_class = "User"
# Method called on the sender to display their name
config.user_display_name_method = :name
end
```
## Sending notifications
Use `Avo::Notifications.send` (aliased as `Avo::Notifications.notify`) to create and deliver notifications.
### Basic usage
```ruby
Avo::Notifications.send( # [!code focus]
to: user, # [!code focus]
title: "Welcome to the admin panel!", # [!code focus]
body: "You now have access to all features.", # [!code focus]
level: :info # [!code focus]
) # [!code focus]
```
| Parameter | Required | Description |
| ---------- | -------- | ------------------------------------------------------------ |
| `title` | Yes | Notification title (max 255 characters) |
| `to` | No | Recipient user. Omit for a global notification |
| `body` | No | Longer description text |
| `level` | No | Severity: `:info`, `:success`, `:warning`, `:error` (default `:info`) |
| `url` | No | URL to navigate to when the notification title is clicked |
| `sender` | No | The user who sent the notification |
| `buttons` | No | Array of action buttons (max 3) |
### Targeted vs global notifications
**Targeted** notifications are sent to a specific user by passing the `to:` parameter. **Global** notifications omit `to:` and are visible to all admin users.
```ruby
# Targeted β only this user sees it
Avo::Notifications.send(
to: @user,
title: "Your export is ready",
level: :success
)
# Global β all admin users see it
Avo::Notifications.send(
title: "System maintenance tonight at 10 PM",
level: :warning
)
```
### Notification levels
Each notification has a level that controls its icon and color in the UI:
| Level | Icon | Color |
| ---------- | ------------------- | ------ |
| `:info` | Info circle (blue) | Blue |
| `:success` | Check circle (green)| Green |
| `:warning` | Alert triangle | Amber |
| `:error` | Alert circle (red) | Red |
### Adding action buttons
Notifications can include up to 3 action buttons. Each button needs a `label` and `url`, and optionally a `method` (defaults to `"get"`).
```ruby
Avo::Notifications.send(
to: @user,
title: "Project review pending",
level: :info,
buttons: [ # [!code focus]
{ label: "Approve", url: "/projects/#{@project.id}/approve", method: "post" }, # [!code focus]
{ label: "Reject", url: "/projects/#{@project.id}/reject", method: "post" }, # [!code focus]
{ label: "View", url: "/projects/#{@project.id}" } # [!code focus]
] # [!code focus]
)
```
Supported `method` values: `get`, `post`, `patch`, `put`, `delete`.
### From Avo actions
```ruby
class Avo::Actions::ApproveProject < Avo::BaseAction
self.name = "Approve project"
def handle(query:, fields:, current_user:, **args)
query.each do |project|
project.approve!
Avo::Notifications.send( # [!code focus]
to: project.owner, # [!code focus]
title: "Your project was approved", # [!code focus]
body: "#{current_user.name} approved '#{project.name}'.", # [!code focus]
level: :success, # [!code focus]
sender: current_user, # [!code focus]
url: "/admin/projects/#{project.id}" # [!code focus]
) # [!code focus]
end
succeed "#{query.count} project(s) approved."
end
end
```
### From model callbacks
```ruby
class Order < ApplicationRecord
after_update :notify_status_change
private
def notify_status_change
return unless saved_change_to_status?
Avo::Notifications.send( # [!code focus]
to: user, # [!code focus]
title: "Order ##{id} status changed to #{status}", # [!code focus]
level: :info, # [!code focus]
url: "/admin/orders/#{id}" # [!code focus]
) # [!code focus]
end
end
```
### From background jobs
```ruby
class ProcessExportJob < ApplicationJob
def perform(export_id, user_id)
export = Export.find(export_id)
user = User.find(user_id)
export.process!
Avo::Notifications.send( # [!code focus]
to: user, # [!code focus]
title: "Your export is ready", # [!code focus]
body: "#{export.name} has finished processing.", # [!code focus]
level: :success, # [!code focus]
url: export.download_url # [!code focus]
) # [!code focus]
end
end
```
## Reading notifications
### Query methods
```ruby
# Get notifications for a user (targeted + global), newest first
Avo::Notifications.for_user(user, limit: 10)
# Count unread notifications
Avo::Notifications.unread_count(user)
# Mark a single notification as read
Avo::Notifications.mark_as_read(notification, user: current_user)
# Mark all notifications as read for a user
Avo::Notifications.mark_all_as_read(user)
```
### User model methods
After including the `HasNotifications` concern, your User model gains these methods:
```ruby
user.unread_avo_notifications_count
# => 5
user.mark_all_avo_notifications_read!
# Marks all targeted notifications as read and updates avo_notifications_last_read_at
user.avo_notification_unread?(notification)
# => true/false β works for both targeted and global notifications
```
:::info
**How read state works:** Targeted notifications track read state via a `read_at` timestamp on the notification itself. Global notifications are considered "read" if they were created before the user's `avo_notifications_last_read_at` timestamp.
:::
## Bell component
The bell icon automatically appears in your Avo navbar when the gem is installed. It shows:
- A bell icon with an unread count badge (hidden when all notifications are read)
- A dropdown panel with the most recent notifications (limited by `dropdown_limit`)
- A "Mark all as read" link when there are unread notifications
- A "View all notifications" link to the full notification resource
No additional configuration is needed β the component renders automatically for logged-in users.
## Real-time delivery
When `config.realtime = true` (the default), notifications are broadcast via ActionCable using Turbo Streams. The bell dropdown updates automatically without a page refresh.
### How it works
Two ActionCable streams are used:
- **Personal stream** β delivers targeted notifications to the specific recipient
- **Global stream** (`avo_notifications:global`) β delivers global notifications to all connected users
The Stimulus controller `notifications-cable` handles subscribing to both streams and processing incoming Turbo Stream messages.
### ActionCable setup
Make sure ActionCable is configured in your Rails app. A typical `config/cable.yml`:
```yaml
development:
adapter: async
production:
adapter: redis
url: redis://localhost:6379/1
```
:::warning
If ActionCable is not available or not configured, real-time delivery is silently skipped β notifications still work, they just require a page refresh to appear. Broadcasting errors are logged but never raise exceptions.
:::
## Notification resource
The installer generates an Avo resource at `app/avo/resources/avo_notification.rb` that gives you a full management interface for notifications.
### Scopes
- **All** β every notification for the current user
- **Unread** β only unread notifications
- **Read** β only read notifications
### Filter
- **Level** β filter by notification level (info, success, warning, error)
### Bulk actions
- **Mark as read** β marks selected notifications as read
- **Mark as unread** β clears the read state on targeted notifications
- **Delete** β permanently removes selected notifications
:::info
The notification resource has `visible_on_sidebar = false` by default. Notifications are accessed through the bell dropdown's "View all" link. You can change this in the generated resource if you prefer sidebar access.
:::
## Cleanup
Over time, expired notifications accumulate in the database. Use the built-in rake task to clean them up:
```bash
bin/rails avo_notifications:cleanup
```
This deletes all notifications past their expiry date (based on the `expires_at` column, which is set from the `ttl` configuration when a notification is created).
**Recommended:** schedule this as a daily cron job:
```bash
# Daily at 2 AM
0 2 * * * cd /path/to/app && bin/rails avo_notifications:cleanup
```
---
# Media Library
If you run an asset-intensive, having a place to view all those asses would be great. It's becoming easier with Avo and it's Media Library feature.
The Media Library has two goals in mind.
1. Browse and manage all your assets
2. Use it to inject assets in all three of Avo's rich text editors (trix, rhino, and markdown).
:::warning
The Media Library feature is still in alpha and future releases might contain breaking changes so keep an eye out for the upgrade guide.
This is just the initial version and we'll be adding more features as we progress and get more feedback on usage.
:::
VIDEO
## How to enable it
The Media Library feature is disabled by default (until we release the stable version). To enable it, you need to do the following:
```ruby{4}
# config/initializers/avo.rb
if defined?(Avo::MediaLibrary)
Avo::MediaLibrary.configure do |config|
config.enabled = true
end
end
```
This is the killswitch of the whole feature.
When disabled, the Media Library will not be available to anyone. It will hide the menu item, block the all the routes, and hide media the library icons from the editors.
## Hide menu item
You can hide the menu item from the sidebar by setting the `visible` option to `false`.
```ruby
# config/initializers/avo.rb
if defined?(Avo::MediaLibrary)
Avo::MediaLibrary.configure do |config|
config.visible = false
end
end
```
You may also use a block to conditionally show the menu item. You'll have access to the `Avo::Current` object and you can use it to show the menu item based on the current user.
```ruby
# config/initializers/avo.rb
if defined?(Avo::MediaLibrary)
Avo::MediaLibrary.configure do |config|
config.visible = -> { Avo::Current.user.is_developer? }
end
end
```
This will hide the menu item from the sidebar if the current user is not a developer.
## Add it to the menu editor
The Media Library is a menu item in the sidebar. You can add it to the menu editor by using the `media_library` helper.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.main_menu = lambda {
link_to 'Media Library', avo.media_library_index_path
}
end
```
## Use it with the rich text editors
The Media Library will seamlessly integrate with all the rich text editors.
```ruby
field :body, as: :trix
field :body, as: :rhino
field :body, as: :markdown
```
The editors will each have a button to open the Media Library modal.
Once open, after the user selects the asset, it will be injected into the editor.
---
# Cache
Avo uses the application's cache system to enhance performance. The cache system is especially beneficial when dealing with resource index tables and license requests.
## Cache store selection
The cache system dynamically selects the appropriate cache store based on the application's environment:
### Production
In production, if the existing cache store is one of the following: `ActiveSupport::Cache::MemoryStore` or `ActiveSupport::Cache::NullStore` it will use the default `:file_store` with a cache path of `tmp/cache`. Otherwise, the existing cache store `Rails.cache` will be used.
### Test
In testing, it directly uses the `Rails.cache` store.
### Development and other environments
In all other environments the `:memory_store` is used.
### Custom selection
You can force Avo to use a particular store.
```ruby
# config/initializers/avo.rb
config.cache_store = -> {
ActiveSupport::Cache.lookup_store(:solid_cache_store)
}
# or
config.cache_store = ActiveSupport::Cache.lookup_store(:solid_cache_store)
```
`cache_store` configuration option is expecting a cache store object, the lambda syntax can be useful if different stores are desired on different environments.
:::warning `MemoryStore` in production
Our recomendation is to not use MemoryStore in production because it will not be shared between multiple processes (when using Puma).
:::
The `cache_hash` method is used to compute the cache key for each row.
More about this on the resource options page.
## Caching caveats
Avo caches each record on the view for improved performance. However side-effects may occur from this strategy. We'll try to outline some of them below and keep this page up to date as we find them or as they get reported to us.
These are things that may happen to regular Rails apps, not just in the Avo context.
### Rows may not be automatically updated when certain associations change
There are two things you could do to prevent this:
#### Option 1: Use `touch: true` on association
Example with Parent Model and Association
```ruby
class Post < ApplicationRecord
has_many :comments, dependent: :destroy
end
```
Example with Child Model and Association with `touch: true`
```ruby
class Comment < ApplicationRecord
belongs_to :post, touch: true
end
```
#### Option 2: override `cache_hash` method on resource to take associations in consideration
Avo, internally, uses the `cache_hash` method to compute the hash that will be remembered by the caching driver when displaying the rows.
You can take control and override it on that particular resource to take the association into account.
```ruby
class Avo::Resources::User < Avo::BaseResource
def fields
# your fields
end
def cache_hash(parent_record)
# record.post will now be taken under consideration
result = [record, file_hash, record.post]
if parent_record.present?
result << parent_record
end
result
end
end
```
### `root_path` change won't break the cache keys
When the rows are cached, the links from the controls, `belongs_to` and `record_link` fields, and maybe others will be cached along.
The best solution here is to clear the cache with this ruby command `Rails.cache.clear`. If that's not an option then you can try to add the `root_path` to the `cache_hash` method in your particular resource.
## Solid Cache
Avo seamlessly integrates with [Solid Cache](https://github.com/rails/solid_cache). To setup Solid Cache follow these essential steps
Add this line to your application's Gemfile:
```ruby
gem "solid_cache"
```
And then execute:
```bash
$ bundle
```
Or install it yourself as:
```bash
$ gem install solid_cache
```
Add the migration to your app:
```bash
$ bin/rails solid_cache:install:migrations
```
Then run it:
```bash
$ bin/rails db:migrate
```
To set Solid Cache as your Rails cache, you should add this to your environment config:
```ruby
# config/environments/production.rb
config.cache_store = :solid_cache_store
```
Check [Solid Cache repository](https://github.com/rails/solid_cache) for additional valuable information.
---
# Views performance
## Log ViewComponent loading times and allocations
Sometimes, you may want to track the loading times and memory allocations of ViewComponents, similar to how you do with partials. Follow these two steps to enable this functionality.
#### 1. Enable ViewComponent Instrumentation
First, you need to enable instrumentation for ViewComponents. Add the following configuration to your `application.rb` or `development.rb` file:
```ruby
# application.rb or development.rb
config.view_component.instrumentation_enabled = true
```
#### 2. Add Logging
Next, set up logging to capture the performance data. Create or update the `config/initializers/view_component.rb` file with the following code:
```ruby
# config/initializers/view_component.rb
module ViewComponent
class LogSubscriber < ActiveSupport::LogSubscriber
define_method :'!render' do |event|
info do
message = +" Rendered #{event.payload[:name]}"
message << " (Duration: #{event.duration.round(1)}ms"
message << " | Allocations: #{event.allocations})"
end
end
end
end
ViewComponent::LogSubscriber.attach_to :view_component
```
:::warning
Enabling this logging can negatively impact your applicationβs performance. We recommend using it in the development environment or disabling it in production once you have completed debugging.
:::
---
# Internals
This section documents on how we think about the internals of Avo and hwo much you could/should hook into them to extend it.
### Public Methods and Internal Usage
Not all public methods within the Avo codebase are meant for direct user consumption. Some methods are publicly accessible but primarily intended for internal use by various components of the Avo framework itself. This distinction arises due to the complex nature of building a framework or an ecosystem of gems, where numerous moving parts require public interfaces for framework developers rather than for end users.
---
# Testing
:::info
We know the testing guides aren't very detailed, and some testing helpers are needed. So please send your feedback [here](https://github.com/avo-hq/avo/discussions/1168).
:::
Testing is an essential aspect of your app. Most Avo DSLs are Ruby classes, so regular testing methods should apply.
## Testing helpers
We prepared a few testing helpers for you to use in your apps. They will help with opening/closing datepickers, choosing the date, saving the records, add/remove tags, and also select a lot of elements throughout the UI.
You can find them all [here](https://github.com/avo-hq/avo/blob/main/lib/avo/test_helpers.rb),
## Testing Actions
Given this `Avo::Actions::ReleaseFish`, this is the `spec` that tests it.
```ruby
class Avo::Actions::ReleaseFish < Avo::BaseAction
self.name = "Release fish"
self.message = "Are you sure you want to release this fish?"
def fields
field :message, as: :textarea, help: "Tell the fish something before releasing."
end
def handle(query:, fields:, **_)
query.each(&:release)
succeed "#{query.count} fish released with message '#{fields[:message]}'."
end
end
```
```ruby
require 'rails_helper'
RSpec.feature Avo::Actions::ReleaseFish, type: :feature do
let(:fish) { create :fish }
let(:current_user) { create :user }
let(:resource) { Avo::Resources::User.new.hydrate model: fish }
it "tests the dummy action" do
args = {
fields: {
message: "Bye fishy!"
},
current_user: current_user,
resource: resource,
query: [fish]
}
action = described_class.new(resource: resource, user: current_user, view: :edit)
expect(action).to receive(:succeed).with "1 fish released with message 'Bye fishy!'."
expect(fish).to receive(:release)
action.handle **args
end
end
```
---
# Keyboard Shortcuts
Avo ships with a built-in keyboard shortcut system that lets users navigate and operate the admin panel without touching the mouse. Press ? at any time to open the shortcuts reference modal.
## Library
Avo uses [**@github/hotkey**](https://github.com/github/hotkey) under the hood β the same library that powers GitHub's own keyboard shortcuts. It handles multi-key sequences (e.g. `r r r`), modifier chords (e.g. `Mod+Enter`), and fires a `hotkey-fire` DOM event that Avo listens to before triggering the bound element's click.
Hotkeys are attached declaratively via `data-hotkey` attributes on HTML elements:
```html
New post
```
For alternatives (Mac vs. non-Mac), space-separate the variants:
```html
Save
```
The library is initialised once on page load and re-applied on every `turbo:load` and `turbo:frame-render` event so shortcuts survive Turbo navigations.
## Global shortcuts
These shortcuts are always available, regardless of the current page.
| Keys | Action |
| ------------------------------------------------------------ | ------------------------------------------- |
| ? | Open/close the keyboard shortcuts modal |
| Cmd +K / Ctrl +K | Focus the global search |
| Cmd +\\ / Ctrl +\\ | Toggle the sidebar |
| r r r | Reload the page (preserves scroll position) |
| Esc | Close modal / unfocus field |
## Page-level shortcuts
### Index view
| Keys | Action |
| --------------------------- | ------------------------------- |
| / | Focus the resource search input |
| C | Create a new record |
| A | Open the actions menu |
| β / β | Navigate table rows |
| β΅ | Open the focused row |
| Space | Select / deselect row |
| Esc | Unfocus the current selection |
| V T | Switch to table view |
| V G | Switch to grid view |
| V M | Switch to map view |
### Show view
| Keys | Action |
| ------------ | ----------------- |
| B | Go back |
| E | Edit the record |
| D | Delete the record |
### Edit view
| Keys | Action |
| ---------------------------------------------------------- | ------------------------- |
| Cmd +β΅ / Ctrl +β΅ | Save / submit the form |
| Esc | Unfocus the current field |
| B | Go back |
### Action modal
| Keys | Action |
| ---------------------------------------------------------- | ------------------------ |
| Cmd +β΅ / Ctrl +β΅ | Run the action |
| Esc | Cancel / close the modal |
### Confirmation modal
| Keys | Action |
| ----------------------------------- | ------------------------ |
| Esc | Cancel / close the modal |
| Comma + Enter | Run the action |
## Some shortcuts are hidden in association panels
When a resource is rendered **inside an association panel** (i.e. as a `has_many`, `has_one`, or similar relation on another record's show page), certain shortcuts are intentionally suppressed:
- **Create** (C ) β hidden because the index panel is embedded; hitting C on a show page that already has its own create shortcut would be ambiguous. Also the user might have multiple create shortcuts for different has_many associations.
- **Actions** (A ) β hidden for the same reason.
- **Edit** (E ) and **Delete** (D ) β hidden because these controls belong to the _show_ view of a top-level resource, not to an association row.
Think of it as "I am nested" β and nested views never receive conflicting hotkeys.
## Guard: no shortcuts while typing
All shortcut handlers check that the keyboard event did not originate from a focusable input element:
```js
const TYPING_SELECTOR = "input, textarea, select, [contenteditable]";
if (event.target instanceof Element && event.target.closest(TYPING_SELECTOR)) {
return;
}
```
This means users can type freely in search boxes, filters, and form fields without triggering shortcuts.
## Sidebar menu hotkeys
The `avo-menu` DSL supports a `hotkey:` option on any item type, letting users jump directly to a sidebar section from anywhere in the admin panel.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.main_menu = -> {
section "Content", icon: "tabler/outline/files" do
resource :post, hotkey: "g p"
resource :category, hotkey: "g c"
link "Analytics", path: "/avo/analytics", hotkey: "g a"
end
}
end
```
The hotkey string follows `@github/hotkey` syntax β use space-separated keys for sequences.
For `resource` items, the hotkey can also be set on the resource class itself:
```ruby
class Avo::Resources::Post < Avo::BaseResource
self.hotkey = "g p"
end
```
The menu item automatically renders a `` badge next to the label and registers the binding.
## Visual feedback
When a hotkey fires on a button or link, Avo adds a `kbd--called` CSS class to the `` badge for one animation frame β long enough to paint a "cold press" visual β before triggering the navigation. This gives users tactile confirmation that the shortcut was recognised.
The class is cleaned up on the next `turbo:load` event.
---
# `Avo::Current`
`Avo::Current` is based on the `Current` pattern Rails exposes using [`ActiveSupport/CurrentAttributes`](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html).
On each request Avo will set some values on it.
This is what will be returned by the `current_user_method` that you've set in your initializer.
Equivalent of `request.params`.
The Rails `request`.
The `context` that you configured in your initializer evaluated in `Avo::ApplicationController`.
An instance of [`ActionView::Rendering`](https://api.rubyonrails.org/classes/ActionView/Rendering.html#method-i-view_context) off of which you can run any methods or variables that are available in your partials.
```ruby
view_context.link_to "Avo", "https://avohq.io"
```
The `locale` of the app.
You can set the `tenant_id` for the current request.
You can set the `tenant` for the current request.
**Related:**
- Multitenancy
---
# Execution context
Avo enables developers to hook into different points of the application lifecycle using blocks.
That functionality can't always be performed in void but requires some pieces of state to set up some context.
Computed fields are one example.
```ruby
field :full_name, as: :text do
"#{record.first_name} #{record.last_name}"
end
```
In that block we need to pass the `record` so you can compile that value. We send more information than just the `record`, we pass on the `resource`, `view`, `view_context`, `request`, `current_user` and more depending on the block that's being run.
## How does the `ExecutionContext` work?
The `ExecutionContext` is an object that holds some pieces of state on which we execute a lambda function.
```ruby
module Avo
class ExecutionContext
attr_accessor :target, :context, :params, :view_context, :current_user, :request
def initialize(**args)
# If target don't respond to call, handle will return target
# In that case we don't need to initialize the others attr_accessors
return unless (@target = args[:target]).respond_to? :call
args.except(:target).each do |key,value|
singleton_class.class_eval { attr_accessor "#{key}" }
instance_variable_set("@#{key}", value)
end
# Set defaults on not initialized accessors
@context ||= Avo::Current.context
@params ||= Avo::Current.params
@view_context ||= Avo::Current.view_context
@current_user ||= Avo::Current.current_user
@request ||= Avo::Current.request
end
delegate :authorize, to: Avo::Services::AuthorizationService
# Return target if target is not callable, otherwise, execute target on this instance context
def handle
target.respond_to?(:call) ? instance_exec(&target) : target
end
end
end
# Use it like so.
SOME_BLOCK = -> {
"#{record.first_name} #{record.last_name}"
}
Avo::ExecutionContext.new(target: &SOME_BLOCK, record: User.first).handle
```
This means you could throw any type of object at it and it it responds to a `call` method wil will be called with all those objects.
## Common objects
The block you'll pass to be evaluated. It may be anything but will only be evaluated if it responds to a `call` method.
Aliased to `Avo::Current.context`.
Aliased to `Avo::Current.user`.
Aliased to `Avo::Current.view_context`.
Aliased to `Avo::Current.request`.
Aliased to `Avo::Current.params`.
You can pass any variable to the `ExecutionContext` and it will be available in that block.
This is how we can expose `view`, `record`, and `resource` in the computed field example.
```ruby
Avo::ExecutionContext.new(target: &SOME_BLOCK, record: User.first, view: :index, resource: resource).handle
```
Within the `ExecutionContext` you might want to use some of your already defined helpers. You can do that using the `helpers` object.
```ruby
# products_helper.rb
class ProductsHelper
# Strips the "CODE_" prefix from the name
def simple_name(name)
name.gsub "CODE_", ""
end
end
field :name, as: :text, format_using: -> { helpers.simple_name(value) }
```
---
# Execution context
[`Avo::Services::EncryptionService`](https://github.com/avo-hq/avo/blob/main/lib/avo/services/encryption_service.rb) it's used internally by Avo when is needed to encrypt sensible params.
One example is the select all feature, where we pass the query, encrypted, through params.
## How does the [`Avo::Services::EncryptionService`](https://github.com/avo-hq/avo/blob/main/lib/avo/services/encryption_service.rb) work?
The `EncryptionService` is an service that can be called anywhere on the app.
### Public methods
Used to encrypt data
Used to decrypt data
### Mandatory arguments:
Object to be encrypted
A symbol with the purpose of encryption, can be anything, it just ***need to match when decrypting***.
### Optional arguments
This service uses [`ActiveSupport::MessageEncryptor`](https://api.rubyonrails.org/v5.2.3/classes/ActiveSupport/MessageEncryptor.html) as encryptor so [`Avo::Services::EncryptionService`](https://github.com/avo-hq/avo/blob/main/lib/avo/services/encryption_service.rb) accepts any argument specified on [`ActiveSupport::MessageEncryptor` documentation](https://api.rubyonrails.org/v5.2.3/classes/ActiveSupport/MessageEncryptor.html)
## Usage example
### Basic text:
```ruby
secret_encryption = Avo::Services::EncryptionService.encrypt(message: "Secret string", purpose: :demo)
# "x+rnETtClF2cb80PtYzlULnVB0vllf+FvwoqBpPbHWa8q6vlml5eRWrwFMcYrjI6--h2MiT1P5ctTUjwfQ--k2WsIRknFVE53QwXADDDJw=="
Avo::Services::EncryptionService.decrypt(message: secret_encryption, purpose: :demo)
# "Secret string"
```
### Objects with custom serializer:
```ruby
secret_encryption = Avo::Services::EncryptionService.encrypt(message:Course::Link.first, purpose: :demo, serializer: Marshal)
# "1UTtkhu9BDywzz8yl8/7cBZnOoM1wnILDJbT7gP+zz8M/t1Dve4QTFQP5nfHZdYK9KvFDwkizm8DTHyNZdixDtCO/M7yNMlzL8Mry1RQ3AF0qhhTzFeqb5UqyQv/Cuq+NWvQ+GXv3gFckXaNqsFSX5yDccEpRDpyNkYT4MFxOa+8hVR4roebkNKB89lb73anBDTHsTAd37y2LFiv2YaiFguPQ/...
Avo::Services::EncryptionService.decrypt(message: secret_encryption, purpose: :demo, serializer: Marshal)
# #
```
## Secret key base
:::warning
[`Avo::Services::EncryptionService`](https://github.com/avo-hq/avo/blob/main/lib/avo/services/encryption_service.rb) fetches a secret key base to be used on the encrypt / decrypt process. Make sure that you have it defined in any of the following:
`ENV["SECRET_KEY_BASE"] || Rails.application.credentials.secret_key_base || Rails.application.secrets.secret_key_base`
:::
---
# Select All
The "Select All" feature is designed to enable users to select all queried records and perform actions on the entire selection. This feature is particularly useful when dealing with large datasets, allowing users to trigger actions on all queried records, not just the ones visible on the current page.
## How does it work?
When a user toggles the "Select all" checkbox, Avo will first check to see if there are more records than just those displayed on that page, and if there are, it will ask if the user if they want to select all the records or not.
This is being done through serializing the query to be unserialized back in the action.
## Serializing the query
The query might include various filters, sorting parameters, and other custom elements. Reconstructing this query at the time of the action request can be complex. Therefore, the system serializes the entire query object into a secure format before sending it with the action request.
- **Security**: To ensure that sensitive data is protected, the serialized query is encrypted before it is transmitted.
- **Efficiency**: This approach allows the system to accurately and efficiently reconstruct the original query when the action is executed, ensuring that all relevant records are included.
:::warning
If an error occurs during the serialization process, the "Select All" feature is automatically disabled. This safeguard ensures that the page will not crash because of a coding error.
We listed a few reasons on why it might crash below.
:::
## Serialization known issues
In this section, we outline common serialization problems and provide guidance on how to resolve them effectively.
##### `normalize`
If your model includes any `normalize` proc, such as:
```ruby
normalizes :status, with: ->(status) { status }
```
Serialization may fail when a filter is applied to the normalized attribute (e.g., `status` in this example). This can result in the error `TypeError: no _dump_data is defined for class Proc`, which causes the "Select All" feature to be automatically disabled.
For applications created before Rails `7.1`, configuring the `marshalling_format_version` to `7.1` or higher will resolve the issue:
```ruby
# config/application.rb
config.active_record.marshalling_format_version = 7.1
```
More details on [`normalizes` documentation](https://api.rubyonrails.org/classes/ActiveRecord/Normalization/ClassMethods.html#method-i-normalizes).
---
# Icons
You can use SVG icons from one of the three provided [libraries](#libraries) or your own.
## How to use
These icons are easily accessible using the [`svg` method](https://github.com/avo-hq/avo-icons/blob/main/lib/avo/icons/helpers.rb#L11).
To render an icon in your application, use the svg method. This method allows you to specify the icon's path and class.
Examples:
```ruby
# in a View Component
helpers.svg("avo/editor-strike")
# in a Rails helper
svg("heroicons/outline/magnifying-glass-circle", class: "block h-6 text-gray-600")
```
```erb
# In an erb file
<%= svg 'avo/bell.svg', class: "h-4" %>
```
There are some places where Avo have custom DSL accepting the `icon` option.
In those cases you only need to specify the `icon`'s path (`avo/avocado`, `tabler/outline/bell`, or `heroicons/micro/device-phone-mobile`).
Avo applies the [`svg` method](https://github.com/avo-hq/avo-icons/blob/main/lib/avo/icons/helpers.rb#L11) behind the scenes.
```ruby
self.row_controls = -> do
action Avo::Actions::PublishPost, label: "Publish", icon: "tabler/outline/book-upload"
end
```
## Libraries
### 1. The Avo icons
Located in the [`avo`](https://github.com/avo-hq/avo/tree/main/app/assets/svgs/avo) directory.
Use them with this notation: `avo/ICON_NAME`.
These are the custom icons that Avo uses throughout the app and don't come from any of the two supported libraries.
:::warning
These icons are considered private API. We may remove or change them without notice. Use at your own risk.
:::
```erb
<%= svg "avo/avocado.svg", class: "h-4" %>
```
### 2. Tabler
[Tabler](https://tabler.io/icons) is considered the official icon library for Avo. It's huge, modern and well maintained.
The icons are provided by the [`avo-icons`](https://github.com/avo-hq/avo-icons) gem.
You can use these icons with this notation: `tabler/ICON_NAME`.
#### Example:
```erb
<%= svg "tabler/outline/bell.svg", class: "h-4" %>
```
### 3. Heroicons
Up to version 4 we used to consider [`heroicons`](https://heroicons.com/) the official icon library for Avo. While we still love it, it's quite limited and we've outgrown it.
We'll continue to support it but we recommend you to use Tabler icons instead.
The heroicons are provided by the [`avo-icons`](https://github.com/avo-hq/avo-icons) gem.
Heroicons come in 4 variants `outline`, `solid`, `mini`, and `micro`.
You can use these icons with this notation: `heroicons/VARIANT/ICON_NAME`.
We usually use the `outline` variant.
#### Examples:
```erb
<%= svg "heroicons/outline/academic-cap.svg" %>
<%= svg "heroicons/mini/arrow-path-rounded-square.svg" %>
```
## Use your own icons
You can use your own icons by placing them in the `app/assets/svgs` directory and then calling the `svg` method with the path to the icon.
```ruby
# app/assets/svgs/cat.svg
svg "cat.svg", class: "h-4"
# app/assets/svgs/my-icons/cat.svg
svg "my-icons/cat.svg", class: "h-4"
```
---
# Reserved model names and routes
When defining models in an Avo-powered application, certain names should be avoided as they are used by Avoβs internal controllers. Using these names may lead to conflicts, routing issues, or unexpected behavior.
## Model names to avoid
Avo uses the following names for its internal controllers:
- `action`
- `application`
- `association`
- `attachment`
- `base_application`
- `base`
- `chart`
- `debug`
- `home`
- `private`
- `resource`
- `search`
Using these names for models may override built-in functionality, cause routing mismatches, or introduce other conflicts.
## Why these names are reserved
Avo relies on these names for its controller and routing system. For example:
- `resource` is essential for managing Avo resources.
- `chart` is used for analytics and visualizations.
- `search` handles search functionality.
Since Avo dynamically maps models and controllers, using these names may interfere with how Avo processes requests and displays resources.
## Alternative approaches
If your application requires one of these names, consider the following alternatives:
- **Use a prefix or suffix**
- `user_resource` instead of `resource`
- `advanced_search` instead of `search`
- **Choose a synonym**
- `graph` instead of `chart`
### Using Avo with existing models
If your application already has models with these names, you can generate an Avo resource with a different name while keeping the same model class.
For example for `Resource` run the following command:
```sh
bin/rails generate avo:resource user_resource --model-class resource
```
This will generate:
- `Avo::Resources::UserResource`
- `Avo::UserResourcesController`
However, it will still use the existing `Resource` model, ensuring no conflicts arise.
## Route Conflicts with `resources :resources`
If your application has a route definition like:
```ruby
resources :resources
```
This will create path helpers such as `resources_path`, which **conflicts with [Avoβs internal routing helpers](https://github.com/avo-hq/avo/blob/main/app/helpers/avo/url_helpers.rb#L3)**. Avo uses `resources_path` internally, and having this route in your application **will override Avoβs default helpers**, potentially breaking parts of the admin panel.
### How to Fix It
To prevent conflicts, rename the route helpers to something more specific:
```ruby
resources :resources, as: 'articles'
```
This allows you to maintain the desired URL structure (`/resources`) without interfering with Avoβs internals.
---
# Rails engines and path helpers
When extending Avo or writing code that runs inside the Avo engine (e.g. custom tools, breadcrumbs, or controller overrides), you need to be aware of how Rails treats engines and their routes.
## The rule
Rails engines have **isolated routes**. When your code runs inside an engine, path helpers are resolved in that engine's context by default. To reference routes from another engine or the main application, you must use the appropriate routing proxy.
## Inside the Avo engine
When your code executes within Avoβfor example in `Avo::ToolsController`, `Avo::ResourcesController`, or when configuring breadcrumbs in `avo.rb`βyou're inside the Avo engine.
### Linking to Avo pages
Use the `avo` prefix for Avo's internal routes:
```ruby
avo.root_path
avo.resources_users_path
avo.custom_tool_path
avo.resource_path(resource: UserResource, record: @user)
```
### Linking to your main application
Use the `main_app` prefix for your application's routes:
```ruby
main_app.root_path
main_app.posts_path
main_app.user_path(@user)
```
## Why this matters
Without the prefix, Rails resolves helpers in the current context. Calling `posts_path` from inside Avo could fail (undefined method) or resolve to the wrong route if both Avo and your app define it. The `avo` and `main_app` proxies explicitly tell Rails which route set to use.
## Quick reference
| You want to link to⦠| Use this prefix |
| -------------------- | --------------- |
| Avo pages (resources, tools, dashboards, etc.) | `avo.` |
| Your main application routes | `main_app.` |
## Learn more
For the full picture on how Rails engines handle routing, see the [Rails Engines guide](https://guides.rubyonrails.org/engines.html#routes).
---
# `Avo::PanelComponent`
The panel component is one of the most used components in Avo.
```erb
<%= render Avo::PanelComponent.new(title: @product.name, description: @product.description) do |c| %>
<% c.with_tools do %>
<%= a_link(@product.link, icon: 'heroicons/solid/academic-cap', style: :primary, color: :primary) do %>
View product
<% end %>
<% end %>
<% c.with_body do %>
Product information
Style: shiny
<% end %>
<% end %>
```
## Options
All options are optional. You may render a panel without options.
```erb
<%= render Avo::PanelComponent.new do |c| %>
<% c.with_body do %>
Something here.
<% end %>
<% end %>
```
The name of the panel. It's displayed on the top under the breadcrumbs.
#### Type
`String`
Small text under the name that speaks a bit about what the panel does.
#### Type
`String`
A list of classes that should be applied to the panel container.
#### Type
`String`
A list of classes that should be applied to the body of panel.
#### Type
`String`
A hash of data attributes to be forwarded to the panel container.
#### Type
`Hash`
Toggles the breadcrumbs visibility. You can't customize the breadcrumbs yet.
#### Type
`Boolean`
## Slots
The component has a few slots where you customize the content in certain areas.
We created this slot as a place to put resource controls like the back, edit, delete, and detach buttons.
This slot will collapse under the title and description when the screen resolution falls under `1024px`.
The section is automatically aligned to the right using `justify-end` class.
```erb
<%= render Avo::PanelComponent.new(name: "Dashboard") do |c| %>
<% c.with_tools do %>
<%= a_link('/admin', icon: 'heroicons/solid/academic-cap', style: :primary) do %>
Admin
<% end %>
<% end %>
<% end %>
```
This is one of the main slots of the component where the bulk of the content is displayed.
```erb{2-4}
<%= render Avo::PanelComponent.new do |c| %>
<% c.with_body do %>
Something here.
<% end %>
<% end %>
```
Used when displaying the Grid view, it displays the data flush in the container and with no background.
```erb{2-4}
<%= render Avo::PanelComponent.new do |c| %>
<% c.with_bare_content do %>
Something here.
<% end %>
<% end %>
```
This is pretty much the same slot as `tools` but rendered under the `body` or `bare_content` slots.
```erb{2-4}
<%= render Avo::PanelComponent.new do |c| %>
<% c.with_footer_controls do %>
Something here.
<% end %>
<% end %>
```
The lowest available area at the end of the component.
```erb{2-4}
<%= render Avo::PanelComponent.new do |c| %>
<% c.with_footer do %>
Something here.
<% end %>
<% end %>
```
The sidebar will conveniently show things in a smaller area on the right of the `body`.
```erb{2-4}
<%= render Avo::PanelComponent.new do |c| %>
<% c.with_Sidebar do %>
Something tiny here.
<% end %>
<% end %>
```
---
# Native field components
One of the most important features of Avo is the ability to extend it pass the DSL. It's very important to us to enable you to add the features you need and create the best experience for your users.
That's why you can so easily create custom fields, resource tools, and custom tools altogether. When you need to augment the UI even more you can use your custom CSS and JS assets too.
When you start adding those custom views you might want to add your own fields, and you'd like to make them look like the rest of the app.
That's why Avo provides a way to use those fields beyond the DSL, in your own custom Rails partials.
## Declaring fields
When you generate a new resource tool you get access to the resource partial.
:::details Sample resource tool
```erb
<%= render Avo::PanelComponent.new title: "Post info" do |c| %>
<% c.with_tools do %>
<%= a_link('/avo', icon: 'heroicons/solid/academic-cap', style: :primary) do %>
Dummy link
<% end %>
<% end %>
<% c.with_body do %>
πͺ§ This partial is waiting to be updated
You can edit this file here app/views/avo/resource_tools/post_info.html.erb.
The resource tool configuration file should be here app/avo/resource_tools/post_info.rb.
<%
# In this partial, you have access to the following variables:
# tool
# @resource
# @resource.model
# form (on create & edit pages. please check for presence first)
# params
# Avo::Current.context
# current_user
%>
<% end %>
<% end %>
```
:::
You may add new fields using the `avo_show_field`, or `avo_edit_field` methods and use the arguments you are used to from resources.
```ruby
# In your resource file
field :name, as: :text
```
```erb
<%= avo_edit_field :name, as: :text %>
```
## The `form` option
If this is an or a view, you should pass it the `form` object that an Avo resource tool provides for you.
```erb
<%= avo_edit_field :name, as: :text, form: form %>
```
## The `value` option
When you are building a show field and you want to give it a value to show, use the `value` options
```erb
<%= avo_show_field(:photo, as: :external_image, value: record.cdn_image) %>
```
## Other field options
The fields take all the field options you are used to like, `help`, `required`, `readonly`, `placeholder`, and more.
```erb
<%= avo_edit_field :name, as: :text, form: form, help: "The user's name", readonly: -> { !current_user.is_admin? }, placeholder: "John Doe", nullable: true %>
```
## Component options
The field taks a new `component_options` argument that will be passed to the view component for that field. Please check out the field wrapper documentation for more details on that.
## `avo_field` helper
You may use the `avo_field` helper to conditionally switch from `avo_show_field` and `avo_edit_field`.
```erb
<%= avo_field :name, as: :text, view: :show %>
<%= avo_field :name, as: :text, view: :edit %>
<%= avo_field :name, as: :text, view: ExampleHelper.view_conditional %>
```
---
# Field wrappers
Each field display in your Avo resource has a field wrapper that helps display it in a cohesive way across the whole app.
This not only helps with a unitary design, but also with styling in a future theming feature.
:::info
You'll probably never have to use these components and helpers by themselves, but we'd like to document how they work as a future reference for everyone.
:::
# Index field wrapper
Each field displayed on the view is wrapped in this component that regulates the way content is displayed and makes it easy to control some options.
You may use the component `Avo::Index::FieldWrapperComponent` or the helper `index_field_wrapper`.
This option renders a dash `β` if the content inside responds to true on the `blank?` method.
In the example below, we'd like to show the field as a red checkmark even if the content is `nil`.
#### Default
`true`
```erb
<%= index_field_wrapper **field_wrapper_args, dash_if_blank: false do %>
<%= render Avo::Fields::Common::BooleanCheckComponent.new checked: @field.value %>
<% end %>
```
Wraps the content in a container with `flex items-center justify-center` classes making everything centered horizontally and vertically.
#### Default
`false`
```erb
<%= index_field_wrapper **field_wrapper_args, center_content: true do %>
<%= render Avo::Fields::Common::BooleanCheckComponent.new checked: @field.value %>
<% end %>
```
Removes the padding around the field allowing it to flow from edge to edge.
#### Default
`false`
```erb
<%= index_field_wrapper **field_wrapper_args, flush: false do %>
<%= render Avo::Fields::Common::BooleanCheckComponent.new checked: @field.value %>
<% end %>
```
The instance of the field. It's usually passed in with the `field_wrapper_args`.
```erb
<%= index_field_wrapper **field_wrapper_args do %>
<%= render Avo::Fields::Common::BooleanCheckComponent.new checked: @field.value %>
<% end %>
```
The instance of the resource. It's usually passed in with the `field_wrapper_args`.
```erb
<%= index_field_wrapper **field_wrapper_args do %>
<%= render Avo::Fields::Common::BooleanCheckComponent.new checked: @field.value %>
<% end %>
```
# Show & Edit field wrapper
The and field wrappers are actually the same component.
You may use the component `Avo::Index::FieldWrapperComponent` or the helper `field_wrapper`.
## Field wrapper areas
Each field wrapper is divided in three areas.
### Label
This is where the field name is being displayed. This is also where the required asterisk is added for required fields.
### Value
This area holds the actual value of the field or it's representation. The falue can be simple text or more advanced types like images, advanced pickers, and content editors.
At the bottom the help text is going to be shown on the view and below it the validation error.
### Extra
This space is rarely used and it's there just to fill some horizontal space so the content doesn't span to the whole width and maintain its readability. With the introduction of the sidebar, this space will be ignored
## Options
This option renders a dash `β` if the content inside responds to true on the `blank?` method.
In the example below, we'd like to show the field as a red checkmark even if the content is `nil`.
#### Default
`true`
```erb
<%= field_wrapper **field_wrapper_args, dash_if_blank: false do %>
<%= render Avo::Fields::Common::BooleanCheckComponent.new checked: @field.value %>
<% end %>
```
This renders the field in a more compact way by removing the **Extra** area and decresing the width of the **Label** and **Content** areas.
This is enabled on the fields displayed in actions.
#### Default
`false`
```erb
<%= field_wrapper **field_wrapper_args, compact: true do %>
<%= render Avo::Fields::Common::BooleanCheckComponent.new checked: @field.value %>
<% end %>
```
Pass in some data attributes. Perhaps you would like to attach a StimulusJS controller to this field.
```erb
<%= field_wrapper **field_wrapper_args, data: {controller: "boolean-check"} do %>
<%= render Avo::Fields::Common::BooleanCheckComponent.new checked: @field.value %>
<% end %>
```
This removes the **Extra** area and renders the **Value** area full width.
This is used on fields that require a larger area to be displayed like WYSIWYG editors, `KeyValue`, or file fields.
#### Default
`false`
```erb
<%= field_wrapper **field_wrapper_args, full_width: true do %>
<%= render Avo::Fields::Common::BooleanCheckComponent.new checked: @field.value %>
<% end %>
```
The instance of the form that is going to be populated. It's usually passed in with the `field_wrapper_args` on the view.
```erb
<%= field_wrapper **field_wrapper_args do %>
<%= render Avo::Fields::Common::BooleanCheckComponent.new checked: @field.value %>
<% end %>
```
The instance of the field. It's usually passed in with the `field_wrapper_args`.
```erb
<%= field_wrapper **field_wrapper_args do %>
<%= render Avo::Fields::Common::BooleanCheckComponent.new checked: @field.value %>
<% end %>
```
The text that is going to be displayed below the actual field on the view.
```erb
<%= field_wrapper **field_wrapper_args, help: "Specify if the post is published or not." do %>
<%= render Avo::Fields::Common::BooleanCheckComponent.new checked: @field.value %>
<% end %>
```
The text that is going to be displayed in the **Label** area. You might want to override it.
```erb
<%= field_wrapper **field_wrapper_args, label: "Post is published" do %>
<%= render Avo::Fields::Common::BooleanCheckComponent.new checked: @field.value %>
<% end %>
```
The instance of the resource. It's usually passed in with the `field_wrapper_args`.
```erb
<%= field_wrapper **field_wrapper_args do %>
<%= render Avo::Fields::Common::BooleanCheckComponent.new checked: @field.value %>
<% end %>
```
Display the field in a column layout with the label on top of the value
```erb
<%= field_wrapper **field_wrapper_args, style: "background: red" do %>
<%= render Avo::Fields::Common::BooleanCheckComponent.new checked: @field.value %>
<% end %>
```
The you might want to pass some styles to the wrapper to change it's looks.
```erb
<%= field_wrapper **field_wrapper_args, style: "background: red" do %>
<%= render Avo::Fields::Common::BooleanCheckComponent.new checked: @field.value %>
<% end %>
```
The view where the field is diplayed so it knows if it's a or view. It's usually passed in with the `field_wrapper_args`.
```erb
<%= field_wrapper **field_wrapper_args do %>
<%= render Avo::Fields::Common::BooleanCheckComponent.new checked: @field.value %>
<% end %>
```
---
# `Avo::ApplicationController`
## On extending the `ApplicationController`
You may sometimes want to add functionality to Avo's `ApplicationController`. That functionality may be setting attributes to `Current` or multi-tenancy scenarios.
When you need to do that, you may feel the need to override it with your own version. That means you go into the source code, find `AVO_REPO/app/controllers/avo/application_controller.rb`, copy the whole thing into your own `YOUR_APP/app/controllers/avo/application_controller.rb` file inside your app, and add your own piece of functionality.
```ruby{10,14-16}
# Copied from Avo to `app/controllers/avo/application_controller.rb`
module Avo
class ApplicationController < ::ActionController::Base
include Pagy::Method
include Avo::ApplicationHelper
include Avo::UrlHelpers
protect_from_forgery with: :exception
around_action :set_avo_locale
before_action :multitenancy_detector
# ... more Avo::ApplicationController methods
def multitenancy_detector
# your logic here
end
end
end
```
That will work just fine until the next time we update it. After that, we might add a method, remove one, change the before/after actions, update the helpers and do much more to it.
**That will definitely break your app the next time when you upgrade Avo**. Avo's private controllers are still considered private APIs that may change at any point. These changes will not appear in the changelog or the upgrade guide.
## Responsibly extending the `ApplicationController`
There is a right way of approaching this scenario using Ruby modules or concerns.
First, you create a concern with your business logic; then you include it in the parent `Avo::ApplicationController` like so:
```ruby{6-8,11-13,18}
# app/controllers/concerns/multitenancy.rb
module Multitenancy
extend ActiveSupport::Concern
included do
before_action :multitenancy_detector
# or
prepend_before_action :multitenancy_detector
end
def multitenancy_detector
# your logic here
end
end
# configuration/initializers/avo.rb
Rails.configuration.to_prepare do
Avo::ApplicationController.include Multitenancy
end
```
With this technique, the `multitenancy_detector` method and its `before_action` will be included safely in `Avo::ApplicationController`.
:::info
If you'd like to add a `before_action` before all of Avo's before actions, use `prepend_before_action` instead. That will run that code first and enable you to set an account or do something early on.
:::
## Override `ApplicationController` methods
Sometimes you don't want to add methods but want to override the current ones.
For example, you might want to take control of the `Avo::ApplicationController.fill_record` method and add your own behavior.
TO do that you should change a few things in the approach we mentioned above. First we want to `prepend` the concern instead of `include` it and next, if we want to run a class method, we used `prepended` instead of `included`.
```ruby{5-8,10-12,14-17,23}
# app/controllers/concerns/application_controller_overrides.rb
module ApplicationControllerOverrides
extend ActiveSupport::Concern
# we use the `prepended` block instead of `included`
prepended do
before_action :some_hook
end
def some_hook
# your logic here
end
def fill_record
# do some logic here
super
end
end
# configuration/initializers/avo.rb
Rails.configuration.to_prepare do
# we will prepend instead of include
Avo::ApplicationController.prepend ApplicationControllerOverrides
end
```
**Related:**
- Multitenancy
---
# Asset manager
In your plugins or custom content you might want to add a new stylesheet or javascript file to be loaded inside Avo.
You can manually add them to the `_head.html.erb` or `_pre_head.html.erb` files or you can use the `AssetManager`.
Next, the asset manager will add them to the `` element of Avo's layout file.
## Add a stylesheet file
Use `Avo.asset_manager.add_stylesheet PATH`
Example:
```ruby
Avo.asset_manager.add_stylesheet "/public/magic_file.css"
Avo.asset_manager.add_stylesheet Avo::Engine.root.join("app", "assets", "stylesheets", "magic_file.css")
```
## Add a javascript file
Use `Avo.asset_manager.add_javascript PATH`
Example:
```ruby
Avo.asset_manager.add_javascript "/public/magic_file.js"
Avo.asset_manager.add_javascript Avo::Engine.root.join("app", "javascripts", "magic_file.js")
```
---
# Plugins
:::warning
This feature is in beta and we might change the API as we develop it.
These docs are in beta too, so please [ask for more information](https://github.com/avo-hq/avo/discussions) when you need it.
:::
## Overview
Plugins are a way to extend the functionality of Avo.
### Light layer
We are in the early days of the plugin system and we're still figuring out the best way to do it. This is why we have a light layer that you can use to extend the functionality of Avo.
This means we provide two hooks that you can use to extend the functionality of the Rails app, and a few Avo APIs to add scrips and stylesheets.
## Register the plugin
The way we do it is through an initializer. We mostly use the `engine.rb` file to register the plugin.
```ruby{8-15}
# lib/avo/feed_view/engine.rb
module Avo
module FeedView
class Engine < ::Rails::Engine
isolate_namespace Avo::FeedView
initializer "avo-feed-view.init" do
# Avo will run this hook on boot time
ActiveSupport.on_load(:avo_boot) do
# Register the plugin
Avo.plugin_manager.register :feed_view
# Register the mounting point
Avo.plugin_manager.mount_engine Avo::FeedView::Engine, at: "/feed_view"
end
end
end
end
end
```
This will add the plugin to a list of plugins which Avo will run the hooks on.
## Hook into Avo
```ruby
module Avo
module FeedView
class Engine < ::Rails::Engine
isolate_namespace Avo::FeedView
initializer "avo-feed-view.init" do
ActiveSupport.on_load(:avo_boot) do
Avo.plugin_manager.register :feed_view
# Add some concerns
Avo::Resources::Base.include Avo::FeedView::Concerns::FeedViewConcern
# Remove some concerns
Avo::Resources::Base.included_modules.delete(Avo::Concerns::SOME_CONCERN)
# Add asset files to be loaded by Avo
# These assets will be added to Avo's `application.html.erb` layout file
Avo.asset_manager.add_javascript "/avo-advanced-assets/avo_advanced"
Avo.asset_manager.add_stylesheet "/avo-kanban-assets/avo_kanban"
end
ActiveSupport.on_load(:avo_init) do
# Run some code on each request
Avo::FeedView::Current.something = VALUE
end
end
end
end
end
```
## Hooks
The `avo_boot` hook is called when the parent Rails application boots up. This is where you can register your scripts and stylesheets and also add your functionality to Avo.
We use it heavily to add our own concerns to the `Avo::BaseResource` and `Avo::BaseController` classes and even extend the `Avo::ApplicationController` class.
The `avo_init` hook is called on every request done inside Avo. You can use this hook to attach some code to the `Avo::App.context` object or do other things.
:::info
We don't use it as much in our plugins as we do in the `avo_boot` hook.
:::
## Avo `AssetManager`
We use the `AssetManager` to add our own asset files (JavaScript and CSS) to be loaded by Avo. They will be added in the `` section of Avo's layout file.
It has two methods:
```ruby
Avo.asset_manager.add_javascript "/avo-kanban-assets/avo_kanban"
```
This snippet will add the `/avo-kanban-assets/avo_kanban.js` file to the `` section of Avo's layout file.
```ruby
Avo.asset_manager.add_stylesheet "/avo-kanban-assets/avo_kanban"
```
This snippet will add the `/avo-kanban-assets/avo_kanban.css` file to the `` section of Avo's layout file.
## Using a middleware to surface asset files
One tricky thing to do with Rails Engines is to expose some asset files to the parent Rails app.
The way we do it is by using a middleware that will serve the files from the Engine's `app/assets/builds` directory.
So `app/assets/builds/feed_view.js` from the `feed_view` engine will be served by the parent Rails app at `/feed-view-assets/feed_view.js` with the following middleware added to your `engine.rb` file.
```ruby
module Avo
module FeedView
class Engine < ::Rails::Engine
isolate_namespace Avo::FeedView
initializer "avo-feed-view.init" do
ActiveSupport.on_load(:avo_boot) do
Avo.plugin_manager.register :feed_view
end
end
config.app_middleware.use(
Rack::Static,
urls: ["/feed-view-assets"], # π This is the path where the files will be served
root: root.join("app", "assets", "builds") # π This is the path where the files are located
)
end
end
end
```
:::info
Avo doesn't compile the assets in any way, but just adds them to the layout file. This means that the assets should be compiled and ready for the browser to use them.
We use [`jsbundling-rails`](https://github.com/rails/jsbundling-rails) with `esbuild` to compile the assets before packaging them in the `gem` file.
Please check out [the scripts](https://github.com/avo-hq/avo/blob/main/package.json) we use.
:::
## Create your own plugin
We don't yet have a generator for that but what we do is to create a new Rails Engine and add the plugin to it.
1. Run `rails plugin new feed-view`
1. Add the plugin to the `engine.rb` file
1. Register the plugin to the `lib/avo/feed_view/engine.rb` file
1. Optionally add assets
1. Add the plugin to your app's `Gemfile` using the `path` option to test it out
---
# Custom view types
Avo ships with three built-in view types for the resource index: **table**, **grid**, and **map**. You can restrict which ones are available per-resource, or create entirely new view types through plugins.
## Restricting available view types
By default, Avo displays all the configured view types on the view switcher. For example, if you have `map_view` and `grid_view` configured, both of them, along with the `table_view`, will be available on the view switcher.
However, there might be cases where you only want to make a specific view type available without removing the configurations for other view types. This can be achieved using the `view_types` class attribute on the resource. Note that when only one view type is available, the view switcher will not be displayed.
```ruby{3}
class Avo::Resources::City < Avo::BaseResource
# ...
self.view_types = :table
#...
end
```
If you want to make multiple view types available, you can use an array. The icons on the view switcher will follow the order in which they are declared in the configuration.
```ruby{3}
class Avo::Resources::City < Avo::BaseResource
# ...
self.view_types = [:table, :grid]
#...
end
```
You can also dynamically restrict the view types based on user roles, params, or other business logic. To do this, assign a block to the `view_types` attribute. Within the block, you'll have access to `resource`, `record`, `params`, `current_user`, and other default accessors provided by `ExecutionContext`.
```ruby{3-9}
class Avo::Resources::City < Avo::BaseResource
# ...
self.view_types = -> do
if current_user.is_admin?
[:table, :grid]
else
:table
end
end
#...
end
```
## Creating a custom view type through a plugin
You can register entirely new view types from a Rails Engine (Avo plugin). The view type will appear in the view switcher alongside the built-in ones and can be set as the default for any resource.
The process has three parts: **create the component**, **register the view type**, and **configure a resource to use it**.
### 1. Create the view type component
Every view type is a ViewComponent that inherits from `Avo::ViewTypes::BaseViewTypeComponent`. The base class provides these props automatically:
| Prop | Description |
| ----------------- | ------------------------------------------------------------- |
| `resources` | Array of Avo resource wrappers (call `.record` for the model) |
| `resource` | The Avo resource class |
| `pagy` | Pagination object |
| `query` | The current query |
| `turbo_frame` | The Turbo Frame ID |
| `index_params` | Current index parameters |
| `reflection` | Association reflection (if nested) |
| `parent_record` | Parent record (if nested) |
| `parent_resource` | Parent resource (if nested) |
| `actions` | Available actions |
Create your component class inside your engine's namespace:
```ruby
# app/components/my_plugin/view_types/timeline_view_type_component.rb
class MyPlugin::ViewTypes::TimelineViewTypeComponent < Avo::ViewTypes::BaseViewTypeComponent # [!code focus]
def grouped_resources
@resources.group_by { |r| r.record.created_at.to_date }
end
def empty?
@resources.blank?
end
end
```
Then create the template. You have full control over the HTML β render items however you like and include the paginator at the bottom:
```erb
<%# app/components/my_plugin/view_types/timeline_view_type_component.html.erb %>
<% if empty? %>
No records found.
<% else %>
<% grouped_resources.each do |date, resources| %>
<%= date.strftime("%B %d, %Y") %>
<% resources.each do |resource| %>
<%= resource.record.title %>
<% end %>
<% end %>
<% end %>
<%= render paginator_component %>
```
:::info
The `paginator_component` method is inherited from the base class. Always render it to keep pagination working.
:::
### 2. Register the view type
In your engine's initializer, register the view type with `Avo.plugin_manager.register_view_type`. This must happen inside the `ActiveSupport.on_load(:avo_boot)` hook so Avo core is loaded first.
```ruby
# lib/my_plugin/engine.rb
module MyPlugin
class Engine < ::Rails::Engine
initializer "my_plugin.init" do
ActiveSupport.on_load(:avo_boot) do
Avo.plugin_manager.register "my_plugin" # [!code focus:5]
Avo.plugin_manager.register_view_type :timeline,
component: "MyPlugin::ViewTypes::TimelineViewTypeComponent",
icon: "tabler/outline/timeline-event",
active_icon: "tabler/filled/timeline-event"
end
end
end
end
```
`register_view_type` accepts these options:
| Option | Required | Description |
| ----------------- | -------- | ----------------------------------------------------- |
| `component` | Yes | Component class or string (auto-constantized) |
| `icon` | Yes | Icon path for the inactive state in the view switcher |
| `active_icon` | Yes | Icon path for the active state in the view switcher |
| `translation_key` | No | I18n key for the view type name in tooltips |
:::info
The `component` can be passed as a string (`"MyPlugin::ViewTypes::TimelineViewTypeComponent"`) or as the class itself. Strings are constantized at render time, which avoids load-order issues during boot.
:::
### 3. Configure a resource to use it
Once registered, you can use your custom view type in any resource:
```ruby
class Avo::Resources::Event < Avo::BaseResource
self.default_view_type = :timeline # [!code focus:2]
self.view_types = [:table, :timeline]
# ... fields
end
```
Setting `default_view_type` makes your view type the one users see first. Including `:table` in `view_types` keeps the table view available as a fallback via the view switcher.
## How it works under the hood
When a user visits a resource index, Avo resolves the current view type through the `ViewTypeManager`:
1. The `ViewTypeManager` holds a registry of all view types (built-in + plugin-registered)
2. It looks up the component class for the current view type via `component_for(name)`
3. The `ResourceListingComponent` renders that component with all the standard props
4. The view switcher partial reads the registry for icons and renders toggle buttons for each available view type
The view type is persisted in the URL as the `view_type` query parameter, so it survives page reloads and can be bookmarked.
## Full example: avo-notifications
The `avo-notifications` gem ships a `:notification` view type as a real-world reference. Here's how it's wired up:
**Registration** in the engine:
```ruby
# lib/avo/notifications/engine_content.rb
Avo.plugin_manager.register_view_type :notification,
component: "Avo::Notifications::ViewTypes::NotificationViewTypeComponent",
icon: "tabler/outline/bell",
active_icon: "tabler/filled/bell"
```
**Component** inherits from the base and adds domain logic (time grouping, unread counts):
```ruby
# app/components/avo/notifications/view_types/notification_view_type_component.rb
class Avo::Notifications::ViewTypes::NotificationViewTypeComponent < Avo::ViewTypes::BaseViewTypeComponent
def grouped_resources
@resources.group_by { |resource| time_group(resource.record.created_at) }
end
def unread_count
@resources.count { |resource| user_unread?(resource.record) }
end
# ...
end
```
**Resource** sets it as the default:
```ruby
# app/avo/resources/avo_notification.rb
class Avo::Resources::AvoNotification < Avo::BaseResource
self.default_view_type = :notification
self.view_types = [:table, :notification]
end
```
## Adding styles
If your view type needs custom CSS, add it to your engine's stylesheet. Follow BEM methodology with Tailwind `@apply` directives:
```css
/* app/assets/stylesheets/my-plugin/application.css */
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);
@layer components {
.timeline-view__item {
@apply flex gap-3 px-5 py-3.5 transition-colors;
&:hover {
@apply bg-gray-50;
}
}
}
```
Then register the stylesheet in your engine initializer:
```ruby
Avo.asset_manager.add_stylesheet "my-plugin/application"
```
## Adding interactivity with Stimulus
For client-side behavior (filtering, toggling, etc.), create a Stimulus controller in your engine and register it:
```javascript
// app/javascript/controllers/my_filter_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["item"];
static values = { filter: { type: String, default: "all" } };
applyFilter() {
this.itemTargets.forEach((item) => {
item.toggleAttribute("hidden", !this.shouldShow(item));
});
}
shouldShow(item) {
if (this.filterValue === "all") return true;
return item.dataset.active === "true";
}
}
```
```javascript
// app/javascript/controllers/index.js
import MyFilterController from "./my_filter_controller";
const application = window.Stimulus;
application.register("my-filter", MyFilterController);
```
Then use it in your template with `data-controller="my-filter"` and `data-action` attributes. Use the `hidden` HTML attribute (not CSS classes) for toggling visibility.
---
# Guides
These are various guides on how to build some things with Avo or how to integrate with different pieces of tech.
Some guides have been written by us, and some by our community members.
# Videos
We regularly publish videos on our [YouTube channel](https://www.youtube.com/@avo_hq).
SupeRails featured Avo in a few of [their videos](https://superails.com/playlists/avo).
- [How to filter associations using dynamic filters](https://www.loom.com/share/d8bd49086d014d77a3013796c8480339)
---
# Act as taggable on integration
A popular way to implement the tags pattern is to use the [`acts-as-taggable-on`](https://github.com/mbleigh/acts-as-taggable-on) gem.
Avo already supports it in the `tags` field, but you might also want to browse the tags as resources.
[This template](https://railsbytes.com/templates/VRZskb) will add the necessarry resource and controller files to your app.
Run `rails app:template LOCATION='https://railsbytes.com/script/VRZskb'`
If you're using the menu editor don't forget to add the resources to your menus.
```ruby
resource :taggings
resource :tags
```
---
# Acts As Tenant Integration
Recipe [contributed](https://github.com/avo-hq/docs.avohq.io/pull/218) by [SahSantoshh](https://github.com/sahsantoshh).
:::warning
The guide expressed here shows how you we can add subdomain-level multitenancy (sah.example.org, adrian.example.org, etc).
This makes for more than one URL per application which in turn requires a special license.
To get more information please reach out to us.
:::
There are different ways to achieve multi-tenancy in an application.
We already have a doc which describes about Multitenancy with Avo.
Here we will deep dive in integrating [Acts As Tenant](https://github.com/ErwinM/acts_as_tenant) which supports row-level multitenancy with Avo.
In this implementation we will be setting tenant to subdomain.
:::info
Check out the [acts_as_tenant](https://github.com/ErwinM/acts_as_tenant) documentation for reference.
:::
## Installation
To use it, add it to your Gemfile:
```ruby
gem 'acts_as_tenant'
```
## Tenant
Let's create model for tenant. We are using `Account` as our tenant.
**Account Migration and Model class**
:::code-group
```ruby [db/migrate/random_number_create_accounts.rb]{3}
# Migration
class CreateAccounts < ActiveRecord::Migration[7.1]
def change
create_table :accounts do |t|
t.string :name
t.string :subdomain
t.timestamps
end
add_index :accounts, :subdomain, unique: true
add_index :accounts, :created_at
end
end
```
```ruby [app/models/account.rb]{3}
# Account model handles Tenant management
class Account < ApplicationRecord
MAX_SUBDOMAIN_LENGTH = 20
validates :name, :subdomain, presence: true
validates_uniqueness_of :name, :subdomain, case_sensitive: false
validates_length_of :subdomain, :name, maximum: MAX_SUBDOMAIN_LENGTH
end
```
:::
## Scope models
___
Now let's add users to `Account`. Here I am assuming to have an existing user model which is used for `Authentication`.
Similarly we can scope other models.
:::code-group
```ruby [db/migrate/random_number_add_account_to_users.rb]{3}
class AddAccountToUsers < ActiveRecord::Migration
def up
add_column :users, :account_id, :integer # if we have existing user set null to true then update the data using seed
add_index :users, :account_id
end
end
```
```ruby [app/models/account.rb]{3}
# Authentication
class User < ActiveRecord::Base
acts_as_tenant(:account)
end
```
:::
## Setting the current tenant
There are three ways to set the current tenant but we be using the subdomain to lookup the current tenant.
Since Avo has it's own `Application Controller` so there is no point in setting the tenant in Rails default `Application Controller` but we will set it there as well just to be safe site and also we might have some other pages other than Admin Dashboard supported by Avo.
:::code-group
```ruby [app/controllers/concerns/multitenancy.rb]{3}
# Multitenancy, to set the current account/tenant.
module Multitenancy
extend ActiveSupport::Concern
included do
prepend_before_action :set_current_account
end
def set_current_account
hosts = request.host.split('.')
# just to make sure we are using subdomain path
subdomain = (hosts[0] if hosts.length > 2)
# We only allow users to login from their account specific subdomain not outside of it.
sign_out(current_user) if subdomain.blank?
current_account = Account.find_by(subdomain:)
sign_out(current_user) if current_account.blank?
# set tenant for Avo and ActAsTenant
ActsAsTenant.current_tenant = current_account
Avo::Current.tenant = current_account
Avo::Current.tenant_id = current_account.id
end
end
```
```ruby [config/initializers/avo.rb]{3}
Avo.configure do |config|
# configuration values
end
Rails.configuration.to_prepare do
Avo::ApplicationController.include Multitenancy
end
```
:::
Now, whenever we navigate to https://sahsantoshh.example.com/ the tenant & the tenant_id will be set to **sahsantoshh**.
## Move existing data to model
We might have to many users and other records which needs to be associated with `Account`.
For example, we will only move users record to the account
:::code-group
```ruby [db/seeds.rb]{3}
# Create default/first account where we want to associate exiting data
account = Account.find_or_create_by!(name: 'Nepal', subdomain: 'sahsantoshh')
User.unscoped.in_batches do |relation|
relation.update_all(account_id: account.id)
sleep(0.01) # throttle
end
```
:::
Now run the seed command to update existing records
---
# Add nested fields to CRUD forms
Please follow this guide to learn how to implement nested fields on Avo forms.
---
# Use Avo in an `api_only` Rails app
**After Avo version 2.9 π**
The `api_mode` might not be supported. The reason for that is that Rails does not generate some paths for the [`resource` route helper](https://guides.rubyonrails.org/routing.html#resource-routing-the-rails-default). Most important being the `new` and `edit` paths. That's because APIs don't have the `new` path (they have the `create` path).
But you're probably safer using Rails with `api_only` disabled (`config.api_only = false`).
**Pre Avo version 2.9 π**
You might have an api-only Rails app where you'd like to use Avo. In my early explorations I found that it needs the `::ActionDispatch::Flash` middleware for it to properly work.
So, add it in your `application.rb` file.
```ruby{18}
require_relative "boot"
require "rails/all"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module RailApi
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.0
# Only loads a smaller set of middleware suitable for API only apps.
# Middleware like session, flash, cookies can be added back manually.
# Skip views, helpers and assets when generating a new resource.
config.api_only = true
config.middleware.use ::ActionDispatch::Flash
end
end
```
---
# Attachment Policy Extension for Pundit
When using Pundit, it's common to define permissions for each attachment action (e.g., upload, delete, download) individually. This can lead to repetitive code like:
```ruby
def upload_logo?
update?
end
def delete_logo?
update?
end
def download_logo?
update?
end
```
To streamline this process, you can extend your `ApplicationPolicy` with a helper method that dynamically handles attachment permissions.
## Step 1: Add the `method_missing` Helper to `ApplicationPolicy`
This method intercepts calls to undefined policy methods that follow a specific pattern and delegates them to a predefined permission mapping:
```ruby
def method_missing(method_name, *args)
if method_name.to_s =~ /^(upload|delete|download)_(.+)\?$/
action = Regexp.last_match(1).to_sym
attachment = Regexp.last_match(2).to_sym
return attachment_concerns[attachment][action] if attachment_concerns.key?(attachment) &&
attachment_concerns[attachment].key?(action)
end
super
end
```
## Step 2: Define `attachment_concerns` in Your Policy
In each model-specific policy, define the permitted actions for each attachment:
```ruby
def attachment_concerns
{
logo: {
upload: update?,
delete: update?,
download: update?
}
}
end
```
With this setup, calls to `upload_logo?`, `delete_logo?`, or `download_logo?` will be automatically resolved based on the configuration in `attachment_concerns`, reducing boilerplate and improving maintainability.
---
# Add Avo behind Basic Authentication
Because in Rails we commonly do that using a static function on the controller we need to [safely extend the controller](https://avohq.io/blog/safely-extend-a-ruby-on-rails-controller) to contain that function.
In actuality we will end up with something that behaves like this:
```ruby{2}
class Avo::ApplicationController < ::ActionController::Base
http_basic_authenticate_with name: "adrian", password: "password"
# More methods here
end
```
## Safely add it to Avo
We described the process in depth in [this article](https://avohq.io/blog/safely-extend-a-ruby-on-rails-controller) so let's get down to business.
1. Add the `BasicAuth` concern
1. The concern will prepend the basic auth method
1. `include` that concern to Avo's `ApplicationController`
:::warning
Ensure you restart the server after you extend the controller in this way.
:::
```ruby{8,20}
# app/controllers/concerns/basic_auth.rb
module BasicAuth
extend ActiveSupport::Concern
# Authentication strategy came from this article:
# https://dev.to/kevinluo201/setup-a-basic-authentication-in-rails-with-http-authentication-388e
included do
http_basic_authenticate_with name: "adrian", password: "password"
end
end
# config/initializers/avo.rb
Avo.configure do |config|
# Avo configuration
end
# Add this to include it in Avo's ApplicationController
Rails.configuration.to_prepare do
# Add basic authentication to Avo
Avo::ApplicationController.include BasicAuth
end
```
---
# Bulk destroy action using customizable controls
In this guide, we'll explore how to implement a customizable bulk destroy action in Avo. This allows to delete multiple records at once while providing users with a clear, informative interface that shows exactly what will be deleted. The implementation includes a confirmation message with a scrollable list of records to be deleted and clear warning messages about the permanent nature of this action.
The bulk destroy action is particularly useful when you need to:
- Delete multiple records simultaneously
- Show users exactly which records will be affected
- Provide clear warnings about the irreversible nature of the action
- Handle the deletion process with proper error handling
## Bulk destroy action
```ruby
# app/avo/actions/bulk_destroy.rb
class Avo::Actions::BulkDestroy < Avo::BaseActionAdd commentMore actions
self.name = "Bulk Destroy"
self.message = -> {
tag.div do
safe_join([
"Are you sure you want to delete these #{query.count} records?",
tag.div(class: "text-sm text-gray-500 mt-2 mb-2 font-bold") do
"These records will be permanently deleted:"
end,
tag.ul(class: "ml-4 overflow-y-scroll max-h-64") do
safe_join(query.map do |record|
tag.li(class: "text-sm text-gray-500") do
"- #{::Avo.resource_manager.get_resource_by_model_class(record.class).new(record:).record_title}"
end
end)
end,
tag.div(class: "text-sm text-red-500 mt-2 font-bold") do
"This action cannot be undone."
end
])
end
}
def handle(query:, **)
query.each(&:destroy!)
succeed "Deleted #{query.count} records"
rescue => e
fail "Failed to delete #{query.count} records: #{e.message}"
end
end
```
## Register it on all resources with except list
Once you've defined your bulk destroy action, you might want to make it available across multiple resources while excluding specific ones. This approach allows you to implement the action globally while maintaining control over where it can be used. The following configuration adds the bulk destroy functionality to your base resource class with a customized appearance and selective implementation.
Related docs:
- [Extending Avo::BaseResource](https://docs.avohq.io/3.0/resources.html#extending-avo-baseresource)
- [Customizable controls](https://docs.avohq.io/3.0/customizable-controls.html)
```ruby
# app/avo/base_resource.rb
class Avo::BaseResource < Avo::Resources::Base
self.index_controls = -> {
# Don't show bulk destroy for these resources
return default_controls if resource.class.in?([
Avo::Resources::User,
Avo::Resources::Post,
Avo::Resources::Product,
Avo::Resources::Person,
Avo::Resources::Spouse,
Avo::Resources::Movie,
Avo::Resources::Fish,
])
bulk_title = tag.span(class: "text-xs") do
safe_join([
"Delete all selected #{resource.plural_name.downcase}",
tag.br,
"Select at least one #{resource.singular_name.downcase} to run this action"
])
end.html_safe
action Avo::Actions::BulkDestroy,
icon: "heroicons/solid/trash",
color: "red",
label: "",
style: :outline,
title: bulk_title
default_controls
}
end
```
## Feedback
We value your experience with this bulk destroy implementation! Whether you've successfully implemented it, made improvements, or encountered challenges, your feedback helps the community. Here's how you can contribute:
- **Success stories**: Share how you've implemented this in your project and what benefits it brought to your workflow
- **Improvements**: Tell us if you've enhanced this implementation with additional features or better error handling
- **Questions**: Ask about specific use cases or implementation details you're unsure about
- **Troubleshooting**: If you're experiencing issues, describe your setup and the problems you're encountering
- **Customizations**: Share how you've adapted this to better suit your specific needs
- **Alternative approaches**: If you've implemented bulk destroy differently, we'd love to hear about your solution
You can share your feedback through [Feedback: Bulk destroy action using customizable controls](https://github.com/avo-hq/avo/discussions/3930).
---
# Conditionally render styled rows
We've had [a request](https://discord.com/channels/740892036978442260/1197693313520771113) come in from a customer to style their soft-deleted records differently than the regular ones.
Their first idea was to add a new option to Avo to enable that. They even tried to monkey-patch our code to achieve that.
It's a "fair" strategy; we're not judging.
Our impression was to add a new option, too, but in the end, we found a better solution. Something that doesn't involve monkey-patching or us adding new code to the framework.
New code that we should maintain in the future and bring on more and more requests.
## Solution
The solution came to me a little while after the request came over, and it's so simple!
**Use the `has` CSS selector.**
#### 1. Attach a CSS class to the `id` field of the records you want to mark
```ruby
def fields
field :id, as: :id, html: -> {
index do
wrapper do
classes do
# We'll mark every record that has an even `id`
if record.id % 2 == 0
"soft-deleted"
end
end
end
end
}
end
```
#### 2. Target the row that has that child element and style it as you need it
```css
tr[data-component-name="avo/index/table_row_component"]:has(.soft-deleted){
background: #fef2f2;
}
/* you may even target a specific resource by it's name */
tr[data-component-name="avo/index/table_row_component"][data-resource-name="course_links"]:has(.soft-deleted){
background: #fef2f2;
}
```
Of course, I chose a trivial rule like the records with an even `id` column, but you can tweak that rule as needed.
I think there's a lesson or two to be learned from this, which I wrote about in [this article](https://avohq.io/blog/state-the-problem-not-the-solution).
---
# How to Use Custom IDs with Avo
Avo seamlessly integrates custom IDs, including popular solutions like FriendlyID, prefixed IDs, or Hashids. Below, you'll find examples illustrating each approach for effortless customization within your application.
## Example with FriendlyID
FriendlyID is a gem that allows you to generate pretty URLs and unique IDs. To integrate FriendlyID with Avo, follow these steps:
**Install [friendly_id](https://github.com/norman/friendly_id) gem by adding this line to your application's Gemfile:**
```ruby
gem "friendly_id", "~> 5.5.0"
```
And then execute:
```bash
bundle install
```
**Generate and run the migration to add a slug column to your model:**
```bash
rails generate friendly_id
rails db:migrate
```
**Add `friendly_id` to your model:**
:::warning
To enable full compatibility with Avo, you need to use the `use: :finders` option.
It also can be used as array of options like `use: [:finders, :slugged]`.
:::
```ruby{3,6}
# app/models/post.rb
class Post < ApplicationRecord
extend FriendlyId
# This post model have a name column
friendly_id :name, use: :finders
end
```
With this setup, you can use `Post.find("bar")` to find records by their custom IDs.
:::info
For a version of [friendly_id](https://github.com/norman/friendly_id) smaller then 5.0 you can use custom query scopes
:::
View [friendly_id](https://github.com/norman/friendly_id) setup in action: [View Demo](https://main.avodemo.com/avo/resources/users)
Check out the code: [Code on GitHub](https://github.com/avo-hq/main.avodemo.com/blob/main/app/models/user.rb)
## Example with Prefixed IDs
Prefixed IDs involve adding a custom prefix to your IDs.
**Install [prefixed_ids](https://github.com/excid3/prefixed_ids) gem by adding this line to your application's Gemfile:**
```ruby
gem "prefixed_ids"
```
And then execute:
```bash
bundle install
```
**Basic Usage**
Add `has_prefix_id :my_prefix` to your models to autogenerate prefixed IDs:
```ruby{3}
# app/models/post.rb
class Post < ApplicationRecord
has_prefix_id :post
end
```
View [prefixed_ids](https://github.com/excid3/prefixed_ids) setup in action: [View Demo](https://main.avodemo.com/avo/resources/teams)
Check out the code: [Code on GitHub](https://github.com/avo-hq/main.avodemo.com/blob/main/app/models/team.rb)
## Example with Hashids
Hashid Rials is a gem that generates short, unique, and cryptographically secure IDs.
**Install [hashid-rails](https://github.com/jcypret/hashid-rails) gem by adding this line to your application's Gemfile:**
```ruby
gem "hashid-rails", "~> 1.0"
```
And then execute:
```bash
bundle install
```
**Include Hashid Rails in the ActiveRecord model you'd like to enable hashids:**
```ruby{3}
# app/models/post.rb
class Post < ApplicationRecord
include Hashid::Rails
end
```
View [hashid-rails](https://github.com/jcypret/hashid-rails) setup in action: [View Demo](https://main.avodemo.com/avo/resources/spouses)
Check out the code: [Code on GitHub](https://github.com/avo-hq/main.avodemo.com/blob/main/app/models/spouse.rb)
---
# Custom link field
When you want to add a custom link as a field on your resource that points to a related resource (and you don't want to use one of the available association fields) you can use the `Text` field like so.
```ruby
# with the format_using option
field :partner_home, as: :text, format_using: -> { link_to(value, value, target: "_blank") } do
avo.resources_partner_url record.partner.id
end
# with the as_html option
field :partner_home, as: :text, as_html: true do
if record.partner.present?
link_to record.partner.first_name, avo.resources_partner_url(record.partner.id)
end
end
```
---
# Display and Edit Join Table Fields in `has_many :through` Associations
A common scenario in Rails is using a `has_many :through` association to connect two models via a join model that contains extra fields. In Avo, you might want to display and edit attributes from the join table directly in your resource views (index, show, edit). This guide demonstrates how to achieve that.
## Example Models
```ruby
class Store < ApplicationRecord
has_one :location
has_many :patronships, class_name: :StorePatron
has_many :patrons, through: :patronships, class_name: :User, source: :user
end
class User < ApplicationRecord
has_many :patronships, class_name: :StorePatron
has_many :stores, through: :patronships
# Needed to make the field editable in Avo
attr_accessor :review
end
# Join Table
class StorePatron < ApplicationRecord
belongs_to :store
belongs_to :user
validates :review, presence: true
end
```
## Displaying Join Table Fields
You can display a join table attribute (like `review`) on the index or show view of the related resource by adding the field in your resource file and using `format_using` to fetch the correct value from the join table.
```ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
field :review,
format_using: -> {
# Fetch the review from the StorePatron join table
record.patronships.find_by(store_id: params[:via_record_id])&.review
}
end
end
```
This will show the `review` field from the join table when viewing users from the context of a store.
## Editing Join Table Fields
To allow editing, you need to:
1. Add a writer for the field to the model (e.g., `attr_accessor :review` or a custom setter).
2. Use the `update_using` option to update the join record.
```ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
if params[:resource_name] == 'stores' || params[:via_resource_class] == 'Avo::Resources::Store'
field :review,
update_using: -> {
# Update the review in the StorePatron join table
patronship = record.patronships.find_by(user_id: record.id.to_i)
patronship.update(review: value)
},
format_using: -> {
record.patronships.find_by(user_id: record.id.to_i)&.review
}
end
end
end
```
**Note:**
- The field will only render on the form if the model has a writer for it.
- You may need to adjust the logic for finding the join record depending on your association direction.
## Conditional Display Based on Parent Resource
You can use the `params` to control when the field is shown or editable. For example:
```ruby
# We use different params to detect the navigation context:
# - `resource_name` identifies when users access through the index table
# - `via_resource_class` identifies when users click to view or edit the resource
if params[:resource_name] == 'stores' || params[:via_resource_class] == 'Avo::Resources::Store'
# field
end
```
In this example, the `review` field is only visible/editable on User when the resource is accessed from the `Store` resource.
```ruby
# app/avo/resources/store.rb
class Avo::Resources::Store < Avo::BaseResource
def fields
field :patrons,
as: :has_many,
through: :patronships,
translation_key: "patrons",
attach_fields: -> {
# Add the review field to the attach form
field :review, as: :text
}
end
end
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
# Only show when accessed from the Store resource
if params[:resource_name] == 'stores' || params[:via_resource_class] == 'Avo::Resources::Store'
field :review,
format_using: -> {
# Fetch the review from the StorePatron join table
record.patronships.find_by(user_id: record.id.to_i)&.review
}
end
end
end
```
## Gotchas & Tips
- Computed fields (using a block) do not render on forms. Use `format_using` and provide a writer on the model.
- Avo checks for a writer method to decide if a field is editable.
- If the form fails to save, your join field may revert to its original value β consider validations and persistence carefully.
---
# Display scope record count
The `name` and `description` scope options can be callable values and receive the `resource`, `scope` and `query` objects.
The `query` object is the actual Active Record query (unscoped) that is made to fetch the records.
There is also possible to access the `scoped_query` method that will return the `query` after applying the `scope`.
You may use that to display a counter of how many records are there in that scope. Notice that it can impact page loading time when applying on large data tables.
### Example
```ruby{2-9}
class Avo::Scopes::Scheduled < Avo::Advanced::Scopes::BaseScope
self.name = -> {
sanitize(
"Scheduled " \
"" \
"#{scoped_query.count}" \
" "
)
}
self.description = -> { "All the scheduled jobs." }
self.scope = -> { query.finished.invert_where }
self.visible = -> { true }
end
```
In this example we made the `name` option a callable block and are returning the name of the scope and a `span` with the count of the records.
We are also using the `sanitize` method to return it as HTML.
In order to make the counter stand out, we're using some Tailwind CSS classes that we have available in Avo. If you're trying different classes and they are not applying, you should consider adding the Tailwind CSS integration.
:::warning
This approach will have some performance implications as it will run the `count` query on every page load.
:::
---
# Export to CSV action
Even if we don't have a dedicated export to CSV feature, you may create an action that will take all the selected records and export a CSV file for you.
Below you have an example which you can take and customize to your liking. It even give you the ability to use custom user-selected attributes.
```ruby
# app/avo/actions/export_csv.rb
class Avo::Actions::ExportCsv < Avo::BaseAction
self.name = "Export CSV"
self.confirmation = true
self.standalone = true
def fields
# Add more fields here for custom user-selected columns
field :id, as: :boolean
field :created_at, as: :boolean
end
def handle(records:, fields:, resource:, **args)
# uncomment if you want to download all the records if none was selected
# records = resource.model_class.all if records.blank?
return error "No record selected" if records.blank?
# uncomment to get all the models' attributes.
# attributes = get_attributes_from_record records.first
# uncomment to get some attributes
# attributes = get_some_attributes
attributes = get_attributes_from_fields fields
# uncomment to get all the models' attributes if none were selected
# attributes = get_attributes_from_record records.first if attributes.blank?
file = CSV.generate(headers: true) do |csv|
csv << attributes
records.each do |record|
csv << attributes.map do |attr|
record.send(attr)
end
end
end
download file, "#{resource.plural_name}.csv"
end
def get_attributes_from_record(record)
record.class.columns_hash.keys
end
def get_attributes_from_fields(fields)
fields.select { |key, value| value }.keys
end
def get_some_attributes
["id", "created_at"]
end
end
```
---
# Pretty JSON objects to the code field
It's common to have JSON objects stored in your database. So you might want to display them nicely on your resource page.
```ruby
field :meta, as: :code, language: 'javascript'
```
But that will be hard to read on one line like that. So we need to format it.
Luckily we can use `JSON.pretty_generate` for that and a computed field.
```ruby{3}
field :meta, as: :code, language: 'javascript' do
if record.meta.present?
JSON.pretty_generate(record.meta.as_json)
end
end
```
That's better! You'll notice that the field is missing on the `Edit` view. That's normal for a computed field to be hidden on `Edit`.
To fix that, we should add another one just for editing.
```ruby{1}
field :meta, as: :code, language: 'javascript', only_on: :edit
field :meta, as: :code, language: 'javascript' do
if record.meta.present?
JSON.pretty_generate(record.meta.as_json)
end
end
```
Now you have a beautifully formatted JSON object in a code editor.
## When you have more JSON fields
We can use a DRY solution that will help us to make our code cleaner and readable.
### 1. Concern
We will create a new concern in `app/models/concerns/avo_json_fields.rb` to be used in our models.
```ruby
module AvoJsonFields
extend ActiveSupport::Concern
class_methods do
def avo_json_fields(*fields)
fields.each do |field|
define_method "#{field}_json" do
JSON.pretty_generate(send(field).as_json)
end
define_method "#{field}_json=" do |value|
begin
send("#{field}=", JSON.parse(value))
rescue JSON::ParserError => e
# handle or ignore it
end
end
end
end
end
end
```
The `AvoJsonFields` prepares two methods for each field we provide. The first is for displaying, and the second is for storing the JSON object.
We can use it only on the models we need or include it in the `ApplicationRecord` for all.
```ruby{4}
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
include AvoJsonFields
end
```
### 2. Usage in models
When we have the concern in place, we can use it. For the example above, it could look like this:
```ruby{2}
class Page < ApplicationRecord
avo_json_fields :meta
end
```
That will create two methods for the `meta` field: `meta_json` and `meta_json=(value)`.
### 3. Usage in Avo resources
Now, we can use the `meta_json` field in our Avo resources. With the `name` option, we set the original name back.
```ruby
field :meta_json, as: :code, name: :meta, only_on: %i[show new edit], language: "javascript"
```
---
# Generating a custom component for a field
Each field in Avo has a component for each view that is responsible for rendering the field in that view.
Some fields, like the `textarea` field, don't have a component for certain views by default. For example, the `textarea` field doesn't have a component for the Index view. This guide shows you how to create one by using an existing field's component as a starting point.
## Using the Text field as a base
Instead of starting from scratch, it's easier to use the `text` field's index component as a base since it handles text content display well.
### Step 1: Eject the Text field component
In this step we're using the eject feature to generate the component files for the `text` field.
Run the following command to eject the text field's index component:
```bash
rails g avo:eject --field-components text --view index
```
This will generate the component files in your application.
### Step 2: Rename the component directory
Rename the generated directory from `text_field` to `textarea_field` to match the field type you're creating the component for.
```bash
mv app/components/avo/fields/text_field/ app/components/avo/fields/textarea_field/
```
### Step 3: Update the class reference
In the generated component file, update the class reference from:
```ruby
Avo::Fields::TextField::IndexComponent
```
to:
```ruby
Avo::Fields::TextareaField::IndexComponent
```
### Step 4: Customize the ERB template
Replace the ERB content with something appropriate for textarea content. For example, to truncate long text:
```erb
<%= index_field_wrapper(**field_wrapper_args) do %>
<%= @field.value.truncate(60) %>
<% end %>
```
### Step 5: Enable index visibility
By default, `textarea` fields are hidden on the index view. You need to explicitly show them by adding the `show_on: :index` option to your textarea fields:
```ruby
field :body, as: :textarea, show_on: :index
```
## Global configuration for all textarea fields
If you want all `textarea` fields in your application to show on the index view by default, you can extend the base resource and override the `field` method:
```ruby
# app/avo/base_resource.rb
module Avo
class BaseResource < Avo::Resources::Base
def field(id, **args, &block)
if args[:as] == :textarea
args[:show_on] = :index
end
super(id, **args, &block)
end
end
end
```
For more information about extending the base resource, see the Extending Avo::BaseResource documentation.
## Related documentation
- Field components - Learn how to eject and override existing field components
- Ejecting views - Learn how to eject and override existing views
- Extending Avo::BaseResource
- Views - Understanding different view types in Avo
---
# Handle 404 responses and redirections in Avo
When Rails raises an `ActiveRecord::RecordNotFound` exception, by default, Avo will let Rails do its thing and send the user on the default 404 page.
There might be cases where you want to handle 404 responses in Avo. You can do that by rescuing the exception and redirecting the user to a custom page.
:::info
Make sure your `Avo::ApplicationController` is ejected by running this command:
```bash
rails generate avo:eject --controller application_controller
```
:::
```ruby
class Avo::ApplicationController
rescue_from ActiveRecord::RecordNotFound, with: -> { redirect_to Avo.configuration.root_path, notice: t("avo.no_item_found") }
end
```
---
# Hide field labels
One common use case for the `file`, `files`, and `external_image` fields is to display the logo for a record. You might want to do that but in a more "un-fieldy" way, so it doesn't look like a field with a label on top.
You can hide that label using CSS in your custom asset pipeline, or in a `_footer` partial.
Avo is littered with great `data` selectors so you can pick and choose any element you'd like. If it doesn't have it, we'll add it.
Here's an example on how to remove the label on an `external_image` field for the `Team` resource (try it [here](https://main.avodemo.com/avo/resources/teams/4)).
```css
[data-resource-name="TeamResource"] [data-field-type="external_image"][data-field-id="logo"] [data-slot="label"]{
display: none;
}
```
---
# How to Use [Phlex](https://www.phlex.fun/) Components in Avo
Avo uses [ViewComponent](https://viewcomponent.org/) to render fields, resources, and other parts of the UI. However, that doesn't mean you can't use [Phlex](https://www.phlex.fun/) components in your Avo views.
The initialization process between the two is quite similar allowing a smooth transition between them. You just need to configure the component you want to use for a given field or resource on a specific view, and Avo will take care of the rest.
This guide walks you through how to use [Phlex](https://www.phlex.fun/) components inside your Avo views.
> _Note: This guide assumes you already have Phlex installed in your app._
## Step 1: Create a [Phlex](https://www.phlex.fun/) component
Let's start with a simple [Phlex](https://www.phlex.fun/) component for a field. This component uses the same Tailwind CSS classes as the default Avo field component, and includes an additional message about the field.
You can make your components as simple or as complex as you'd like, this is just an example.
:::warning
All Tailwind CSS classes used in this guide are already included in Avo's design system and available in its pre-purged assets. If you plan to customize the appearance beyond what's shown here, consider setting up the TailwindCSS integration.
:::
```ruby
# app/components/phlex_component.rb
class PhlexComponent < Phlex::HTML
def initialize(field:, **)
@field = field
end
def view_template
div class: "flex items-center px-6 py-4" do
span class: "font-semibold text-gray-500 text-sm w-64 uppercase" do
@field.name
end
span class: "text-gray-900" do
@field.value
end
span class: "text-gray-300 mx-3" do
"|"
end
span class: "mr-1" do
"βΉοΈ"
end
span class: "text-sm text-gray-500 italic" do
"This is a unique course link. Share it with enrolled users."
end
end
end
end
```
---
## Step 2: Use the component in your field declaration
With the `components` option, you can specify the component to be used for the `show` view of a field.
```ruby{10-12}
# app/avo/resources/course_link.rb
class Avo::Resources::CourseLink < Avo::BaseResource
self.title = :link
self.model_class = ::Course::Link
def fields
field :id, as: :id
field :link,
as: :text,
components: {
show_component: PhlexComponent
}
field :course, as: :belongs_to, searchable: true
end
end
```
:::tip
While this example uses a field, the same pattern applies to resources. You can use the `components` option to customize the component for the `index`, `show`, `edit`, and `new` views.
:::
## Conclusion
Even though Avo relies on [ViewComponent](https://viewcomponent.org/) under the hood, you're free to use [Phlex](https://www.phlex.fun/) components in your Avo views.
This guide covered a basic example, but [Phlex](https://www.phlex.fun/) is capable of much more. Check out the [official Phlex documentation](https://www.phlex.fun/introduction/) to learn how to build more advanced components.
If you have questions, suggestions, or feedback, join the conversation in the [Feedback: Using Phlex Components in Avo Views](https://github.com/avo-hq/avo/discussions/3860).
---
# Manage information-heavy resources
This has been sent in by our friends at [Wyndy.com](https://wyndy.com). I'm just going to paste David's message because it says it all.
David π
Hey y'all - we've got a very information heavy app where there are pretty distinct differences between the data we display on index, show, & form views as well as how it's ordered.
We created a concern for our resources to make organizing this a bit easier, would love y'all's thoughts/feedback as to whether this could be a valuable feature! Example gist: [https://gist.github.com/davidlormor/d1d7e32a3568f6a9b3540669e7f601dc](https://gist.github.com/davidlormor/d1d7e32a3568f6a9b3540669e7f601dc)
We went with a concern because I ran into inheritance issues trying to create a `BaseResource` class (issues with Avo's `model_class` expectations) and monkey-patching `Avo::BaseResource` seemed to cause issues with Rails' autoloading/zeitwork?
```ruby
class ExampleResource < Avo::BaseResource
include ResourceExtensions
field :id, as: :id
field :name, as: :text
index do
field :some_field, as: :text
field :some_index_field, as: :text, sortable: true
end
show do
field :some_show_field, as: :markdown
field :some_field, as: :text
end
create do
field :some_create_field, as: :number
end
edit do
field :some_create_field, as: :number, readonly: true
field :some_field
field :some_editable_field, as: :text
end
end
```
```ruby
require "active_support/concern"
module ResourceExtensions
extend ActiveSupport::Concern
class_methods do
def index(&block)
with_options only_on: :index, &block
end
def show(&block)
with_options only_on: :show, &block
end
def create(&block)
with_options only_on: :new, &block
end
def edit(&block)
with_options only_on: :edit, &block
end
end
end
```
---
# Multi-language URLs
Implementing multi-language URLs is a common use-case. Using a route scope block in Avo allows you to seamlessly adapt your application to support multiple languages, enhancing the user experience. This recipe will guide you through the steps to configure a locale scope, ensuring your application dynamically sets and respects the user's preferred language. Let's dive in!
## 1. Mount Avo within a `:locale` scope
Using a locale scope is an effective way to set the locale for your users.
```ruby{3-5}
# config/routes.rb
Rails.application.routes.draw do
scope ":locale" do
mount_avo
end
end
```
## 2. Apply the `locale` Scope
To properly handle localization within Avo, you'll need to ensure the `locale` parameter is respected throughout the request which we'll do by overriding the `set_avo_locale` method in your `Avo::ApplicationController` as follows:
:::info
If you don't have the `app/controllers/avo/application_controller.rb` file present in your app, you can eject it using this command:
```bash
rails generate avo:eject --controller application_controller
```
:::
```ruby{4-6}
# app/controllers/avo/application_controller.rb
module Avo
class ApplicationController < BaseApplicationController
def set_avo_locale(&action)
I18n.with_locale(params[:locale], &action)
end
end
end
```
This implementation uses `I18n.with_locale` to set the desired locale for the duration of the request, ensuring consistent localization behavior across Avo's interface and that it won't impact the other non-Avo parts of your app.
---
# Multilingual content
This is not an official feature yet, but until we add it with all the bells and whistles, you can use this guide to monkey-patch it into your app.
We pushed some code to take in the `set_locale` param and set the `I18n.locale` and `I18n.default_locale` so all subsequent requests will use that locale. **That will change the locale for your whole app. Even to the front office**.
If you don't want to change the locale for the whole app, you can use `force_locale`, which will change the locale for that request only. It will also append `force_locale` to all your links going forward.
```ruby
def set_default_locale
I18n.locale = params[:set_locale] || I18n.default_locale
I18n.default_locale = I18n.locale
end
# Temporary set the locale
def set_force_locale
if params[:force_locale].present?
initial_locale = I18n.locale.to_s.dup
I18n.locale = params[:force_locale]
yield
I18n.locale = initial_locale
else
yield
end
end
```
## Install the mobility gem
Follow the install instructions [here](https://github.com/shioyama/mobility#installation). A brief introduction below (but follow their guide for best results)
- add the gem to your `Gemfile` `gem 'mobility', '~> 1.2.5'`
- `bundle install`
- install mobility `rails generate mobility:install`
- update the backend (like in the guide) `backend :key_value, type: :string`
- add mobility to your model `extend Mobility`
- add translatable field `translates :name`
- π that's it. The content should be translatable now.
## Add the language switcher
**Before v 2.3.0**
First, you need to eject the `_profile_dropdown` partial using this command `bin/rails generate avo:eject :profile_dropdown`. In that partial, add the languages you need to support like so:
```erb
<% destroy_user_session_path = "destroy_#{Avo.configuration.current_user_resource_name}_session_path".to_sym %>
```
```erb
<% destroy_user_session_path = "destroy_#{Avo.configuration.current_user_resource_name}_session_path".to_sym %>
data-controller="toggle-panel" <% end %>>
<% if _current_user.respond_to?(:avatar) && _current_user.avatar.present? %>
<%= image_tag _current_user.avatar, class: "h-12 rounded-full border-4 border-white mr-1" %>
<% end %>
<% if _current_user.respond_to?(:name) && _current_user.name.present? %>
<%= _current_user.name %>
<% elsif _current_user.respond_to?(:email) && _current_user.email.present? %>
<%= _current_user.email %>
<% else %>
Avo user
<% end %>
<% if main_app.respond_to?(destroy_user_session_path) %>
<%= svg 'chevron-down', class: "ml-1 h-4" %>
<% end %>
<% if main_app.respond_to?(destroy_user_session_path) %>
<% classes = "appearance-none bg-white text-left cursor-pointer text-green-600 font-semibold hover:text-white hover:bg-green-500 block px-4 py-1 w-full" %>
<% if I18n.locale == :en %>
<%= link_to "Switch to Portuguese", { set_locale: 'pt-BR' }, class: classes %>
<% else %>
<%= link_to "Switch to English", { set_locale: 'en' }, class: classes %>
<% end %>
<%= button_to t('avo.sign_out'), main_app.send(:destroy_user_session_path), method: :delete, form: { "data-turbo" => "false" }, class: classes %>
<% end %>
```
Feel free to customize the dropdown as much as you need it to and add as many locales as you need.
**After v2.3.0**
Use the `profile_menu` to add the language-switching links.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.profile_menu = -> {
link "Switch to Portuguese", path: "?set_locale=pt-BR"
link "Switch to English", path: "?set_locale=en"
}
end
```
**After v2.10**
The `set_locale` param will change the locale for the entire website (for you and your customers). If you need to change it just for the current visit, use `force_locale`. That will switch the locale for that request only, not for your customers. It will also add the `force_locale` param to each link as we advance, making it easy to update all your multilingual content.
**After v2.11**
A change was pushed to consider the `locale` from the initializer. That will change the locale for Avo requests.
```ruby{2}
Avo.configure do |config|
config.locale = :en # default is nil
end
```
## Workflow
You will now be able to edit the attributes you marked as translatable (eg: `name`) in the locale you are in (default is `en`). Next, you can go to the navbar on the top and switch to a new locale. The switch will then allow you to edit the record in that locale and so on.
## Support
This is the first iteration of multilingual content. It's obvious that this could be done in a better way, and we'll add that better way in the future, but until then, you can use this method to edit your multilingual content.
Thanks!
---
# Use route-level multitenancy
:::tip
We published a new multitenancy guide.
:::
Multitenancy is not a far-fetched concept, and you might need it when you reach a certain level with your app. Avo is ready to handle that.
This guide will show you **one way** of achieving that, but if can be changed if you have different needs.
## Prepare the Current model
We will use Rails' [`Current`](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html) model to hold the account.
```ruby{3}
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :account
end
```
## Add middleware to catch the account param
We're trying to fetch the account number from the `params` and see if we have an account with that ID in this middleware. If so, store it in the `Current.account` model, where we can use it throughout the app.
```ruby{18,21,23,25}
## Multitenant Account Middleware
#
# Included in the Rails engine if enabled.
#
# Used for setting the Account by the first ID in the URL like Basecamp 3.
# This means we don't have to include the Account ID in every URL helper.
# From JumpstartRails AccountMiddleware
class AccountMiddleware
def initialize(app)
@app = app
end
# http://example.com/12345/projects
def call(env)
request = ActionDispatch::Request.new env
# Fetch the account id from the path
_, account_id, request_path = request.path.split("/", 3)
# Check if the id is a number
if /\d+/.match?(account_id)
# See if that account is present in the database.
if (account = Account.find_by(id: account_id))
# If the account is present, set the Current.account to that
Current.account = account
else
# If not, redirect to the root path
return [302, {"Location" => "/"}, []]
end
request.script_name = "/#{account_id}"
request.path_info = "/#{request_path}"
end
@app.call(request.env)
end
end
```
## Update the custom tools routes
By default, when generating custom tools, we're adding them to the parent app's routes. Because we're declaring them there, the link helpers don't hold the account id in the params.
```ruby{2-4}
Rails.application.routes.draw do
scope :avo do
get "custom_page", to: "avo/tools#custom_page"
end
devise_for :users
# Your routes
authenticate :user, -> user { user.admin? } do
mount_avo
end
end
```
To fix that, we need to move them as if they were added to Avo's routes.
```ruby{13-18}
# config/routes.rb
Rails.application.routes.draw do
devise_for :users
# Your routes
authenticate :user, -> user { user.admin? } do
mount_avo
end
end
# Move Avo custom tools routes to Avo engine
if defined? ::Avo
Avo::Engine.routes.draw do
# make sure you don't add the `avo/` prefix to the controller below
get 'custom_page', to: "tools#custom_page"
end
end
```
```ruby
# app/controllers/avo/tools_controller.rb
class Avo::ToolsController < Avo::ApplicationController
def custom_page
@page_title = "Your custom page"
add_breadcrumb "Your custom page"
end
end
```
## Retrieve and use the account
Throughout your app you can use `Current.account` or if you add it to Avo's `context` object and use it from there.
```ruby{8}
# config/initializers/avo.rb
Avo.configure do |config|
config.set_context do
{
foo: 'bar',
user: current_user,
params: request.params,
account: Current.account
}
end
end
```
Check out [this PR](https://github.com/avo-hq/avodemo/pull/4) for how to update an app to support multitenancy.
---
# Nested records when creating
A lot of you asked for the ability to create nested `has_many` records on the view. Although it's fairly "easy" to implement using `accepts_nested_attributes_for` for simple cases, it's a different story to extract it, make it available, and cover most edge cases for everyone.
That's why Avo and no other similar gems dont't offer this feature as a first-party feature.
But, that doesn't mean that it's impossible to implement it yourself. It's actually similar to how you'd implement it for your own app.
We prepared this scenario where a `Fish` model `has_many` `Review`s. I know, it's not the `Slider` `has_many` `Item`s example, but you'll get the point.
## Full set of changes
The full code is available in Avo's [dummy app](https://github.com/avo-hq/avo/tree/main/spec/dummy) and the changes in [this PR](https://github.com/avo-hq/avo/pull/1472).
## Guide to add it to your app
You can add this functionality using these steps.
### 1. Add `accepts_nested_attributes_for` on your parent model
```ruby{4}
class Fish < ApplicationRecord
has_many :reviews, as: :reviewable
accepts_nested_attributes_for :reviews
end
```
:::warning
Ensure you have the `has_many` association on the parent model.
:::
### 2. Add a JS helper package that dynamically adds more review forms
`yarn add stimulus-rails-nested-form`
In your JS file register the controller.
```js{3,6}
// Probably app/javascript/avo.custom.js
import { Application } from '@hotwired/stimulus'
import NestedForm from 'stimulus-rails-nested-form'
const application = Application.start()
application.register('nested-form', NestedForm)
```
:::info
Use this guide to add custom JavaScript to your Avo app.
:::
### 3. Generate a new resource tool
`bin/rails generate avo:resource_tool nested_fish_reviews`
This will generate two files. The `NestedFishReviews` ruby file you'll register on the `Avo::Resources::Fish` file and we'll edit the template to contain our fields.
### 4. Register the tool on the resource
We'll display it only on the view.
```ruby{7}
class Avo::Resources::Fish < Avo::BaseResource
# other fields actions, filters and more
def fields
field :reviews, as: :has_many
tool Avo::ResourceTools::NestedFishReviews, only_on: :new
end
end
```
### 5. Create a partial for one new review
This partial will have the fields for one new review which we'll add more on the page.
```erb
<%= render Avo::PanelComponent.new do |c| %>
<% c.with_body do %>
<%= avo_edit_field :body, as: :trix, form: f, help: "What should the review say", required: true %>
<%= avo_edit_field :user, as: :belongs_to, form: f, help: "Who created the review", required: true %>
<% end %>
<% end %>
```
### 6. Update the resource tool partial
It's time to put it all together. In the resource tool partial we're wrapping the whole thing with the `nested-form` controller div, creating a new `form` helper to reference the nested fields with `form.fields_for` and wrapping the "new" template so we can use replicate it using the `nested-form` package.
In the footer we'll also add the button that will add new reviews on the page.
```erb
<%= content_tag :div,data: { controller: 'nested-form', nested_form_wrapper_selector_value: '.nested-form-wrapper' } do %>
<%= render Avo::PanelComponent.new(name: "Reviews", description: "Create some reviews for this fish") do |c| %>
<% c.with_bare_content do %>
<% if form.present? %>
<%= form.fields_for :reviews, Review.new, multiple: true, child_index: 'NEW_RECORD' do |todo_fields| %>
<%= render "avo/partials/fish_review", f: todo_fields %>
<% end %>
<%= form.fields_for :reviews, Review.new, multiple: true do |todo_fields| %>
<%= render "avo/partials/fish_review", f: todo_fields %>
<% end %>
<% end %>
<% end %>
<% c.with_footer_tools do %>
<%= a_link 'javascript:void(0);', icon: 'plus', color: :primary, style: :outline, data: {action: "click->nested-form#add"} do %>
Add another review
<% end %>
<% end %>
<% end %>
<% end %>
```
### 7. Permit the new nested params
There's one more step we need to do and that's to whitelist the new `reviews_attributes` params to be passed to the model.
```ruby{2}
class Avo::Resources::Fish < Avo::BaseResource
self.extra_params = [reviews_attributes: [:body, :user_id]]
# other fields actions, filters and more
def fields
field :reviews, as: :has_many
tool Avo::ResourceTools::NestedFishReviews, only_on: :new
end
end
```
## Conclusion
There you have it!
Apart from the resource tool and the `extra_params` attribute, we wrote regular Rails code that we would have to write to get this functionality in our app.
---
# Authentication using Rails' scaffold
In essence, the [authentication scaffold](https://github.com/rails/rails/pull/52328) that Rails 8 comes with is custom authentication so we need to do a few things to ensure it's working properly with Avo.
## 1. Set the current user
The scaffold uses the `Current.user` thread-safe global to hold the current authenticated user so we need to tell Avo how to fetch them.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
# other pieces of configuration
# tell Avo how to find the current authenticated user.
config.current_user_method do
Current.user
end
end
```
## 2. Set the sign out link
The scaffold uses the `SessionsController` to sign out the user so the link should be `sessions_path`. We need to add that to Avo as well.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
# other pieces of configuration
# tell Avo how to sign out the authenticated user.
config.sign_out_path_name = :session_path
end
```
## 3. Ensure only authenticated users are allowed on Avo
Now, here comes the part which might seem unfamiliar but it's actually pretty standard.
The scaffold adds the `Authentication` concern to your `ApplicationController` which is great. We will add it to Avo's `ApplicationController` and also add the `before_action`, but instead of just appending it wil will prepend it so we can ensure it will be fired as soon as possible in the request lifecycle.
Since `require_authentication` runs in the Avo context, it's necessary to delegate the `new_session_path` to the `main_app` to ensure proper routing.
```ruby{4,5,8}
# app/controllers/avo/application_controller.rb
module Avo
class ApplicationController < BaseApplicationController
include Authentication
delegate :new_session_path, to: :main_app
# we are prepending the action to ensure it will be fired very early on in the request lifecycle
prepend_before_action :require_authentication
end
end
```
:::info
If you don't have the `app/controllers/avo/application_controller.rb` file present in your app, you can eject it using this command:
```bash
rails generate avo:eject --controller application_controller
```
:::
---
# REST API integration
Recipe [contributed](https://github.com/avo-hq/avo/issues/656) by [santhanakarthikeyan](https://github.com/santhanakarthikeyan).
I've built a page using AVO + REST API without using the ActiveRecord model. I was able to build an index page + associated has_many index page. It would be great if we could offer this as a feature, I guess, Avo would be the only admin framework that can offer this feature in case we take it forward :+1:
I've made it work along with Pagination, Filter and even search are easily doable.
`app/avo/filters/grace_period.rb`
```ruby
class GracePeriod < Avo::Filters::BooleanFilter
self.name = 'Grace period'
def apply(_request, query, value)
query.where(value)
end
def options
{
grace_period: 'Within graceperiod'
}
end
end
```
`app/avo/resources/aging_order_resource.rb`
```ruby
class AgingOrderResource < Avo::BaseResource
self.title = :id
self.includes = []
field :id, as: :text
field :folio_number, as: :text
field :order_submitted_at, as: :date_time, timezone: 'Chennai', format: '%B %d, %Y %H:%M %Z'
field :amc_name, as: :text
field :scheme, as: :text
field :primary_investor_id, as: :text
field :order_type, as: :text
field :systematic, as: :boolean
field :order_reference, as: :text
field :amount, as: :text
field :units, as: :text
field :age, as: :text
filter GracePeriod
end
```
`app/controllers/avo/aging_orders_controller.rb`
```ruby
module Avo
class AgingOrdersController < Avo::ResourcesController
def pagy_get_items(collection, _pagy)
collection.all.items
end
def pagy_get_vars(collection, vars)
collection.where(page: page, size: per_page)
vars[:count] = collection.all.count
vars[:page] = params[:page]
vars
end
private
def per_page
params[:per_page] || Avo.configuration.per_page
end
def page
params[:page]
end
end
end
```
`app/models/aging_order.rb`
```ruby
class AgingOrder
include ActiveModel::Model
include ActiveModel::Conversion
include ActiveModel::Validations
extend ActiveModel::Naming
attr_accessor :id, :investment_date, :folio_number, :order_submitted_at,
:amc_name, :scheme, :primary_investor_id, :order_type, :systematic,
:order_reference, :amount, :units, :age
class << self
def column_names
%i[id investment_date folio_number order_submitted_at amc_name
scheme primary_investor_id order_type systematic
order_reference amount units age]
end
def base_class
AgingOrder
end
def root_key
'data'
end
def count_key
'total_elements'
end
def all(query)
response = HTTParty.get(ENV['AGING_URL'], query: query)
JSON.parse(response.body)
end
end
def persisted?
id.present?
end
end
```
`app/models/lazy_loader.rb`
```ruby
class LazyLoader
def initialize(klass)
@offset, @limit = nil
@params = {}
@items = []
@count = 0
@klass = klass
end
def where(query)
@params = @params.merge(query)
self
end
def items
all
@items
end
def count(_attr = nil)
all
@count
end
def offset(value)
@offset = value
self
end
def limit(value)
@limit = value
items[@offset, @limit]
end
def all
api_response
self
end
def to_sql
""
end
private
def api_response
@api_response ||= begin
json = @klass.all(@params)
json.fetch(@klass.root_key, []).map do |obj|
@items << @klass.new(obj)
end
@count = json.fetch(@klass.count_key, @items.size)
end
end
end
```
`app/policies/aging_order_policy.rb`
```ruby
class AgingOrderPolicy < ApplicationPolicy
class Scope < Scope
def resolve
LazyLoader.new(scope)
end
end
def index?
user.admin?
end
def show?
false
end
end
```
`config/initializers/array.rb`
```ruby
class Array
def limit(upto)
take(upto)
end
end
```
---
# Integration with rolify
_Recipe contributed by [Paul](https://github.com/FLX-0x00) after discussing it [here](https://github.com/avo-hq/avo/issues/1568)._
It is possible to implement the [`rolify`](https://github.com/RolifyCommunity/rolify) gem in conjunction with `pundit` in an Avo using basic functionality.
Following the next steps allows for easy management of roles within the admin panel, which can be used to control access to different parts of the application based on user roles. By assigning specific permissions to each user role, Avo users can ensure that their admin panels remain secure and accessible only to authorised users.
:::warning
You must manually require `rolify` in your `Gemfile`.
:::
```ruby
gem "rolify"
```
**If this is a new app you need to do some initial steps, create the role model and specify which models should be handled by rolify**
:::info
Check out the [rolify documentation](https://github.com/RolifyCommunity/rolify) for reference.
:::
We assume that your model for managing users is called `Account` (default when using `rodauth`) and your role model is called `Role` (default when using `rolify`).
```ruby
class Account < ApplicationRecord
rolify
# ...
end
```
A `Role` connects to an `Account` through `has_and_belongs_to_many` while an `Account` connects to `Role` through `has_many` (not directly used in the model because the `rolify` statement manage this). Although rolify has its own functions for adding and deleting roles, normal rails operations can also be used to manage the roles. To implement this in avo, the appropriate resources need to be created.
*Perhaps the creation of the account resource is not necessary, as it has already been done in previous steps or has been created automatically by the avo generator through a scaffold/model. So we assume this step is already done.*
```zsh
bin/rails generate avo:resource role
```
After this step the `roles` should now accessible via the avo interface. The final modification should be done in the corresponding `Account` resource file.
```ruby
class AccountResource < Avo::BaseResource
# ...
field :assigned_roles, as: :tags, hide_on: :forms do
record.roles.map {|role|role.name}
end
# Only show roles that have not already been assigned to the object, because Avo does not use the add_role method, so it is possible to assign a role twice
field :roles, as: :has_many, attach_scope: -> { query.where.not(id: parent.roles.pluck(:id)) }
# ...
end
```
Example of RoleResource file:
```ruby
class RoleResource < Avo::BaseResource
self.title = :name
self.includes = []
field :name, as: :text
field :accounts, as: :has_and_belongs_to_many
end
```
The roles of an account can now be easily assigned and removed using avo. The currently assigned roles are displayed in the index and show view using the virtual `assigned_roles' field.
---
# Run Avo on the root path
You might want to run avo on the root path on your app.
We've seen plenty of users use this strategy.
This is as simple as changing the `root_path` from the `avo.rb` initializer to `/`.
```ruby{5}
Avo.configure do |config|
# other pieces of configuration
# Change the path to `/` to make it the root path
config.root_path = '/'
end
```
I used these commands to create a new repo and change the path.
```bash
rails new avo-root-path
cd avo-root-path
bin/rails app:template LOCATION='https://avohq.io/app-template'
sed -i '' "s|config.root_path = '/avo'|config.root_path = '/'|" config/initializers/avo.rb
```
---
# How to safely override the resource views without maintaining core components
Sometimes it's the small things in a UI that make a big impact. One of those things is being able to show a helpful message at the top of an index view page. This is typically where users land to see lists of posts, products, orders, or anything else. You might want to point out something important, offer quick guidance, or simply highlight a recent change.
:::info
What makes this guide particularly valuable is that it demonstrates how to safely override and customize the resource index component without having to maintain the original index component on each version update. While we'll be focusing on the index component in this guide, this technique can be applied to any resource view component in Avo. This approach lets you add custom functionality while still benefiting from Avo's updates to the core components, ensuring your customizations remain compatible across upgrades.
:::
That's where this guide comes in. I'll walk you through how to inject a custom message at the top of the index view. We'll do this by creating a new component that extends the one Avo already uses to render index pages, setting it as the default for specific resources (or all of them), and customizing the view to display our message cleanly above the list.
Let's jump in.
## Create a new view component
Start by generating a new view component that inherits from Avo's index view:
```sh
rails generate component Avo::Views::ResourceCustomIndex --parent=Avo::Views::ResourceIndexComponent
```
This will generate three files:
```ruby
# app/components/avo/views/resource_custom_index_component.rb
# frozen_string_literal: true
class Avo::Views::ResourceCustomIndexComponent < Avo::Views::ResourceIndex
end
```
```html
Add Avo::Views::ResourceCustomIndexComponent template here
```
```rb
# test/components/avo/views/resource_custom_index_component_test.rb
# frozen_string_literal: true
require "test_helper"
class Avo::Views::ResourceCustomIndexeComponentTest < ViewComponent::TestCase
def test_component_renders_something_useful
# assert_equal(
# %(Hello, components! ),
# render_inline(Avo::Views::ResourceCustomIndexeComponent.new(message: "Hello, components!")).css("span").to_html
# )
end
end
```
:::tip
You can delete the generated test file `test/components/avo/views/resource_custom_index_component_test.rb` since we won't cover testing in this guide.
:::
## Use the custom component in a resource
Let's apply the new component to a specific resource. I'll use the `Movie` resource as an example.
Update the resource file (`Avo::Resources::Movie`) to use the new component via the `self.components` configuration:
```ruby
# app/avo/resources/movie.rb
class Avo::Resources::Movie < Avo::Resources::ArrayResource
self.components = { # [!code ++]
"Avo::Views::ResourceIndexComponent": Avo::Views::ResourceCustomIndexComponent # [!code ++]
} # [!code ++]
# ...
end
```
Now when you visit the Movies resource page, it will render the custom component, currently just showing the placeholder text.
## Render the parent view and add your message
Next, let's modify the component so it wraps the original Avo index component and adds a message on top.
Avo will now call this custom component first, let's update the Ruby component file to store all keyword arguments, and use those to render the parent component.
```ruby
# app/components/avo/views/resource_custom_index_component.rb
# frozen_string_literal: true
class Avo::Views::ResourceCustomIndexComponent < Avo::Views::ResourceIndex
def initialize(**kwargs) # [!code ++]
@kwargs = kwargs # [!code ++]
end # [!code ++]
end
```
Update the ERB template to render a message above the original component:
:::warning
All Tailwind CSS classes used in this guide are already part of Avo's design system and included in its pre-purged assets. If you plan to customize the appearance of the message component beyond what's shown here, you may need to set up the TailwindCSS integration.
:::
```html
Add Avo::Views::ResourceCustomIndexComponent template here
MovieFest 2025 β’ Discover what\'s trending this season in cinema πΏ
<%= render Avo::Views::ResourceIndexComponent.new(**@kwargs) %>
```
Now when you visit the Movies resource page, it will render the custom component that shows the original component and your custom message on top. πππ
## Apply this component to all the resources
You can apply the new component to each resource individually by setting `self.components`, but there's a more efficient approach. Since all your resources inherit from `Avo::BaseResource`, we can centralize this configuration by extending that base class.
To do this, override the base resource class by creating or modifying `app/avo/base_resource.rb`:
```rb
# app/avo/base_resource.rb
module Avo
class BaseResource < Avo::Resources::Base
self.components = { # [!code ++]
"Avo::Views::ResourceIndexComponent": Avo::Views::ResourceCustomIndexComponent # [!code ++]
} # [!code ++]
end
end
```
Now you can remove this configuration from the Movie resource:
```ruby
# app/avo/resources/movie.rb
class Avo::Resources::Movie < Avo::Resources::ArrayResource
self.components = { # [!code --]
"Avo::Views::ResourceIndexComponent": Avo::Views::ResourceCustomIndexComponent # [!code --]
} # [!code --]
# ...
end
```
With this change in place, every resource will automatically use the custom index component, no extra configuration needed. However, that raises a practical question: what if some resources should have a message, and others shouldn't?
Let's make the component more flexible by introducing a lightweight DSL extension.
## Make the message configurable via a resource method
To turn our static message into something dynamic and optional we'll fetch the message from a method on each resource. If a resource defines the `index_message` method, the component will render it. If not, it wonβt show anything.
Letβs update the Ruby component to support this:
```ruby
# app/components/avo/views/resource_custom_index_component.rb
# frozen_string_literal: true
class Avo::Views::ResourceCustomIndexComponent < Avo::Views::ResourceIndex
def initialize(**kwargs)
@kwargs = kwargs
@index_message = kwargs[:resource].try(:index_message) # [!code ++]
end
end
```
Now tweak the view to conditionally render the message:
```html
<% if @index_message.present? %>
MovieFest 2025 β’ Discover what\'s trending this season in cinema πΏ
<%= @index_message %>
<% end %>
<%= render Avo::Views::ResourceIndexComponent.new(**@kwargs) %>
```
To use this, just add an `index_message` method to any resource:
```ruby
# app/avo/resources/movie.rb
class Avo::Resources::Movie < Avo::Resources::ArrayResource
def index_message # [!code ++]
'MovieFest 2025 β’ Discover what\'s trending this season in cinema πΏ'.html_safe # [!code ++]
end # [!code ++]
# ...
end
```
---
### Wrapping up
Adding contextual messages to index pages can go a long way in making your internal tool more helpful. With this approach, you've learned how to:
- Extend Avo's default index view component
- Add custom UI above the resource index table
- Apply the enhancement globally across all resources
- Keep it flexible using a simple per-resource DSL
This solution is modular, declarative, and easy to maintain. You can now provide dynamic guidance to your users where it makes the most sense.
The beauty of this approach is that it safely overrides and customizes the resource index component without requiring you to maintain the original index component on each version update. While we've focused on adding a message at the top, this pattern opens horizons for extending the index component in any direction, whether adding elements at the bottom, on the sides, or anywhere else your application needs. You get the flexibility of customization while continuing to benefit from Avo's ongoing improvements to the core components.
---
# Display counter indicator on tabs switcher
When a tab contains an association field you may want to show some counter indicator about how many records are on that particular tab. You can include that information inside tab's name.
```ruby{7,10,16-23}
class Avo::Resources::User < Avo::BaseResource
def fields
main_panel do
end
tabs do
tab name_with_counter("Teams", record&.teams&.size) do
field :teams, as: :has_and_belongs_to_many
end
tab name_with_counter("People", record&.people&.size) do
field :people, as: :has_many
end
end
end
def name_with_counter(name, counter)
view_context.sanitize(
"#{name} " \
"" \
"#{counter}" \
" "
)
end
end
```
We are also using the `sanitize` method to return it as HTML.
In order to make the counter stand out, we're using some Tailwind CSS classes that we have available in Avo. If you're trying different classes and they are not applying, you should consider adding the Tailwind CSS integration.
:::warning
This approach will have some performance implications as it will run the `count` query on every page load.
:::
---
# Use markdown for help attributes
:::info User contribution
Recipe [contributed](https://github.com/avo-hq/avo/issues/1390#issuecomment-1302553590) by [dhnaranjo](https://github.com/dhnaranjo).
:::
Desmond needed a way to write markdown in the help field and built an HTML to Markdown compiler.
```ruby
module MarkdownHelpText
class Renderer < Redcarpet::Render::HTML
def header(text, level)
case level
when 1 then %(#{text} )
when 2 then %(#{text})
else
%(#{text} )
end
end
def paragraph(text)
%( #{text}
)
end
def block_code(code, language)
<<~HTML
#{code.chomp}
HTML
end
def codespan(code)
%(#{code})
end
def list(contents, list_type)
list_style = case list_type
when "ul" then "list-disc"
when "ol" then "list-decimal"
else "list-none"
end
%(<#{list_type} class="ml-8 mb-2 #{list_style}">#{contents}#{list_type}>)
end
end
def markdown_help(content, renderer: Renderer)
markdown = Redcarpet::Markdown.new(
renderer.new,
filter_html: false,
escape_html: false,
autolink: true,
fenced_code_blocks: true
).render(content)
%()
end
end
```
```ruby
field :description_copy, as: :markdown,
help: markdown_help(<<~MARKDOWN
# Dog
## Cat
### bird
paragraph about hats **bold hat**
~~~
class Ham
def wow
puts "wow"
end
end
~~~
`code thinger`
- one
- two
- three
MARKDOWN
)
```
---
# Use own helpers in Resource files
## TL;DR
Run `rails app:template LOCATION='https://railsbytes.com/script/V2Gsb9'`
## Details
A common pattern is to have some helpers defined in your app to manipulate your data. You might need those helpers in your `Resource` files.
#### Example:
Let's say you have a `Post` resource and you'd like to show a stripped-down version of your `body` field. So in your `posts_helper.rb` file you have the `extract_excerpt` method that sanitizes the body and truncates it to 120 characters.
```ruby
# app/helpers/posts_helper.rb
module PostsHelper
def extract_excerpt(body)
ActionView::Base.full_sanitizer.sanitize(body).truncate 120
end
end
```
Now, you'd like to use that helper inside one of you computed fields.
```ruby
class Avo::Resources::Post < Avo::BaseResource
def fields
field :excerpt, as: :text do |model|
extract_excerpt model.body
end
end
end
```
Initially you'll get an error similar to `undefined method 'extract_excerpt' for #`. That's because the compute field executes that method in a scope that's different from your application controller, thus not having that method present.
## The solution
The fix is to include the helper module in the `BaseField` and we can do that using this snippet somewhere in the app (you can add it in `config/initializers/avo.rb`).
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
# Usual Avo config
end
module FieldExtensions
# Include a specific helper
include PostsHelper
end
Rails.configuration.to_prepare do
Avo::Fields::BaseField.include FieldExtensions
end
```
Or you can go wild and include all helpers programatically.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
# Usual Avo config
end
module FieldExtensions
# Include all helpers
helper_names = ActionController::Base.all_helpers_from_path Rails.root.join("app", "helpers")
helpers = ActionController::Base.modules_for_helpers helper_names
helpers.each do |helper|
send(:include, helper)
end
end
Rails.configuration.to_prepare do
Avo::Fields::BaseField.include FieldExtensions
end
```
Now you can reference all helpers in your `Resource` files.
---
## Generation Information
- **Generated at:** 2026-05-13T21:32:08.974Z
- **Total sections:** 167
### Source Files
- docs/4.0/index.md
- docs/4.0/technical-support.md
- docs/4.0/best-practices.md
- docs/4.0/agentic-engineering.md
- docs/4.0/installation.md
- docs/4.0/gem-server-authentication.md
- docs/4.0/license-troubleshooting.md
- docs/4.0/authentication.md
- docs/4.0/authorization.md
- docs/4.0/resources.md
- docs/4.0/array-resources.md
- docs/4.0/http-resources.md
- docs/4.0/record-previews.md
- docs/4.0/scopes.md
- docs/4.0/records-reordering.md
- docs/4.0/discreet-information.md
- docs/4.0/customizable-controls.md
- docs/4.0/avatar.md
- docs/4.0/cover.md
- docs/4.0/views.md
- docs/4.0/table-view.md
- docs/4.0/grid-view.md
- docs/4.0/map-view.md
- docs/4.0/controllers.md
- docs/4.0/breadcrumbs.md
- docs/4.0/fields.md
- docs/4.0/field-options.md
- docs/4.0/field-discovery.md
- docs/4.0/resource-header.md
- docs/4.0/resource-panels.md
- docs/4.0/resource-sidebar.md
- docs/4.0/tabs.md
- docs/4.0/fields/array.md
- docs/4.0/fields/avatar.md
- docs/4.0/fields/badge.md
- docs/4.0/fields/boolean.md
- docs/4.0/fields/boolean_group.md
- docs/4.0/fields/code.md
- docs/4.0/fields/country.md
- docs/4.0/fields/date.md
- docs/4.0/fields/date_time.md
- docs/4.0/fields/easy_mde.md
- docs/4.0/fields/external_image.md
- docs/4.0/fields/file.md
- docs/4.0/fields/files.md
- docs/4.0/fields/gravatar.md
- docs/4.0/fields/heading.md
- docs/4.0/fields/hidden.md
- docs/4.0/fields/id.md
- docs/4.0/fields/key_value.md
- docs/4.0/fields/location.md
- docs/4.0/fields/markdown.md
- docs/4.0/fields/money.md
- docs/4.0/fields/number.md
- docs/4.0/fields/password.md
- docs/4.0/fields/preview.md
- docs/4.0/fields/progress_bar.md
- docs/4.0/fields/radio.md
- docs/4.0/fields/record_link.md
- docs/4.0/fields/rhino.md
- docs/4.0/fields/select.md
- docs/4.0/fields/stars.md
- docs/4.0/fields/status.md
- docs/4.0/fields/tags.md
- docs/4.0/fields/text.md
- docs/4.0/fields/textarea.md
- docs/4.0/fields/time.md
- docs/4.0/fields/tip_tap.md
- docs/4.0/fields/trix.md
- docs/4.0/associations.md
- docs/4.0/associations/belongs_to.md
- docs/4.0/associations/has_one.md
- docs/4.0/associations/has_many.md
- docs/4.0/associations/has_and_belongs_to_many.md
- docs/4.0/actions/overview.md
- docs/4.0/actions/generate.md
- docs/4.0/actions/registration.md
- docs/4.0/actions/execution.md
- docs/4.0/actions/customization.md
- docs/4.0/actions/guides-and-tutorials.md
- docs/4.0/filters.md
- docs/4.0/basic-filters.md
- docs/4.0/dynamic-filters.md
- docs/4.0/customization.md
- docs/4.0/eject-views.md
- docs/4.0/custom-view-types.md
- docs/4.0/menu-editor.md
- docs/4.0/search/resource-search.md
- docs/4.0/search/global-search.md
- docs/4.0/i18n.md
- docs/4.0/branding.md
- docs/4.0/routing.md
- docs/4.0/multitenancy.md
- docs/4.0/custom-tools.md
- docs/4.0/custom-fields.md
- docs/4.0/custom-errors.md
- docs/4.0/resource-tools.md
- docs/4.0/stimulus-integration.md
- docs/4.0/custom-asset-pipeline.md
- docs/4.0/tailwindcss-integration.md
- docs/4.0/dashboards.md
- docs/4.0/cards.md
- docs/4.0/kanban-boards.md
- docs/4.0/forms-and-pages/overview.md
- docs/4.0/forms-and-pages/generator.md
- docs/4.0/forms-and-pages/pages.md
- docs/4.0/forms-and-pages/forms.md
- docs/4.0/forms-and-pages/guides-and-tutorials.md
- docs/4.0/rest-api/mount.md
- docs/4.0/rest-api/generators.md
- docs/4.0/rest-api/csrf-protection.md
- docs/4.0/rest-api/authentication.md
- docs/4.0/collaboration/overview.md
- docs/4.0/collaboration/authorization.md
- docs/4.0/notifications.md
- docs/4.0/media-library.md
- docs/4.0/cache.md
- docs/4.0/views-performance.md
- docs/4.0/internals.md
- docs/4.0/testing.md
- docs/4.0/keyboard-shortcuts.md
- docs/4.0/avo-current.md
- docs/4.0/execution-context.md
- docs/4.0/encryption-service.md
- docs/4.0/select-all.md
- docs/4.0/icons.md
- docs/4.0/internal-model-names.md
- docs/4.0/rails-engines-paths.md
- docs/4.0/native-components/avo-panel-component.md
- docs/4.0/native-field-components.md
- docs/4.0/field-wrappers.md
- docs/4.0/avo-application-controller.md
- docs/4.0/asset-manager.md
- docs/4.0/plugins.md
- docs/4.0/custom-view-types.md
- docs/4.0/guides.md
- docs/4.0/guides/act-as-taggable-on-integration.md
- docs/4.0/guides/acts_as_tenant_integration.md
- docs/4.0/guides/add-nested-fields-to-forms.md
- docs/4.0/guides/api-only-app.md
- docs/4.0/guides/attachment-policy-extension-for-pundit.md
- docs/4.0/guides/basic-authentication.md
- docs/4.0/guides/bulk_destroy_action_using_customizable_controls.md
- docs/4.0/guides/conditionally-render-styled-rows.md
- docs/4.0/guides/custom-ids.md
- docs/4.0/guides/custom-link-field.md
- docs/4.0/guides/display-and-edit-join-table-fields.md
- docs/4.0/guides/display-scope-record-count.md
- docs/4.0/guides/export-to-csv.md
- docs/4.0/guides/format-ruby-object-to-json.md
- docs/4.0/guides/generating-components-for-fields.md
- docs/4.0/guides/handle-404-responses-in-avo.md
- docs/4.0/guides/hide-field-labels.md
- docs/4.0/guides/how-to-use-phlex-components.md
- docs/4.0/guides/manage-information-heavy-resources.md
- docs/4.0/guides/multi-language-urls.md
- docs/4.0/guides/multilingual-content.md
- docs/4.0/guides/multitenancy.md
- docs/4.0/guides/nested-records-when-creating.md
- docs/4.0/guides/rails-authentication-scaffold.md
- docs/4.0/guides/rest-api-integration.md
- docs/4.0/guides/rolify-integration.md
- docs/4.0/guides/run-avo-on-the-root-path.md
- docs/4.0/guides/safely-override-resource-components.md
- docs/4.0/guides/tabs-counter-indicator.md
- docs/4.0/guides/use-markdown-in-help-attributes.md
- docs/4.0/guides/use-own-helpers-in-resource-files.md