# Avo for Ruby on Rails Documentation - Version 2.0
Generated from Avo documentation v2.0 for LLM consumption
# Getting Started
Avo is a tool that helps developers and teams build apps 10x faster. It takes the things we always build for every app and abstracts them in familiar configuration files.
It has three main parts:
1. [The CRUD UI](#_1-the-crud-ui)
2. [Dashboards](#_2-dashboards)
3. [The custom content](#_3-the-custom-content)
## 1. The CRUD UI
If before, we built apps by creating layouts, adding controller methods to extract _data_ from the database, display it on the screen, worrying how we present it to the user, capture the users input as best we can and writing logic to send that data back to the database, Avo takes a different approach.
It only needs to know what kind of data you need to expose and what type it is. After that, it takes care of the rest.
You **tell it** you need to manage Users, Projects, Products, or any other types of data and what properties they have; `first_name` as `text`, `birthday` as `date`, `cover_photo` as `file` and so on.
There are the basic fields like text, textarea, select and boolean, and the more complex ones like trix, markdown, gravatar, and boolean_group. There's even an amazing file field that's tightly integrated with `Active Storage`. **You've never added files integration as easy as this before.**
## 2. Dashboards
Most apps need a way of displaying the stats in an aggregated form. Using the same configuration-based approach, Avo makes it so easy to display data in metric cards, charts, and even lets you take over using partial cards.
## 3. Custom content
Avo is a shell in which you develop your app. It offers a familiar DSL to configure the app you're building, but sometimes you might have custom needs. That's where the custom content comes in.
You can extend Avo in different layers. For example, in the CRUD UI, you may add Custom fields that slot in perfectly in the current panels and in each view. You can also add Resource tools to control the experience using standard Rails partials completely.
You can even create Custom tools where you can add all the content you need using Rails partials or View Components.
Most of the places where records are listed like Has many associations, attach modals, search, and more are scopable to meet your multi-tenancy scenarios.
Most of the views you see are exportable using the `eject` command.
StimulusJS is deeply baked into the CRUD UI and helps you extend the UI and make a complete experience for your users.
## Seamless upgrades
Avo comes packaged as a [gem](https://rubygems.org/gems/avo). Therefore, it does not pollute your app with its internal files. Instead, everything is tucked away neatly in the package.
That makes for a beautiful upgrade experience. You hit `bundle update avo` and get the newest and best of Avo without any file conflicts.
## Next up
Please take your time and read the documentation pages to see how Avo interacts with your app and how one should use it.
1. Install Avo in your app
1. Set up the current user
1. Create a Resource
1. Set up authorization
1. Set up licensing
1. [Explore the live demo app](https://main.avodemo.com/)
1. Explore these docs
1. Enjoy building your app without ever worrying about the admin layer ever again
1. Explore the FAQ pages for guides on how to set up your Avo instance.
## Walkthrough videos
### Build a blog admin panel
### Build a booking app
---
# Avo ❤️ Rails & Hotwire
In order to provide this all-in-one full-interface experience, we are using Rails' built-in [engines functionality](https://guides.rubyonrails.org/engines.html).
## Avo as a Rails engine
Avo is a **Ruby on Rails engine** that runs isolated and side-by-side with your app. You configure it using a familiar DSL and sometimes regular Rails code through controller methods and partials.
Avo's philosophy is to have as little business logic in your app as possible and give the developer the right tools to extend the functionality when needed.
That means we use a few files to configure most of the interface. When that configuration is not enough, we enable the developer to export (eject) partials or even generate new ones for their total control.
### Prepend engine name in URL path helpers
Because it's a **Rails engine** you'll have to follow a few engine rules. One of them is that [routes are isolated](https://guides.rubyonrails.org/engines.html#routes). That means that whenever you're using Rails' [path helpers](https://guides.rubyonrails.org/routing.html#generating-paths-and-urls-from-code) you'll need to prepend the name of the engine. For example, Avo's name is `avo,` and your app's engine name is `main_app`.
```ruby
# When referencing an Avo route, use avo
link_to 'Users', avo.resources_users_path
link_to user.name, avo.resources_user_path(user)
# When referencing a path for your app, use main_app
link_to "Contact", main_app.contact_path
link_to post.name, main_app.posts_path(post)
```
### Use your helpers inside Avo
This is something that we'd like to improve in the future, but the flow right now is to 1. include the helper module inside the controller you need it for and then 2. reference the methods from the `view_context.controller` object in resource files or any other place you'd need them.
```ruby{3-5,10,16}
# app/helpers/application_helper.rb
module ApplicationHelper
def render_copyright_info
"Copyright #{Date.today.year}"
end
end
# app/controller/avo/products_controller.rb
class Avo::ProductsController < Avo::ResourcesController
include ApplicationHelper
end
# app/avo/resources/products_resource.rb
class ProductsResource < Avo::BaseResource
field :copyright, as: :text do |model|
view_context.controller.render_copyright_info
end
end
```
## Hotwire
Avo's built with Hotwire, so anytime you'd like to use Turbo Frames, that's supported out of the box.
## StimulusJS
Avo comes loaded with Stimulus JS and has a quite deep integration with it by providing useful built-in helpers that improve the development experience.
Please follow the Stimulus JS guide that takes an in-depth look at all the possible ways of extending the UI.
---
# Licensing
Avo has two types of licenses. The **Community edition** is free to use and works best for personal, hobby, and small commercial projects, and the **Pro edition** for when you need more advanced features.
## Community vs. Pro
The **Community version** has powerful features that you can use today like Resource management, most feature-rich fields, out-of-the box sorting, filtering and actions and all the associations you need.
The **Pro version** has advanced authorization using Pundit, localization support, Custom tools, Custom fields and much [more](https://avohq.io/pricing). [More](https://avohq.io/roadmap) features like Settings screens and Themes are coming soon.
The features are separated by their level of complexity and maintenance needs. Selling the Avo Pro edition as a paid upgrade allows us to fund this business and work on it full-time. That way, Avo improves over time, helping developers with more features and customization options.
## One license per site
Each license can be used to run one application in one `production` environment on one URL. So when an app is in the `production` environment (`Rails.env.production?` is `true`), we only need to check that the license key and URL match the purchased license you're using for that app.
### More installations/environments per site
You might have the same site running in multiple environments (`development`, `staging`, `test`, `QA`, etc.) for non-production purposes. You don't need extra licenses for those environments as long as they are not production environments (`Rails.env.production?` returns `false`).
### Sites
You can see your license keys on your [licenses](https://avohq.io/licenses) page.
## Add the license key
After you purchase an Avo license, add it to your `config/initializers/avo.rb` file on the `license_key`, and change the license type from `community` to `pro`.
```ruby{3-4}
# config/initializers/avo.rb
Avo.configure do |config|
config.license = 'pro'
config.license_key = '************************' # or use ENV['AVO_LICENSE_KEY']
end
```
## Configure the display of license request timeout error
If you want to hide the badge displaying the license request timeout error, you can do it by setting the `display_license_request_timeout_error` configuration to `false`. It defaults to `true`.
```ruby{3}
# config/initializers/avo.rb
Avo.configure do |config|
config.display_license_request_timeout_error = false
end
```
## Purchase a license
You can purchase a license on the [purchase](https://avohq.io/purchase/pro) page.
## License validation
### "Phone home" mechanism
Avo pings the [HQ](https://avohq.io) (the license validation service) with some information about the current Avo installation. You can find the full payload below.
```ruby
# HQ ping payload
{
license: Avo.configuration.license,
license_key: Avo.configuration.license_key,
avo_version: Avo::VERSION,
rails_version: Rails::VERSION::STRING,
ruby_version: RUBY_VERSION,
environment: Rails.env,
ip: current_request.ip,
host: current_request.host,
port: current_request.port,
app_name: Rails.application.class.to_s.split("::").first,
avo_metadata: avo_metadata
}
```
That information helps us to identify your license and return a license valid/invalid response to Avo.
The requests are made at boot time and every hour when you use Avo on any license type.
If you need a special build without the license validation mechanism please get in touch.
## Upgrade your 1.0 license to 2.0
We are grateful to our `1.0` customers for believing in us. So we offer a free and easy upgrade path and **a year of free updates** for version `2.0`.
If you have a 1.0 license and want to upgrade to 2.0, you need to log in to [avohq.io](https://avohq.io), and go to the [licenses page](https://avohq.io/subscriptions), and hit the `Upgrade` button next to your license. You'll be redirected to the new subscription screen where you can start the subscription for 2.0.
After you add your billing details, you won't get charged immediately, but on the next billing cycle next year.
If you choose not to renew the subscription after one year, that's fine; you can cancel at any time, no biggie. You won't get charged and will keep the last version available at the end of that subscription.
---
# Technical support
Avo is designed to be a self-serve product with [comprehensive documentation](https://docs.avohq.io) and [demo apps](#demo-apps) to be used as references.
But, even the best of us get stuck at some point and you might need a nudge in the right direction. There are a few levels of how can get help.
1. [Open Source Software Support Policy](#open-source-software-support-policy)
1. [Self-help](#self-help)
1. [Help from the official team](#official-support)
## Open Source Software Support
Avo's Open Source Software (OSS) support primarily revolves around assisting users with issues related to the Avo and other Avo libraries. This involves troubleshooting and providing solutions for problems originating from Avo or its related subcomponents.
However, it is crucial to understand that the OSS support does not extend to application-specific issues that do not originate from Avo or its related parts.
This includes but is not limited to:
- Incorrect application configurations unrelated to Avo.
- Conflicts with other libraries or frameworks within your application.
- Deployment issues on specific infrastructure or platforms.
- Application-specific runtime errors.
- Problems caused by third-party plugins or extensions.
- Data issues within your application.
- Issues related to application performance optimization.
- Integration problems with other services or databases.
- Design and architecture questions about your specific application.
- Language-specific issues are unrelated to Avo or other Avo libraries.
We acknowledge that understanding your specific applications and their configuration is essential, but due to the time and resource demands, this goes beyond the scope of our OSS support.
:::tip Enhanced support
For users seeking assistance with application-specific issues, we offer a few paid technical support plans. These subscriptions provide comprehensive support, including help with application-specific problems.
1. Priority chat support
2. Advanced hands-on support
For more information about our support plans, please visit [this](https://avohq.io/support) page.
:::
## Self help
This is how you can help yourself.
## Help from the official team
You sometimes need help from the authors. There are a few ways to do that.
## Reproduction repository
The easiest way for us to troubleshoot and check on an issue is to send us a reproduction repository which we can install and run in our local environments.
```bash
# run this command to get a new Rails app with Avo installed
rails new -m https://avo.cool/new.rb APP_NAME
# run to install avo-pro
rails new -m https://avo.cool/new-pro.rb APP_NAME
# run to install avo-advanced
rails new -m https://avo.cool/new-advanced.rb APP_NAME
```
---
# Installation
## Requirements
- Ruby on Rails >= 6.0
- Ruby >= 2.7
- `api_only` set to `false`. More here.
- `propshaft` or `sprockets` gem
- Have the `secret_key_base` defined in any of the following `ENV["SECRET_KEY_BASE"]`, `Rails.application.credentials.secret_key_base`, or `Rails.application.secrets.secret_key_base`
:::warning Zeitwerk autoloading is required.
When adding Avo to a Rails app that was previously a Rails 5 app you must ensure that it uses zeitwerk for autoloading and Rails 6 defaults.
```ruby
# config/application.rb
config.autoloader = :zeitwerk
config.load_defaults 6.0
```
More on this [here](https://github.com/avo-hq/avo/issues/1096).
:::
## Installing Avo
Use [this](https://railsbytes.com/public/templates/zyvsME) RailsBytes template for a one-liner install process.
`rails app:template LOCATION='https://avohq.io/app-template'`
**OR**
Take it step by step.
1. Add `gem 'avo'` to your `Gemfile`
1. Run `bundle install`.
1. Run `bin/rails generate avo:install` to generate the initializer and add Avo to the `routes.rb` file.
1. Generate an Avo Resource
:::info
This will mount the app under `/avo` path. Visit that link to see the result.
:::
## Install from GitHub
You may also install Avo from GitHub but when you do that you must compile the assets yourself. You do that using the `rake avo:build-assets` command.
When pushing to production, make sure you build the assets on deploy time using this task.
```ruby
# Rakefile
Rake::Task["assets:precompile"].enhance do
Rake::Task["avo:build-assets"].execute
end
```
:::info
If you don't have the `assets:precompile` step in your deployment process, please adjust that with a different step you might have like `db:migrate`.
:::
## Mount Avo to a subdomain
You can use the regular `host` constraint in the `routes.rb` file.
```ruby
constraint host: 'avo' do
mount Avo::Engine, at: '/'
end
```
## Next steps
Please follow the next steps to ensure your app is secured and you have access to all the features you need.
1. Set up authentication and tell Avo who is your `current_user`. This step is required for the authorization feature to work.
1. Set up authorization. Don't let your data be exposed. Give users access to the data they need to see.
1. Set up licensing.
---
# Authentication
## Customize the `current_user` method
Avo will not assume your authentication provider (the `current_user` method returns `nil`). That means that you have to tell Avo who the `current_user` is.
### Using devise
For [devise](https://github.com/heartcombo/devise), you should set it to `current_user`.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.current_user_method = :current_user
end
```
### Use a different authenticator
Using another authentication provider, you may customize the `current_user` method to something else.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.current_user_method = :current_admin
end
```
If you get the current user from another object like `Current.user`, you may pass a block to the `current_user_method` key.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.current_user_method do
Current.user
end
end
```
## Customize the sign-out link
If your app responds to `destroy_user_session_path`, a sign-out menu item will be added on the bottom sidebar (when you click the three dots). If your app does not respond to this method, the link will be hidden unless you provide a custom sign-out path. There are two ways to customize the sign-out path.
### Customize the current user resource name
You can customize just the "user" part of the path name by setting `current_user_resource_name`. For example if you follow the `User` -> `current_user` convention, you might have a `destroy_current_user_session_path` that logs the user out.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.current_user_resource_name = :current_user
end
```
Or if your app provides a `destroy_current_admin_session_path` then you would need to set `current_user_resource_name` to `current_admin`.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.current_user_resource_name = :current_admin
end
```
### Customize the entire sign-out path
Alternatively, you can customize the sign-out path name completely by setting `sign_out_path_name`. For example, if your app provides `logout_path` then you would pass this name to `sign_out_path_name`.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.sign_out_path_name = :logout_path
end
```
If both `current_user_resource_name` and `sign_out_path_name` are set, `sign_out_path_name` takes precedence.
## Filter out requests
You probably do not want to allow Avo access to everybody. If you're using [devise](https://github.com/heartcombo/devise) in your app, use this block to filter out requests in your `routes.rb` file.
```ruby
authenticate :user do
mount Avo::Engine => '/avo'
end
```
You may also add custom user validation such as `user.admin?` to only permit a subset of users to your Avo instance.
```ruby
authenticate :user, -> user { user.admin? } do
mount Avo::Engine => '/avo'
end
```
Check out more examples of authentication on [sidekiq's authentication section](https://github.com/mperham/sidekiq/wiki/Monitoring#authentication).
## `authenticate_with` method
Alternatively, you can use the `authenticate_with` config attribute. It takes a block and evaluates it in Avo's `ApplicationController` as a `before_action`.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.authenticate_with do
authenticate_admin_user
end
end
```
Note that Avo's `ApplicationController` does not inherit from your app's `ApplicationController`, so any protected methods you defined would not work. Instead, you would need to explicitly write the authentication logic in the block. For example, if you store your `user_id` in the session hash, then you can do:
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.authenticate_with do
redirect_to '/' unless session[:user_id] == 1 # hard code user ids here
end
end
```
## Authorization
When you share access to Avo with your clients or large teams, you may want to restrict access to a resource or a subset of resources. You should set up your authorization rules (policies) to do that. Check out the authorization page for details on how to set that up.
---
# Authorization
When you share access to Avo with your clients or large teams, you may want to restrict access to a resource or a subset of resources. One example may be that only admin-level users may delete or update records.
By default, Avo leverages Pundit under the hood to manage the authorization.
:::info Pundit alternative
Pundit is just the default choice. You may plug in your own client using the instructions [here](#custom-authorization-clients).
:::
:::warning
You must manually require `pundit` or your authorization library in your `Gemfile`.
```ruby
# Minimal authorization through OO design and pure Ruby classes
gem "pundit"
```
:::
## Ensure Avo knows who your current user is
Before setting any policies up, please ensure Avo knows your current user. Usually, this 👇 set up should be fine, but follow the authentication guide for more information.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.current_user_method = :current_user
end
```
## Policies
Just run the regular pundit `bin/rails g pundit:policy Post` to generate a new policy.
**If this is a new app you need to install pundit first bin/rails g pundit:install.**
With this new policy, you may control what every type of user can do with Avo. The policy has the default methods for the regular controller actions: `index?`, `show?`, `create?`, `new?`, `update?`, `edit?` and `destroy?`.
These methods control whether the resource appears on the sidebar, if the view/edit/destroy buttons are visible or if a user has access to those index/show/edit/create pages.
## Associations
When using associations, you would like to set policies for `creating` new records on the association, allowing to `attach`, `detach`, `create` or `destroy` relevant records. Again, Avo makes this easy using a straightforward naming schema.
:::warning
Make sure you use the same pluralization as the association name.
For a `has_many :users` association use the plural version method `view_users?`, `edit_users?`, `detach_users?`, etc., not the singular version `detach_user?`.
:::
### Example scenario
We'll have this example of a `Post` resource with many `Comment`s through the `has_many :comments` association.
:::info The `record` variable in policy methods
In the `Post` `has_many` `Comments` example, when you want to authorize `show_comments?` in `PostPolicy` you will have a `Comment` instance as the `record` variable, but when you try to authorize the `attach_comments?`, you won't have that `Comment` instance because you want to create one, but we expose the parent `Post` instance so you have more information about that authorization action that you're trying to make.
:::
## Removing duplication
:::info A note on duplication
Let's take the following example:
A `User` has many `Contract`s. And you represent that in your Avo resource. How do you handle authorization to the `ContractResource`?
For one, you set the `ContractPolicy.index?` and `ContractPolicy.edit?` methods to `false` so regular users don't have access to all contracts (see and edit), and the `UserPolicy.view_contracts?` and `UserPolicy.edit_contracts?` set to `false`, because, when viewing a user you want to see all the contracts associated with that user and don't let them edit it.
You might be thinking that there's code duplication here. "Why do I need to set a different rule for `UserPolicy.edit_contracts?` when I already set the `ContractPolicy.edit?` to `false`? Isn't that going to take precedence?"
Now, let's imagine we have a user that is an admin in the application. The business need is that an admin has access to all contracts and can edit them. This is when we go back to the `ContractPolicy.edit?` and turn that to true for the admin user. And now we can separately control who and where a user can edit a contract.
:::
You may remove duplication by applying the same policy rule from the original policy.
```ruby
class CommentPolicy
# ... more policy methods
def edit
record.user_id == current_user.id
end
end
class PostPolicy
# ... more policy methods
def edit_comments?
Pundit.policy!(user, record).edit?
end
end
```
Now, whatever action you take for one comment, it will be available for the `edit_comments?` method in `PostPolicy`.
From version 2.31 we introduced a concern that removes the duplication and helps you apply the same rules to associations. You should include `Avo::Concerns::PolicyHelpers` in the `ApplicationPolicy` for it to be applied to all policy classes.
`PolicyHelpers` allows you to use the method `inherit_association_from_policy`. This method takes two arguments; `association_name` and the policy file you want to be used as a template.
```ruby
inherit_association_from_policy :comments, CommentPolicy
```
With just one line of code, it will define the following methods to policy your association:
```ruby
def create_comments?
CommentPolicy.new(user, record).create?
end
def edit_comments?
CommentPolicy.new(user, record).edit?
end
def update_comments?
CommentPolicy.new(user, record).update?
end
def destroy_comments?
CommentPolicy.new(user, record).destroy?
end
def show_comments?
CommentPolicy.new(user, record).show?
end
def reorder_comments?
CommentPolicy.new(user, record).reorder?
end
def act_on_comments?
CommentPolicy.new(user, record).act_on?
end
def view_comments?
CommentPolicy.new(user, record).index?
end
```
Although these methods won't be visible in your policy code, you can still override them. For instance, if you include the following code in your `CommentPolicy`, it will be executed in place of the one defined by the helper:
```ruby
inherit_association_from_policy :comments, CommentPolicy
def destroy_comments?
false
end
```
## Attachments
When working with files, it may be necessary to establish policies that determine whether users can `upload`, `download` or `delete` files. Fortunately, Avo simplifies this process by providing a straightforward naming schema for these policies.
Both the `record` and the `user` will be available for you to access.
:::info AUTHORIZE IN BULK
If you want to allow or disallow these methods in bulk you can use a little meta-programming to assign all the same value.
```ruby
[:cover_photo, :audio].each do |file|
[:upload, :download, :delete].each do |action|
define_method "#{action}_#{file}?" do
true
end
end
end
```
:::
## Scopes
You may specify a scope for the , , and views.
```ruby{3-9}
class PostPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.admin?
scope.all
else
scope.where(published: true)
end
end
end
end
```
:::warning
This scope will be applied only to the view of Avo. It will not be applied to the association view.
Example:
A `Post` has_many `Comment`s. The `CommentPolicy::Scope` will not affect the `has_many` field. You need to add the `scope` option to the `has_many` field where you can modify the query.
```ruby
```
:::
## Using different policy methods
By default Avo will use the generated Pundit methods (`index?`, `show?`, `create?`, `new?`, `update?`, `edit?` and `destroy?`). But maybe, in your app, you're already using these methods and would like to use different ones for Avo. You may want override these methods inside your configuration with a simple map using the `authorization_methods` key.
```ruby{6-14}
Avo.configure do |config|
config.root_path = '/avo'
config.app_name = 'Avocadelicious'
config.license = 'pro'
config.license_key = ENV['AVO_LICENSE_KEY']
config.authorization_methods = {
index: 'avo_index?',
show: 'avo_show?',
edit: 'avo_edit?',
new: 'avo_new?',
update: 'avo_update?',
create: 'avo_create?',
destroy: 'avo_destroy?',
search: 'avo_search?',
}
end
```
Now, Avo will use `avo_index?` instead of `index?` to manage the **Index** view authorization.
## Raise errors when policies are missing
The default behavior of Avo is to allow missing policies for resources silently. So, if you have a `User` model and a `UserResource` but don't have a `UserPolicy`, Avo will not raise errors regarding missing policies and authorize that resource.
If, however, you need to be on the safe side of things and raise errors when a Resource is missing a Policy, you can toggle on the `raise_error_on_missing_policy` configuration.
```ruby{7}
# config/initializers/avo.rb
Avo.configure do |config|
config.root_path = '/avo'
config.app_name = 'Avocadelicious'
config.license = 'pro'
config.license_key = ENV['AVO_LICENSE_KEY']
config.raise_error_on_missing_policy = true
end
```
Now, you'll have to provide a policy for each resource you have in your app, thus making it a more secure app.
## Custom policies
By default, Avo will infer the policy from the model of the resource object. If you wish to use a different policy for a given resource, you can specify it directly in the resource using the `authorization_policy` option.
```ruby
class PhotoCommentResource < Avo::BaseResource
self.model_class = ::Comment
self.authorization_policy = PhotoCommentPolicy
# ...
end
```
## Custom authorization clients
:::info
Check out the [Pundit client](https://github.com/avo-hq/avo/blob/main/lib/avo/services/authorization_clients/pundit_client.rb) for reference.
:::
### Change the authorization client
In order to use a different client change the `authorization_client` option in the initializer.
The built-in possible values are `nil` and `:pundit`.
When you create your own client, pass the class name.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.authorization_client = 'Services::AuthorizationClients::CustomClient'
end
```
### Client methods
Each authorization client must expose a few methods.
## Rolify integration
Check out this guide to add rolify role management with Avo.
---
# 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
There is the possibility to force the usage of a custom cache store into Avo.
```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 computed system do not use MemoryStore in production because it will not be shared between multiple processes (when using Puma).
:::
## 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.cache_store = :solid_cache_store
```
Check [Solid Cache repository](https://github.com/rails/solid_cache) for additional valuable information.
---
# Resource options
Avo effortlessly empowers you to build an entire customer-facing interface for your Ruby on Rails application. One of the most powerful features is how easy you can administer your database records using the CRUD UI.
## Overview
Similar to how you configure your database layer using Rails `Model` files and their DSL, Avo's CRUD UI is configured using `Resource` files.
Each `Resource` maps out one of your models. There can be multiple `Resource`s associated to the same model if you need that.
All resources are located in the `app/avo/resources` directory. Unfortunately, `Resource`s can't be namespaced yet, so they all need to be in the root level of that directory.
:::warning
All resources from `app/avo/resources` are eager loaded on app boot-time to automatically have them available in your app.
This might might interfere with some setups.
If that happens you can manually register resources using [this guide](#manually-registering-resources).
:::
## Resources from model generation
```bash
bin/rails generate model car make:string mileage:integer
```
Running this command will generate the expected Rails files for a model and for Avo the `CarResource` and `CarsController`.
The auto-generated resource file will look like this:
```ruby
class CarResource < Avo::BaseResource
self.title = :id
self.includes = []
# self.search_query = -> do
# scope.ransack(id_eq: params[:q], m: "or").result(distinct: false)
# end
field :id, as: :id
# Generated fields from model
field :make, as: :text
field :mileage, as: :number
# add fields here
end
```
This behavior can be ommited by using the argument `--skip-avo-resource`. For example if we want to generate a `Car` model but no Avo counterpart we should use the following command:
```bash
bin/rails generate model car make:string kms:integer --skip-avo-resource
```
## Manually defining resources
```bash
bin/rails generate avo:resource post
```
This command will generate the `PostResource` file in `app/avo/resources/post_resource.rb` with the following code:
```ruby
# app/avo/resources/post_resource.rb
class PostResource < Avo::BaseResource
self.title = :id
self.includes = []
# self.search_query = -> do
# scope.ransack(id_eq: params[:q], m: "or").result(distinct: false)
# end
field :id, as: :id
# add fields here
end
```
From this config, Avo will infer a few things like the resource's model will be the `Post` model and the name of the resource is `Post`. But all of those inferred things are actually overridable.
Now, let's say we already have a model Post well defined with the following attributes:
```ruby
# == Schema Information
#
# Table name: posts
#
# id :bigint not null, primary key
# name :string
# body :text
# is_featured :boolean
# published_at :datetime
# user_id :bigint
# created_at :datetime not null
# updated_at :datetime not null
# status :integer default("draft")
#
class Post < ApplicationRecord
enum status: [:draft, :published, :archived]
validates :name, presence: true
has_one_attached :cover_photo
has_one_attached :audio
has_many_attached :attachments
belongs_to :user, optional: true
has_many :comments, as: :commentable
has_many :reviews, as: :reviewable
acts_as_taggable_on :tags
end
```
In this case, the avo resource will generate the fields (without any configuration) from the model attributes and relationships resulting in the following resource:
```ruby
class PostResource < Avo::BaseResource
self.title = :id
self.includes = []
# self.search_query = -> do
# scope.ransack(id_eq: params[:q], m: "or").result(distinct: false)
# end
field :id, as: :id
# Generated fields from model
field :name, as: :text
field :body, as: :textarea
field :is_featured, as: :boolean
field :published_at, as: :datetime
field :user_id, as: :number
field :status, as: :select, enum: ::Post.statuses
field :cover_photo, as: :file
field :audio, as: :file
field :attachments, as: :files
field :user, as: :belongs_to
field :comments, as: :has_many
field :reviews, as: :has_many
field :tags, as: :tags
# add fields here
end
```
It's also possible to specify the resource model class. For example, if we want to create a new resource named `MiniPostResource` using the `Post` model we can do that using the following command:
```bash
bin/rails generate avo:resource mini-post --model-class post
```
That command will create a new resource with the same attributes as the post resource above with specifying the `model_class`:
```ruby
class MiniPostResource < Avo::BaseResource
self.model_class = ::Post
end
```
:::info
You can see the result in the admin panel using this URL `/avo`. The `Post` resource will be visible on the left sidebar.
:::
### Fields
`Resource` files tell Avo what models should be displayed in the UI, but not what kinds of data they hold. You do that using fields.
One can add more fields to this resource below the `id` field using the `field DATABASE_COLUMN, as: FIELD_TYPE, **FIELD_OPTIONS` signature.
```ruby{5-15}
class PostResource < Avo::BaseResource
self.title = :id
self.includes = []
field :id, as: :id
field :name, as: :text, required: true
field :body, as: :trix, placeholder: "Add the post body here", always_show: false
field :cover_photo, as: :file, is_image: true, link_to_resource: true
field :is_featured, as: :boolean
field :is_published, as: :boolean do |model|
model.published_at.present?
end
field :user, as: :belongs_to, placeholder: "—"
end
```
## Use multiple resources for the same model
### `model_resource_mapping`
Usually, an Avo Resource maps to one Rails model. So there will be a one-to-one relationship between them. But there will be scenarios where you'd like to create another resource for the same model.
Let's take as an example the `User` model. You'll have an `UserResource` associated with it.
```ruby
# app/models/user.rb
class User < ApplicationRecord
end
# app/avo/resources/user_resource.rb
class UserResource < Avo::BaseResource
self.title = :name
field :id, as: :id, link_to_resource: true
field :email, as: :gravatar, link_to_resource: true, as_avatar: :circle
field :first_name, as: :text, required: true, placeholder: "John"
field :last_name, as: :text, required: true, placeholder: "Doe"
end
```

So when you click on the Users sidebar menu item, you get to the `Index` page where all the users will be displayed. The information displayed will be the gravatar image, the first and the last name.
Let's say we have a `Team` model with many `User`s. You'll have a `TeamResource` like so:
```ruby{11}
# app/models/team.rb
class Team < ApplicationRecord
end
# app/avo/resources/team_resource.rb
class TeamResource < Avo::BaseResource
self.title = :name
field :id, as: :id, link_to_resource: true
field :name, as: :text
field :users, as: :has_many
end
```
From that configuration, Avo will figure out that the `users` field points to the `UserResource` and will use that one to display the users.
But, let's imagine that we don't want to display the gravatar on the `has_many` association, and we want to show the name on one column and the number of projects the user has on another column.
We can create a different resource named `TeamUserResource` and add those fields.
```ruby
# app/avo/resources/team_user_resource.rb
class TeamUserResource < Avo::BaseResource
self.title = :name
field :id, as: :id, link_to_resource: true
field :name, as: :text
field :projects_count, as: :number
end
```
We also need to update the `TeamResource` to use the new `TeamUserResource` for reference.
```ruby
# app/avo/resources/team_resource.rb
class TeamResource < Avo::BaseResource
self.title = :name
field :id, as: :id, link_to_resource: true
field :name, as: :text
field :users, as: :has_many, use_resource: TeamUserResource
end
```

But now, if we visit the `Users` page, we will see the fields for the `TeamUserResource` instead of `UserResource`, and that's because Avo fetches the resources in an alphabetical order, and `TeamUserResource` is before `UserResource`. That's definitely not what we want.
The same might happen if you reference the `User` in other associations throughout your resource files.
To mitigate that, we are going to use the `model_resource_mapping` option to set the "default" resource for a model.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.model_resource_mapping = {
'User': 'UserResource'
}
end
```
That will "shortcircuit" the regular alphabetical search and use the `UserResource` every time we don't specify otherwise.
We can still tell Avo which resource to use in other `has_many` or `has_and_belongs_to_many` associations with the `use_resource` option.
## Setting the title of the resource
Initially, the `title` attribute is set to `:id`, so the model's `id` attribute will be used to display the resource in search results and belongs select fields. You usually change it to something more representative, like the model's `title`, `name` or `label` attributes.
```ruby
class PostResource < Avo::BaseResource
self.title = :name # it will now reference @post.name to show you the title
end
```
### Using a computed title
If you don't have a `title`, `name`, or `label` attribute in the database, you can add a getter method to your model where you compose the name.
```ruby{2}
# app/avo/resources/comment_resource.rb
class CommentResource < Avo::BaseResource
self.title = :tiny_name
# fieldd go here
end
# app/models/comment.rb
class Comment < ApplicationRecord
def tiny_name
ActionView::Base.full_sanitizer.sanitize(body).truncate 30
end
end
```
## Resource description
You might want to display information about the current resource to your users. Then, using the `description` class attribute, you can add some text to the `Index`, `Show`, `Edit`, and `New` views.
There are two ways of setting the description. The quick way as a `string` and the more customizable way as a `block`.
### Set the description as a string
```ruby{3}
class UserResource < Avo::BaseResource
self.title = :name
self.description = "These are the users of the app."
end
```
This is the quick way to set the label, and it will be displayed **only on the `Index` page**. If you want to show the message on all views, use the block method.
### Set the description as a block
This is the more customizable method where you can access the `model`, `view`, `user` (the current user), and `params` objects.
```ruby{3-13}
class UserResource < Avo::BaseResource
self.title = :name
self.description = -> do
if view == :index
"These are the users of the app"
else
if user.is_admin?
"You can update all properties for this user: #{model.id}"
else
"You can update some properties for this user: #{model.id}"
end
end
end
end
```
## Eager loading
If you regularly need access to a resource's associations, you can tell Avo to eager load those associations on the **Index** view using `includes`. That will help you avoid those nasty `n+1` performance issues.
```ruby
class PostResource < Avo::BaseResource
self.includes = [:user, :tags]
end
```
## Views
### Grid view
On **Index**, the most common view type is `:table`, but you might have some data that you want to display in a **grid**. You can change that by setting `default_view_type` to `:grid` and by adding the `grid` block.
```ruby{2}
class PostResource < Avo::BaseResource
self.default_view_type = :grid
end
```
Find out more on the grid view documentation page.
## Custom model class
You might have a model that belongs to a namespace or has a different name than the resource. For that scenario, you can use the `@model` option to tell Avo which model to reference.
```ruby{2}
class DelayedJobResource < Avo::BaseResource
self.model_class = ::Delayed::Job
field :id, as: :id
# ... other fields go here
end
```
## Routing
Avo will automatically generate routes based on the resource name when generating a resource.
```
PostResource -> /avo/resources/posts
PhotoCommentResource -> /avo/resources/photo_comments
```
If you change the resource name, you should change the generated controller name too.
## Devise password optional
If you use `devise` and update your user models (usually `User`) without passing a password, you will get a validation error. You can use `devise_password_optional` to stop receiving that error. It will [strip out](https://stackoverflow.com/questions/5113248/devise-update-user-without-password/11676957#11676957) the `password` key from `params`.
```ruby
class UserResource < Avo::BaseResource
self.devise_password_optional = true
end
```
Related:
- Password field
## Unscoped queries on `Index`
You might have a `default_scope` on your model that you don't want to be applied when you render the `Index` view.
```ruby{2}
class Project < ApplicationRecord
default_scope { order(name: :asc) }
end
```
You can unscope the query using the `unscoped_queries_on_index` (defaults to `false`) class variable on that resource.
```ruby{3}
class ProjectResource < Avo::BaseResource
self.title = :name
self.unscoped_queries_on_index = true
# fields go here
end
```
## Hide resource from the sidebar
When you get started, the sidebar will be auto-generated for you with all the dashboards, resources, and custom tools. However, you may have resources that should not appear on the sidebar, which you can hide using the `visible_on_sidebar` option.
```ruby{3}
class TeamMembershipResource < Avo::BaseResource
self.title = :id
self.visible_on_sidebar = false
# fields declaration
end
```
:::warning
This option is used in the **auto-generated menu**, not in the **menu editor**.
You'll have to use your own logic in the `visible` block for that.
:::
## Extending `Avo::ResourcesController`
You may need to execute additional actions on the `ResourcesController` before loading the Avo pages. You can create an `Avo::BaseResourcesController` and extend your resource controller from it.
```ruby
# app/controllers/avo/base_resources_controller.rb
class Avo::BaseResourcesController < Avo::ResourcesController
include AuthenticationController::Authentication
before_action :is_logged_in?
end
# app/controllers/avo/posts_controller.rb
class Avo::PostsController < Avo::BaseResourcesController
end
```
*You can't use `Avo::BaseController` and `Avo::ResourcesController` as **your base controller**. They are defined inside Avo.*
## Show buttons on form footers
If you have a lot of fields on a resource, that form might get pretty tall. So it would be useful to have the `Save` button in the footer of that form.
You can do that by setting the `buttons_on_form_footers` option to `true` in your initializer. That will add the `Back` and `Save` buttons on the footer of that form for the `New` and `Edit` screens.
```ruby{3}
# config/initializers/avo.rb
Avo.configure do |config|
config.buttons_on_form_footers = true
end
```
## Customize what happens after a record is created/edited
For some resources, it might make sense to redirect to something other than the `Show` view. With `after_create_path` and `after_update_path` you can control that.
The valid options are `:show` (default), `:edit`, or `:index`.
```ruby{2-3}
class CommentResource < Avo::BaseResource
self.after_create_path = :index
self.after_update_path = :edit
field :id, as: :id
field :body, as: :textarea
end
```
## Hide the record selector checkbox
You might have resources that will never be selected, and you do not need that checkbox to waste your horizontal space.
You can hide it using the `record_selector` class_attribute.
```ruby{2}
class CommentResource < Avo::BaseResource
self.record_selector = false
field :id, as: :id
field :body, as: :textarea
end
```
## Link to child resource (STI)
`self.link_to_child_resource = true|false`
Let's take an example. We have a `Person` model and `Sibling` and `Spouse` models that inherit from it (STI).
Declare this option on the parent resource. When a user is on the view of your the `PersonResource` and clicks on the view button of a `Person` they will be redirected to a `Child` or `Spouse` resource instead of a `Person` resource.
## Manually registering resources
In order to have a more straightforward experience when getting started with Avo, we are eager-loading the `app/avo/resources` directory. That makes all those resources available to your app without you doing anything else.
That might make some Rails apps raise some errors.
In order to mitigate that we added a way of manually declaring resources.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.resources = [
"UserResource",
"FishResource",
]
end
```
This tells Avo which resources you use and stops the eager-loading process on boot-time.
This means that other resources that are not declared in this array will not show up in your app.
---
# 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.
## Update methods
For the `update` method, you can modify the `after_update_path`, the messages, and the actions both on success or failure.
## Destroy methods
For the `destroy` method, you can modify the `after_destroy_path`, the messages, and the actions both on success or failure.
---
# Field options
## Declaring fields
Each Avo resource has a `field` method that registers your `Resource`'s fields. Avo ships with various simple fields like `text`, `textarea`, `number`, `password`, `boolean`, `select`, and more complex ones like `markdown`, `key_value`, `trix`, and `code`.
We can use the `field` method like so:
```ruby
field :name, as: :text
```
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 admin panel. On the view, we will get a new text column. On the view, we will also get a text value 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.
## 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.
:::
## 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`).
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]
```
## 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 `model` object, too (`resource.model`).
```ruby
field :is_featured, as: :boolean, visible: -> (resource:) { context[:user].is_admin? } # show field based on the context object
field :is_featured, as: :boolean, visible: -> (resource:) { resource.name.include? 'user' } # show field based on the resource name
field :is_featured, as: :boolean, visible: -> (resource:) { resource.model.published_at.present? } # show field based on a model attribute
```
### Using `if` for field visibility
You might be tempted to use the `if` statement to show/hide fields conditionally. However, that's not the best choice because the fields are registered at boot time, and some features are only available at runtime. Let's take the `context` object, for example. You might have the `current_user` assigned to the `context`, which will not be present at the app's boot time. Instead, that's present at request time when you have a `request` present from which you can find the user.
```ruby{4-7,13-16}
# ❌ Don't do
class CommentResource < Avo::BaseResource
field :id, as: :id
if context[:current_user].admin?
field :body, as: :textarea
field :tiny_name, as: :text, only_on: :index, as_description: true
end
end
# ✅ Do instead
class CommentResource < Avo::BaseResource
field :id, as: :id
with_options visible: -> (resource:) { context[:current_user].admin?} do
field :body, as: :textarea
field :tiny_name, as: :text, only_on: :index, as_description: true
end
end
```
So now, instead of relying on a request object unavailable at boot time, you can pass it a lambda function that will be executed on request time with all the required information.
:::warning Since 2.30.2
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.model` object is present before trying to use it.
:::
```ruby
# `resource.model` is nil when submitting the form on resource creation
field :name, as: :text, visible -> (resource: ) { resource.model.enabled? }
# Do this instead
field :name, as: :text, visible -> (resource: ) { resource.model&.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 `model` (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 |model, resource, view|
model.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. You may do that using `format_using` block.
Notice that this block will have effect on **all** views.
You have access to a bunch of variables inside this block, all the defaults that `Avo::ExecutionContext` provides plus `value`, `model`, `key`, `resource`, `view` and `field`.
:::warning
We removed the `value` argument from `format_using` since version `2.36`.
:::
```ruby
field :is_writer, as: :text, format_using: -> {
if view == :new || view == :edit
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: -> {
link_to(value, value, target: "_blank")
} do |model, *args|
main_app.companies_url(model)
end
```
### 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) }
```
## Modify the value before saving it to the database
Similar to `format_using` we added `update_using` which will process the value sent from the UI before setting it on the model.
```ruby
# Cast the text version of the field to actual JSOn to save to the database.
field :metadata, as: :code, update_using: -> {
# You have access to the following variables:
# - value
# - resource
# - record
# - view
# - view_context
# - context
# - params
# - request
ActiveSupport::JSON.decode(value)
}
```
## 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
```
## 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 UserResource < Avo::BaseResource
field :is_writer,
as: :text,
sortable: ->(query, direction) {
# 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 |model, resource, view, field|
model.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_resource.rb]
class PostResource < Avo::BaseResource
field :last_commented_at,
as: :date,
sortable: ->(query, direction) {
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
When you want to mark a field as mandatory, you may use the `required` option to add an asterisk to that field, indicating that it's mandatory.
```ruby
field :name, as: :text, required: true
```
:::warning
This option is only a cosmetic one. It will not add the validation logic to your model. You must add that yourself (`validates :name, presence: true`).
:::
:::info
For Avo version 2.14 and higher Avo will automatically detect your validation rules and mark the field as required by default.
:::
You may use a block as well. It will be executed in the `ViewRecordHost` 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
```
## Readonly
When you need to prevent the user from editing a field, the `readonly` 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, readonly: true
```
### Readonly as a block
You may use a block as well. It will be executed in the `ViewRecordHost` and you will have access to the `view`, `record`, `params`, `context`, `view_context`, and `current_user`.
```ruby
field :id, as: :number, readonly: -> { view == :edit } # make the field readonly only on the new edit view
```
## Disabled
When you need to prevent the user from editing a field, the `disabled` 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, disabled: 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' }
```
## Help text
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.
```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.'
```
:::info
Since version `2.19`, the `default` block is being evaluated in the `ResourceViewRecordHost`.
:::
## 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 resource
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_resource` to change a table cell to be a link to that resource.
```ruby
# for id field
field :id, as: :id, link_to_resource: true
# for text field
field :name, as: :text, link_to_resource: true
# for gravatar field
field :email, as: :gravatar, link_to_resource: 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 `index_text_align` option. Valid values are `:right` or `:center`.
```ruby{2}
class ProjectResource < Avo::BaseResource
field :users_required, as: :number, index_text_align: :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.
---
# Records ordering
A typical scenario is when you need to set your records into a specific order. Like re-ordering `Slide`s inside a `Carousel` or `MenuItem`s inside a `Menu`.
The `ordering` class attribute is your friend for this. You can set four actions `higher`, `lower`, `to_top` or `to_bottom`, and the `display_inline` and `visible_on` options.
The actions are simple lambda functions but coupled with your logic or an ordering gem, and they can be pretty powerful.
## Configuration
I'll demonstrate the ordering feature using the `act_as_list` gem.
Install and configure the gem as instructed in the [tutorials](https://github.com/brendon/acts_as_list#example). Please ensure you [give all records position attribute values](https://github.com/brendon/acts_as_list#adding-acts_as_list-to-an-existing-model), so the gem works fine.
Next, add the order actions like below.
```ruby
class CourseLinkResource < Avo::BaseResource
self.ordering = {
visible_on: :index,
actions: {
higher: -> { record.move_higher },
lower: -> { record.move_lower },
to_top: -> { record.move_to_top },
to_bottom: -> { record.move_to_bottom },
}
}
end
```
The `record` is the actual instantiated model. The `move_higher`, `move_lower`, `move_to_top`, and `move_to_bottom` methods are provided by `act_as_list`. If you're not using that gem, you can add your logic inside to change the position of the record.
The actions have access to `record`, `resource`, `options` (the `ordering` class attribute) and `params` (the `request` params).
That configuration will generate a button with a popover containing the ordering buttons.
## Always show the order buttons
If the resource you're trying to update requires re-ordering often, you can have the buttons visible at all times using the `display_inline: true` option.
```ruby
class CourseLinkResource < Avo::BaseResource
self.ordering = {
display_inline: true,
visible_on: :index,
actions: {
higher: -> { record.move_higher },
lower: -> { record.move_lower },
to_top: -> { record.move_to_top },
to_bottom: -> { record.move_to_bottom },
}
}
end
```
## Display the buttons in the `Index` view or association view
A typical scenario is to order the records only in the scope of a parent record, like order the `MenuItems` for a `Menu` or `Slides` for a `Slider`. So you wouldn't need to have the order buttons on the `Index` view but only in the association section.
To control that, you can use the `visible_on` option. The possible values are `:index`, `:association` or `[:index, :association]` for both views.
## Change the scope on the `Index` view
Naturally, you'll want to apply the `order(position: :asc)` condition to your query. You may do that in two ways.
1. Add a `default_scope` to your model. If you're using this ordering scheme only in Avo, then, this is not the recommended way, because it will add that scope to all queries for that model and you probably don't want that.
2. Use the [`resolve_query_scope`](https://docs.avohq.io/2.0/customization.html#custom-query-scopes) to alter the query in Avo.
```ruby{2-4}
class CourseLinkResource < Avo::BaseResource
self.resolve_query_scope = ->(model_class:) do
model_class.order(position: :asc)
end
self.ordering = {
display_inline: true,
visible_on: :index, # :index or :association
actions: {
higher: -> { record.move_higher }, # has access to record, resource, options, params
lower: -> { record.move_lower },
to_top: -> { record.move_to_top },
to_bottom: -> { record.move_to_bottom }
}
}
end
---
# Tabs and panels
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.
## Panels
First, we should talk a bit about panels. They are the backbone of Avo's display infrastructure. Most of the information that's on display is wrapped inside a panel. They help to give Avo that uniform design on every page. 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 UserResource < Avo::BaseResource
field :id, as: :id, link_to_resource: 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
```
You can customize the panel `name` and panel `description`.
### Index view fields
By default, only the fields declared in the root will be visible on the `Index` view.
```ruby{3-7}
class UserResource < Avo::BaseResource
# Only these fields will be visible on the `Index` view
field :id, as: :id, link_to_resource: true
field :email, as: :text, name: "User Email", required: true
field :name, as: :text, only_on: :index do |model|
"#{model.first_name} #{model.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
```
## Tabs
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 UserResource < Avo::BaseResource
field :id, as: :id, link_to_resource: true
field :email, as: :text, name: "User Email", required: true
tabs do
tab "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
```
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 `name` is mandatory 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 `Show` 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' Edit', 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.
## Display as pills
When you have a lot of tabs in one group the tab switcher will overflow on the right-hand side. It will become scrollable to allow your users to get to the last tabs in the group.

If you want to be able to see all your tabs in one group at a glance you may change the display to `:pills`. The pills will collapse and won't overflow off the page.

### Display all tabs as pills
If you want to display all tabs as pills update your initializer's `tabs_style`.
```ruby
Avo.configure do |config|
config.tabs_style = :pills
end
```
### Display only some tabs as pills
If you only need to display certain tabs as pills you can do that using the `style` option.
```ruby
tabs style: :pills do
# tabs go here
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 woul don the root level.
```ruby
class UserResource < Avo::BaseResource
field :id, as: :id, link_to_resource: true
field :first_name, as: :text, placeholder: "John"
field :last_name, as: :text, placeholder: "Doe"
sidebar do
field :email, as: :gravatar, link_to_resource: true, as_avatar: :circle, only_on: :show
field :active, as: :boolean, name: "Is active", only_on: :show
end
end
```

:::info
For this initial iteration you may use the `field` and `heading` helpers.
:::
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.
---
# Customizable controls

One of the things that we wanted to support from day one is customizable controls on resource pages.
:::warning
At the moment, only the `Show` view has customizable controls.
:::
## Default buttons
By default, Avo displays a few buttons for the user to use on the , , and views which you can override using the appropriate resource options.
## Show page
On the view the default configuration is `back_button`, `delete_button`, `detach_button`, `actions_list`, and `edit_button`. You can override them using `show_controls`.
## Customize the controls
To start customizing the buttons, add a `show_controls` block and start adding the desired controls.
```ruby
class FishResource < Avo::BaseResource
self.show_controls = -> do
back_button label: "", title: "Go back now"
link_to "Fish.com", "https://fish.com", icon: "heroicons/outline/academic-cap", target: :_blank
link_to "Turbo demo", "/admin/resources/fish/#{params[:id]}?change_to=🚀🚀🚀 New content here 🚀🚀🚀",
class: ".custom-class",
data: {
turbo_frame: "fish_custom_action_demo"
}
delete_button label: "", title: "something"
detach_button label: "", title: "something"
actions_list exclude: [ReleaseFish], style: :primary, color: :slate
action ReleaseFish, style: :primary, color: :fuchsia, icon: "heroicons/outline/globe"
edit_button label: ""
end
end
```
## Controls
A control is an item that you can place in a designated area. They can be one of the default ones like `back_button`, `delete_button`, or `edit_button` to custom ones like `link_to` or `action`.
You may use the following controls:
:::warning
The way `show_controls` works is like a shortcut the the actions that you already declared on your resource, so you should also declare it on the resource as you normally would in order to have it here.
```ruby{6,10}
class FishResource < Avo::BaseResource
self.title = :name
self.show_controls = -> do
# In order to use it here
action ReleaseFish, style: :primary, color: :fuchsia
end
# Also declare it here
action ReleaseFish, arguments: { both_actions: "Will use them" }
end
:::
## Control Options
## Conditionally hiding/showing actions
Actions have the `visible` block where you can control the visibility of an action. In the context of `show_controls` that block is not taken into account, but yiou can use regular `if`/`else` statements because the action declaration is wrapped in a block.
```ruby{6-8}
class FishResource < Avo::BaseResource
self.show_controls = -> do
back_button label: "", title: "Go back now"
# visibility conditional
if record.something?
action ReleaseFish, style: :primary, color: :fuchsia, icon: "heroicons/outline/globe"
end
edit_button label: ""
end
end
```
---
# 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 blongs to many
## 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 `SuperUserResource` should receive the underlying `model_class` so Avo knows which model it represents.
```ruby{4}
class SuperUserResource < Avo::BaseResource
self.title = :name
self.includes = []
self.model_class = ::SuperUser
field :id, as: :id
field :name, as: :text
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 `PersonResource` to list all the records, but when your user clicks on a person, you want to use the inherited resources (`SiblingResource` and `SpouseResource`) 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 `PersonResource` 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: true` Use it on a `has_many` field. On the `PersonResource` 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
## Overview
On the `Index` and `Show` views, Avo will generate a link to the associated record containing the `@title` value.
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{12}
class CommentResource < Avo::BaseResource
self.title = :id
field :id, as: :id
field :body, as: :textarea
field :excerpt, as: :text, show_on: :index, as_description: true do |model|
ActionView::Base.full_sanitizer.sanitize(model.body).truncate 60
rescue
""
end
field :commentable, as: :belongs_to, polymorphic_as: :commentable, types: [::Post, ::Project]
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{16-17}
class CommentResource < Avo::BaseResource
self.title = :id
field :id, as: :id
field :body, as: :textarea
field :excerpt, as: :text, show_on: :index, as_description: true do |model|
ActionView::Base.full_sanitizer.sanitize(model.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
```
## 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{7}
class CommentResource < Avo::BaseResource
self.title = :id
field :id, as: :id
field :body, as: :textarea
field :user, as: :belongs_to, searchable: true
end
```
`searchable` works with `polymorphic` `belongs_to` associations too.
```ruby{7}
class CommentResource < Avo::BaseResource
self.title = :id
field :id, as: :id
field :body, as: :textarea
field :commentable, as: :belongs_to, polymorphic_as: :commentable, types: [::Post, ::Project], searchable: true
end
```
:::info
Avo uses the search feature behind the scenes, so **make sure the target resource has the `search_query` option configured**.
:::
```ruby
# app/avo/resources/post_resource.rb
class PostResource < Avo::BaseResource
self.search_query = -> do
scope.ransack(id_eq: params[:q], name_cont: params[:q], body_cont: params[:q], m: "or").result(distinct: false)
end
end
# app/avo/resources/project_resource.rb
class ProjectResource < Avo::BaseResource
self.search_query = -> do
scope.ransack(id_eq: params[:q], name_cont: params[:q], country_cont: params[:q], m: "or").result(distinct: false)
end
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_resource.rb
class PostResource < Avo::BaseResource
field :user, as: :belongs_to, attach_scope: -> { query.non_admins }
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{11}
class CommentResource < Avo::BaseResource
self.title = :id
field :id, as: :id
field :body, as: :textarea
field :commentable,
as: :belongs_to,
polymorphic_as: :commentable,
types: [::Post, ::Project],
allow_via_detaching: true
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
## Show on edit screens
By default, `has_and_belongs_to_many` is only visible on the `Show` page. If you want to enable it on the `Edit` page, too, you need to add the `show_on: :edit` option.
:::warning
Adding associations on the `New` screen is not currently supported. The association needs some information from the parent record that hasn't been created yet (because the user is on the `New` screen).
:::
You may use the redirect helpers to have the following flow:
1. User is on the `New` view. They can't see the association panels yet.
1. User creates the record.
1. They get redirected to the `Show`/`Edit` view, where they can see the association panels.
1. User attaches associations.
---
# 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
## Search query scope
If the resource used for the `has_many` association has the `search_query` block configured, 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 = -> do
if params[:via_association] == 'has_many'
scope.ransack(id_eq: params[:q], m: "or").result(distinct: false).order(name: :asc)
else
scope.ransack(id_eq: params[:q], m: "or").result(distinct: false)
end
end
```
## Has Many Through
The `HasMany` association also supports the `:through` option.
```ruby{3}
field :members,
as: :has_many,
through: :memberships
```
## Show on edit screens
By default, `has_and_belongs_to_many` is only visible on the `Show` page. If you want to enable it on the `Edit` page, too, you need to add the `show_on: :edit` option.
:::warning
Adding associations on the `New` screen is not currently supported. The association needs some information from the parent record that hasn't been created yet (because the user is on the `New` screen).
:::
You may use the redirect helpers to have the following flow:
1. User is on the `New` view. They can't see the association panels yet.
1. User creates the record.
1. They get redirected to the `Show`/`Edit` view, where they can see the association panels.
1. User attaches associations.
## 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_resource.rb
class UserResource < Avo::BaseResource
# Before v2.5.0
field :comments, as: :has_many, scope: -> { approved }
end
# app/avo/resources/user_resource.rb
class UserResource < Avo::BaseResource
# After v2.5.0
field :comments, as: :has_many, scope: -> { query.approved }
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.
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.
---
# Has And Belongs To Many
The `HasAndBelongsToMany` association works similarly to `HasMany`.
```ruby
field :users, as: :has_and_belongs_to_many
```
## Options
## Search query scope
If the resource used for the `has_many` association has the `search_query` block configured, 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 = -> do
if params[:via_association] == 'has_many'
scope.ransack(id_eq: params[:q], m: "or").result(distinct: false).order(name: :asc)
else
scope.ransack(id_eq: params[:q], m: "or").result(distinct: false)
end
end
```
## Show on edit screens
By default, `has_and_belongs_to_many` is only visible on the `Show` page. If you want to enable it on the `Edit` page, too, you need to add the `show_on: :edit` option.
:::warning
Adding associations on the `New` screen is not currently supported. The association needs some information from the parent record that hasn't been created yet (because the user is on the `New` screen).
:::
You may use the redirect helpers to have the following flow:
1. User is on the `New` view. They can't see the association panels yet.
1. User creates the record.
1. They get redirected to the `Show`/`Edit` view, where they can see the association panels.
1. User attaches associations.
### 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_resource.rb
class UserResource < Avo::BaseResource
# Before v2.5.0
field :comments, as: :has_many, scope: -> { approved }
end
# app/avo/resources/user_resource.rb
class UserResource < Avo::BaseResource
# After v2.5.0
field :comments, as: :has_many, scope: -> { query.approved }
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.
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.
---
# 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 MyDashboard < Avo::Dashboards::BaseDashboard
self.id = 'my_dashboard'
self.name = 'Dashy'
self.description = 'The first dashbaord'
self.grid_cols = 3
card ExampleMetric
card ExampleAreaChart
card ExampleScatterChart
card PercentDone
card AmountRaised
card ExampleLineChart
card ExampleColumnChart
card ExamplePieChart
card ExampleBarChart
divider label: "Custom partials"
card ExampleCustomPartial
card MapCard
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
All cards have a few base settings and a few custom ones.
### 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 dashboard grid. They can have values from `1` to `6`.
```ruby{2-7}
class UsersMetric < Avo::Dashboards::MetricCard
self.id = 'users_metric'
self.label = 'Users count'
self.description = 'Users description'
self.cols = 1
self.rows = 1
self.display_header = true
end
```
### 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.
```ruby{4,5}
class UsersMetric < Avo::Dashboards::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 this dashboard is something you keep on the big screen, you must 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 UsersMetric < Avo::Dashboards::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 UsersMetric < Avo::Dashboards::MetricCard
self.id = 'users_metric'
self.display_header = false
end
```
### Override card options 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 `options` attribute. It allows you to send arbitrary options to the card from the parent.
```ruby{6-8}
class Dashy < Avo::Dashboards::BaseDashboard
self.id = "dashy"
self.name = "Dashy"
card UsersCount
card UsersCount, options: {
active_users: true
}
end
```
Now we can pick up that option in the card and update the query accordingly.
```ruby{9-11}
class UsersCount < Avo::Dashboards::MetricCard
self.id = "users_metric"
self.label = "Users count"
# You have access to context, params, range, current dashboard, and current card
def query
scope = User
if options[: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`, and `refresh_every` options from the parent declaration.
```ruby{7-11}
class Dashy < Avo::Dashboards::BaseDashboard
self.id = "dashy"
self.name = "Dashy"
card UsersCount
card UsersCount,
label: "Active users",
description: "Active users count",
cols: 2,
rows: 2,
refresh_every: 2.minutes,
options: {
active_users: true
}
end
```
## Cards visibility
It's common to show the same dashboard 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-6}
class UsersCount < Avo::Dashboards::MetricCard
self.id = "users_metric"
self.label = "Users count"
self.visible = -> do
# You have access to context, params, parent (the current dashboard), and current card
true
end
def query
result User.count
end
end
```
You may also control the visibility from the dashboard class.
```ruby
class Dashy < Avo::Dashboards::BaseDashboard
self.name = "Dashy"
card UsersCount, visible: -> { true }
end
```
## Card types
You can add three types of cards to your dashboard: `metric`, `chartkick`, and `partial`.
### Metric card
The metric card is your friend when you only need to display a simple significant number on your dashboard. Generate one run `bin/rails g avo:card:metric users_metric`.
#### Calculate results
To calculate your result, you may use the `query` method. After you run your query, use the `result` method to store the value that will be 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` (the current dashboard the card is on), and current `card`.
```ruby{13-34,36}
class UsersMetric < Avo::Dashboards::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, 30, 60, 365, 'TODAY', 'MTD', 'QTD', 'YTD', '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 UsersMetric < Avo::Dashboards::MetricCard
self.id = 'users_metric'
self.prefix = '$'
self.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:chartkick users_chart`.
```ruby
class UserSignups < Avo::Dashboards::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 chart type. 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).
### Partial card
You can use a partial card to add custom content to a card. Generate one by running `bin/rails g avo:card:partial custom_card`. That will create the card class and the partial for it.
```ruby{5}
class ExampleCustomPartial < Avo::Dashboards::PartialCard
self.id = "users_custom_card"
self.cols = 1
self.rows = 4
self.partial = "avo/cards/custom_card"
# self.display_header = true
end
```
You may 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 MapCard < Avo::Dashboards::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
```
## Dividers
You may want to separate the cards. You can use dividers to do that.
```ruby{16}
class Dashy < Avo::Dashboards::BaseDashboard
self.id = 'dashy'
self.name = 'Dashy'
self.description = 'The first dashbaord'
self.grid_cols = 3
card ExampleMetric
card ExampleAreaChart
card ExampleScatterChart
card PercentDone
card AmountRaised
card ExampleLineChart
card ExampleColumnChart
card ExamplePieChart
card ExampleBarChart
divider label: "Custom partials"
card ExampleCustomPartial
card MapCard
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
class Dashy < Avo::Dashboards::BaseDashboard
self.name = "Dashy"
card UsersCount, visible: -> {
# You have access to context, params, parent (the current dashboard)
true
}
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 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
card UsersCount
end
```
## Dashboards authorization
You can set authorization rules for dashboards using the `authorize` block.
```ruby{3-6}
class Dashy < Avo::Dashboards::BaseDashboard
self.id = 'dashy'
self.authorization = -> do
# You have access to current_user, params, request, context, adn view_context.
current_user.is_admin?
end
end
```
---
# Cards
Cards are one way of quickly adding custom content for your users.
You can add three types of cards to your dashboard: `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 dashboard grid. They can have values from `1` to `6`.
```ruby{2-7}
class UsersMetric < Avo::Dashboards::MetricCard
self.id = 'users_metric'
self.label = 'Users count'
self.description = 'Users description'
self.cols = 1
self.rows = 1
self.display_header = true
end
```
#### 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 UsersMetric < Avo::Dashboards::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 this dashboard 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 UsersMetric < Avo::Dashboards::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 UsersMetric < Avo::Dashboards::MetricCard
self.id = 'users_metric'
self.display_header = false
end
```
### Metric card
The metric card is your friend when you only need to display a simple big number on your dashboard. To generate one run `bin/rails g avo:card:metric users_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` (the current dashboard the card is on), and current `card`.
```ruby{23-47,36}
class UsersMetric < Avo::Dashboards::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 UsersMetric < Avo::Dashboards::MetricCard
self.id = 'users_metric'
self.prefix = '$'
self.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:chartkick users_chart`.
```ruby
class UserSignups < Avo::Dashboards::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).
### Partial card
You can use a partial card to add custom content to a card. Generate one by running `bin/rails g avo:card:partial custom_card`. That will create the card class and the partial for it.
```ruby{5}
class ExampleCustomPartial < Avo::Dashboards::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 MapCard < Avo::Dashboards::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
```
---
# 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'`.
Since Avo 2.32.6 the `app_name` option is 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.
Table view
Grid view
## 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 side
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{2}
Avo.configure do |config|
config.resource_controls_placement = :left
end
```
## Container width
```ruby{2-3}
Avo.configure do |config|
config.full_width_index_view = false
config.full_width_container = false
end
```
Avo's default main content is constrained to a regular [Tailwind CSS container](https://tailwindcss.com/docs/container). If you have a lot of content or prefer to display it full-width, you have two options.
### Display the `Index` view full-width
Using `full_width_index_view: true` tells Avo to display the **Index** view full-width.
### Display all views full-width
Using `full_width_container: true` tells Avo to display all views full-width.
## 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{2-8}
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::App.context` object.
## Eject views
If you want to change one of Avo's built-in views, you can eject it, update it and use it in your admin.
### Prepared templates
We prepared a few templates to make it.
`bin/rails generate avo:eject :logo` will eject the `_logo.html.erb` partial.
```
▶ bin/rails generate avo:eject :logo
Running via Spring preloader in process 20947
create app/views/avo/logo/_logo.html.erb
```
A list of prepared templates:
- `:logo` ➡️ `app/views/avo/partials/_logo.html.erb`
- `:head` ➡️ `app/views/avo/partials/_head.html.erb`
- `:header` ➡️ `app/views/avo/partials/_header.html.erb`
- `:footer` ➡️ `app/views/avo/partials/_footer.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.
#### Footer
The `_footer.html.erb` partial enables you to customize the footer of your admin.
#### 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 app/views/layouts/avo/application.html.erb
create app/views/layouts/avo/application.html.erb
```
:::warning
Once ejected, the views will not receive updates on new Avo releases. You must maintain them yourself.
:::
## Breadcrumbs
By default, Avo ships with breadcrumbs enabled.
You may disable them using the `display_breadcrumbs` configuration option.
```ruby{2}
Avo.configure do |config|
config.display_breadcrumbs = false
end
```
The first item on the breadcrumb is **Home** with the `root_path` URL. You can customize that using the `set_initial_breadcrumbs` block.
```ruby{2-5}
Avo.configure do |config|
config.set_initial_breadcrumbs do
add_breadcrumb "Casa", root_path
add_breadcrumb "Something else", something_other_path
end
end
```
Avo uses the [breadcrumbs_on_rails](https://github.com/weppos/breadcrumbs_on_rails) gem under the hood.
### 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 "Custom tool"
end
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.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, at: Avo.configuration.root_path # 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 `resolve_query_scope` for multiple records and `resolve_find_scope` when fetching one record.
### Custom scope for `Index` page
Using `resolve_query_scope` you tell Avo how to fetch the records for the `Index` view.
```ruby
class UserResource < Avo::BaseResource
self.resolve_query_scope = ->(model_class:) do
model_class.order(last_name: :asc)
end
end
```
### Custom scope for `Show` and `Edit` pages
:::warning
The `resolve_find_scope` method is deprecated in favor of `find_record_method` (below).
:::
:::details If you're following the `friendly_id` example, you must also add the `friendly_id` configuration to the model definition.
```ruby
class User < ApplicationRecord
extend FriendlyId
friendly_id :name, use: :slugged
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 `UserResource` so it knows how to search for that model.
::: code-group
```ruby [app/avo/resources/user_resource.rb]
class PostResource < Avo::BaseResource
self.find_record_method = ->(model_class:, id:, params:) do
# If the id is an integer use the classic `find` method.
# But if it's not an integer, search for that post by the slug.
id.to_i == 0 ? model_class.find_by_slug(id) : model_class.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_resource.rb]
class UserResource < Avo::BaseResource
self.find_record_method = ->(model_class:, id:, params:) do
# We have to add .friendly to the query
model_class.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
```
## Disable features
You might want to disable some Avo features. You can do that using the `disabled_features` option.
```ruby{3}
# config/initializers/avo.rb
Avo.configure do |config|
config.disabled_features = [:global_search]
end
```
After this setting, the global search will be hidden for users.
Supported options:
- `global_search`
## 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.resource_default_view = :edit` 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.resource_default_view = :edit
end
```

---
# Grid view
Some resources are best displayed in a grid view. We can do that with Avo using a `cover`, a `title`, and a `body`.
## Enable grid view
To enable grid view for a resource, you need to add the `grid` block. That will add the view switcher to the **Index** view.
```ruby
class PostResource < Avo::BaseResource
# ...
grid do
cover :cover_photo, as: :file, link_to_resource: true
title :name, as: :text, required: true, link_to_resource: true
body :excerpt, as: :text
end
end
```
## Make 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{7}
class PostResource < Avo::BaseResource
self.default_view_type = :grid
end
```
## Fields configuration
Besides the regular `field` methods, you should add a new `grid` block configuring the grid fields. The main difference is that the fields are not declared using the `field` class method but three new ones `cover`, `title`, and `body`
```ruby{9-13}
class PostResource < Avo::BaseResource
self.default_view_type = :grid
field :id, as: :id
field :name, as: :text, required: true
field :body, as: :textarea
field :cover_photo, as: :file, is_image: true
grid do
cover :cover_photo, as: :file, is_image: true
title :name, as: :text
body :body, as: :textarea
end
end
```
That will render the `Post` resource index view as a **Grid view** using the selected fields. Avo will also display a button to toggle between the view types `:grid` and `:table`.
These fields take the same options as those in the `fields` method, so you can configure them however you want.
For example, in the **Grid view**, you might want to truncate the `:body` to a certain length and use an external image for the cover you compute on the fly. And also, render the `:cover` and the `:title` fields as links to that resource with `link_to_resource: true`.
```ruby
grid do
cover :logo, as: :external_image, link_to_resource: true do |model|
if model.url.present?
"//logo.clearbit.com/#{URI.parse(model.url).host}?size=180"
end
end
title :name, as: :text, link_to_resource: true
body :excerpt, as: :text do |model|
begin
ActionView::Base.full_sanitizer.sanitize(model.body).truncate 130
rescue => exception
''
end
end
end
```
## Use a computed field for the `cover` field
A common use case is to have the assets stored on a separate model and would like to display an image from that related association.
```ruby
class Post < ApplicationRecord
has_many :post_assets
end
class PostAssets < ApplicationRecord
belongs_to :post
has_one_attached :image
end
```
Luckily, the `grid` display can be a computed field too
```ruby
grid do
cover :image, as: :file, is_image: true, link_to_resource: true do |model|
# we find the first asset association and use it's image attachment
model.post_assets.first.image
end
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 attribtue to a resource. That will add the view switcher to the view.
```ruby
class CityResource < 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
```
## 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 CityResource < Avo::BaseResource
self.default_view_type = :map
end
```
---
# 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-22}
# config/initializers/avo.rb
Avo.configure do |config|
config.main_menu = -> {
section "Resources", icon: "heroicons/outline/academic-cap" do
group "Academia" do
resource :course
resource :course_link
end
group "Blog", collapsable: true, collapsed: true do
dashboard :dashy
resource :post
resource :comment
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.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.main_menu = -> {
section I18n.t("avo.dashboards"), icon: "dashboards" do
dashboard :dashy, visible: -> { true }
dashboard :sales, visible: -> { true }
group "All dashboards", visible: false do
all_dashboards
end
end
section "Resources", icon: "heroicons/outline/academic-cap" do
group "Academia" do
resource :course
resource :course_link
end
group "Blog" do
resource :posts
resource :comments
end
group "Other" do
resource :fish
end
end
section "Tools", icon: "heroicons/outline/finger-print" do
all_tools
end
group do
link_to "Avo", path: "https://avohq.io"
link_to "Google", path: "https://google.com", target: :_blank
end
}
config.profile_menu = -> {
link_to "Profile", path: "/profile", icon: "user-circle"
}
end
```
## Menu item types
A few menu item types are supported `link_to`, `section`, `group`, `resource`, and `dashboard`. There are a few helpers too, like `all_resources`, `all_dashboards`, and `all_tools`.
### `all_` helpers
```ruby
section "App", icon: "heroicons/outline/beaker" do
group "Dashboards", icon: "dashboards" do
all_dashboards
end
group "Resources", icon: "resources" do
all_resources
end
group "All tools", icon: "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
For [`Section`](#section)s, you can use icons to make them look better. You can use some local ones that we used throughout the app and all [heroicons](https://heroicons.com/) designed by [Steve Schoger](https://twitter.com/steveschoger). In addition, you can use the `solid` or `outline` versions. We used the `outline` version throughout the app.
```ruby
section "Resources", icon: "heroicons/outline/academic-cap" do
resource :course
end
section "Resources", icon: "heroicons/solid/finger-print" do
resource :course
end
section "Resources", icon: "heroicons/outline/adjustments" do
resource :course
end
```
### Icons on resource, dashboard, and link_to items
Since [2.36](https://avohq.io/releases/2.36) you can add icons to other menu items like `resource`, `dashboard`, and `link_to`.
```ruby
link_to "Avo", "https://avohq.io", icon: "globe"
```
## Collapsable sections and groups
When you have a lot of items they can take up a lot of vertical space. You can choose to make those sidebar sections collapsable by you or your users.
```ruby
section "Resources", icon: "resources", collapsable: true do
resource :course
end
```
That will add the arrow icon next to the section to indicate it's collapsable. So when your users collapse and expand it, their choice will be stored in Local Storage and remembered in that browser.
### Default collapsed state
You can however, set a default collapsed state using the `collapsed` option.
```ruby
section "Resources", icon: "resources", collapsable: true, collapsed: true do
resource :course
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, wnat 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 :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 leverages [ransack's](https://github.com/activerecord-hackery/ransack) powerful query language.
:::info
While we show you examples using `ransack`, you can use other search engines, so `ransack` is not mandatory.
:::
:::warning
If you're using the authorization feature, ensure [you authorize the action](#authorize-search).
:::
First, you need to add `ransack` as a dependency to your app (breaking change from Avo v1.10).
```ruby
# Gemfile
gem 'ransack'
```
## Enable search for a resource
To enable search for a resource, you need to add the `search_query` class variable to the resource file.
```ruby{3-5}
class UserResource < Avo::BaseResource
self.title = :name
self.search_query = -> do
scope.ransack(id_eq: params[:q], first_name_cont: params[:q], last_name_cont: params[:q], m: "or").result(distinct: false)
end
# fields go here
end
```
The `search_query` block passes over the `params` object that holds the `q` param, the actual query string. It also provides the `scope` variable on which you run the query. That ensures that the authorization scopes have been appropriately applied.
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
Since Avo 2.29 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
```
## Configure the search result
### Label
By default, the search results will be displayed as text. The text label will be the title column you previously configured.
You may configure that to be something more complex using the `as_label` option. That will take the final value of that field and display it as the label of the search result.
```ruby{9-11}
class PostResource < Avo::BaseResource
self.title = :name
self.search_query = -> do
scope.ransack(id_eq: params[:q], m: "or").result(distinct: false)
end
field :id, as: :id
field :name, as: :text, required: true, as_label: true
field :complex_name, as: :text, hide_on: :all, as_label: true do |model|
"[#{model.id}]#{model.name}"
end
end
```
Notice the `hide_on: :all` option used to hide the computed `complex_name` attribute from the rest of the views. That is because you **may or may not** want to show that attribute in other views.
### Description
You might want to show more than just the title in the search result. Avo provides the `as_description` option to add some more information.
```ruby{12-16}
class PostResource < Avo::BaseResource
self.title = :name
self.search_query = -> do
scope.ransack(id_eq: params[:q], m: "or").result(distinct: false)
end
field :id, as: :id
field :name, as: :text, required: true, as_label: true
field :complex_name, as: :text, hide_on: :all, as_label: true do |model|
"[#{model.id}]#{model.name}"
end
field :excerpt, as: :text, as_description: true do |model|
ActionView::Base.full_sanitizer.sanitize(model.body).truncate 130
rescue
""
end
end
```
### Avatar
* Search Avatar is a [Pro feature](https://avohq.io/purchase/pro).
You may improve the results listing by adding an avatar to each search result. You do that by using the `as_avatar` attribute. This attribute has three options `:square`, `:rounded` or `:circle`. That influences the final roundness of the avatar.
```ruby{17}
class PostResource < Avo::BaseResource
self.title = :name
self.search_query = -> do
scope.ransack(id_eq: params[:q], m: "or").result(distinct: false)
end
field :id, as: :id
field :name, as: :text, required: true, as_label: true
field :complex_name, as: :text, hide_on: :all, as_label: true do |model|
"[#{model.id}]#{model.name}"
end
field :excerpt, as: :text, as_description: true do |model|
ActionView::Base.full_sanitizer.sanitize(model.body).truncate 130
rescue
""
end
field :cover_photo, as: :file, is_image: true, as_avatar: :rounded
end
```
### Header Help Text
You may improve the results listing header by adding a piece of text highlighting the fields you are looking for or any other instruction for the user. You do that by using the `search_query_help` attribute. This attribute takes a string and appends it to the title of the resource.
```ruby{6}
class PostResource < Avo::BaseResource
self.title = :name
self.search_query = -> do
scope.ransack(id_eq: params[:q], m: "or").result(distinct: false)
end
self.search_query_help = "- search by id"
field :id, as: :id
end
```
## Resource search
When a resource has the `search_query` attribute, a new search input will be displayed on the `Index` view.

## Global search
Avo also has a global search feature. It will search through all the resources that have the `search_query` attribute present.
You open the global search input by clicking the trigger on the navbar or by using the CMD + K keyboard shortcut (Ctrl + K on windows).
### Hide the global search
If you, by any chance, want to hide the global search, you can do so using this setting 👇
```ruby{3}
# config/initializers/avo.rb
Avo.configure do |config|
config.disabled_features = [:global_search]
end
```
## Hide a resource from the global search
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_from_global_search = true`.
```ruby{8}
class TeamMembershipResource < Avo::BaseResource
self.title = :id
self.includes = [:user, :team]
self.visible_on_sidebar = false
self.search_query = -> do
scope.ransack(id_eq: params[:q], m: "or").result(distinct: false)
end
self.hide_from_global_search = true
field :id, as: :id
field :user, as: :belongs_to
field :team, as: :belongs_to
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{8}
class OrderResource < Avo::BaseResource
self.search_query = -> do
if params[:global]
# Perform global search
scope.ransack(id_eq: params[:q], m: "or").result(distinct: false)
else
# Perform resource search
scope.ransack(id_eq: params[:q], details_cont: params[:q], m: "or").result(distinct: false)
end
end
end
```
## Search result path
By default, when a user clicks on a search result, they will be redirected to that record, but you can change that using the `search_result_path` option.
```ruby
class CityResource < Avo::BaseResource
self.search_result_path = -> {
# Return any path here. You have access to the search `record` the user clicked on.
avo.resources_city_path record, custom: "yup"
}
end
```
---
# Filters
Filters allow you to better scope the index queries for records you are looking for.
## Defining filters
Avo has two types of filters available at the moment [Boolean filter](#boolean-filter) and [Select filter](#select-filter).
### Filter values
Because the filters get serialized back and forth, the final `value`/`values` in the `apply` method will be stringified or have the keys stringified if they are hashes. You can declare them as regular hashes (with the keys symbolized) in the `options` method, but they will get stringified in the end.
## Boolean Filter
You generate one running `bin/rails generate avo:filter featured_filter`, creating a filter configuration file.
```ruby
class FeaturedFilter < 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 FeaturedFilter < 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 Filter
Select filters are similar to Boolean ones. For example, you generate one running `rails generate avo:filter published_filter --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 PublishedFilter < 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 PublishedFilter < 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
```
## Multiple select filter
You may also use a multiple select filter.
```ruby
class PostStatusFilter < 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 AuthorFilter < 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 = {
# "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
```
## Text Filter
You can add complex text filters to Avo by running `rails generate avo:filter name_filter --text`.
```ruby
class NameFilter < 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
```
## Default value
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.
## Registering filters
To add a filter to one of your resources, you need to declare it on the resource using the `filter` method to which you pass the filter class.
```ruby{8}
class PostResource < Avo::BaseResource
self.title = :name
self.search = :id
field :id, as: :id
# other fields
filter PublishedFilter
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 `CourseResource` 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{3-4}
# app/avo/resources/course_resource.rb
class CourseResource < Avo::BaseResource
filter CourseCountryFilter
filter CourseCityFilter
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_filter.rb
class CourseCountryFilter < 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 (`CourseCountryFilter`) and formats them to a `Hash`.
```ruby{6,10}
# app/avo/filters/course_city_filter.rb
class CourseCityFilter < 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 = {
# "CourseCountryFilter" => {
# "USA" => true,
# "Japan" => true,
# "Spain" => false,
# "Thailand" => false,
# }
# }
def countries
if applied_filters["CourseCountryFilter"].present?
# Fetch the value of the countries filter
applied_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 `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{13-28}
# app/avo/filters/course_city_filter.rb
class CourseCityFilter < 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 = {
# "CourseCountryFilter" => {
# "USA" => true,
# "Japan" => true,
# "Spain" => false,
# "Thailand" => false,
# }
# }
def react
# Check if the user selected a country
if applied_filters["CourseCountryFilter"].present? && applied_filters["CourseCityFilter"].blank?
# Get the selected countries, get their cities, and select the first one.
selected_countries = applied_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 = {
# "CourseCountryFilter" => {
# "USA" => true,
# "Japan" => true,
# "Spain" => false,
# "Thailand" => false,
# }
# }
def countries
if applied_filters["CourseCountryFilter"].present?
# Fetch the value of the countries filter
applied_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["CourseCountryFilter"].present? && applied_filters["CourseCityFilter"].blank?
# Get the selected countries, get their cities, and select the first one.
selected_countries = applied_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["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["CourseCityFilter"].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_filter.rb
class CourseCityFilter < 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.
```ruby{2}
class CourseResource < Avo::BaseResource
self.keep_filters_panel_open = true
field :id, as: :id
field :name, as: :text
field :country, as: :select, options: Course.countries.map { |country| [country, country] }.to_h
field :city, as: :select, options: Course.cities.values.flatten.map { |country| [country, country] }.to_h
field :links, as: :has_many, searchable: true, placeholder: "Click to choose a link"
filter CourseCountryFilter
filter CourseCityFilter
end
```
## Visibility
You may want to manipulate your filter visibility on screens. You can do that using the `self.visible` attribute.
Inside the visible block you can acces the following variables:
```ruby
self.visible = -> do
# You have access to:
# block
# context
# current_user
# params
# parent_model
# parent_resource
# resource
# view
# view_context
end
```
## Filters 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{9-11}
class FishResource < Avo::BaseResource
self.title = :name
field :id, as: :id
field :name, as: :text
field :user, as: :belongs_to
field :type, as: :text, hide_on: :forms
filter NameFilter, arguments: {
case_insensitive: true
}
end
```
Now, the arguments can be accessed inside `NameFilter` ***`apply` method*** and on the ***`visible` block***!
```ruby{4-6,8-14}
class NameFilter < 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
### Standalone helpers
## Persistent filters
By default, when a user visits an view of a resource the filters payload will be empty, so they will be set on their [default values](#default-value).
---
# Actions
Avo actions allow you to perform specific tasks on one or more of your records.
For example, you might want to mark a user as active/inactive and optionally send a message that may be customized by the person that wants to run the action.
Once you attach an action to a resource using the `action` method, it will appear in the **Actions** dropdown. By default, actions appear on the `Index`, `Show`, and `Edit` views. Versions previous to 2.9 would only display the actions on the `Index` and `Show` views.
!Actions dropdown
:::info
Since version you may use the customizable controls feature to show the actions outside the dropdown.
:::
## Overview
You generate one running `bin/rails generate avo:action toggle_inactive`, creating an action configuration file.
```ruby
class ToggleInactive < Avo::BaseAction
self.name = 'Toggle inactive'
field :notify_user, as: :boolean, default: true
field :message, as: :text, default: 'Your account has been marked as inactive.'
def handle(**args)
models, fields, current_user, resource = args.values_at(:models, :fields, :current_user, :resource)
models.each do |model|
if model.active
model.update active: false
else
model.update active: true
end
# Optionally, you may send a notification with the message to that user from inside the action
UserMailer.with(user: model).toggle_inactive(fields["message"]).deliver_later
end
succeed 'Perfect!'
end
end
```
You may add fields to the action just as you do it in a resource. Adding fields is optional. You may have actions that don't have any fields attached.
```ruby
field :notify_user, as: :boolean
field :message, as: :textarea, default: 'Your account has been marked as inactive.'
```
:::warning Files authorization
If you're using the `file` field on an action and attach it to a resource that's using the authorization feature, please ensure you have the `upload_{FIELD_ID}?` policy method returning `true`. Otherwise, the `file` input might be hidden.
More about this on the authorization page.
:::
!Actions
The `handle` method is where the magic happens. That is where you put your action logic. In this method, you will have access to the selected `models` (if there's only one, it will be automatically wrapped in an array) and the values passed to the `fields`.
```ruby
def handle(**args)
models, fields = args.values_at(:models, :fields)
models.each do |model|
if model.active
model.update active: false
else
model.update active: true
end
# Optionally, you may send a notification with the message to that user.
UserMailer.with(user: model).toggle_inactive(fields["message"]).deliver_later
end
succeed 'Perfect!'
end
```
## Registering actions
To add an action to one of your resources, you need to declare it on the resource using the `action` method.
```ruby{8}
class UserResource < Avo::BaseResource
self.title = :name
self.search = [:id, :first_name, :last_name]
field :id, as: :id
# other fields
action ToggleActive
end
```
## Action responses
After an action runs, you may use several methods to respond to the user. For example, you may respond with just a message or with a message and an action.
The default response is to reload the page and show the _Action ran successfully_ message.
### Message responses
You will have four message response methods at your disposal `succeed`, `error`, `warn`, and `inform`. These will render the user green, red, orange, and blue alerts.
```ruby{4-7}
def handle(**args)
# Demo handle action
succeed "Success response ✌️"
warn "Warning response ✌️"
inform "Info response ✌️"
error "Error response ✌️"
end
```
:::warning
Since Avo 2.20 we deprecated the `fail` method in favor of `error`.
:::
### Run actions silently
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
def handle(**args)
# Demo handle action
redirect_to "/admin/some-tool"
silent
end
```
## Response types
After you notify the user about what happened through a message, you may want to execute an action like `reload` (default action) or `redirect_to`. You may use message and action responses together.
```ruby{14}
def handle(**args)
models = args[:models]
models.each do |model|
if model.admin?
error "Can't mark inactive! The user is an admin."
else
model.update active: false
succeed "Done! User marked as inactive!"
end
end
reload
end
```
The available action responses are:
## Customization
```ruby{2-6}
class TogglePublished < Avo::BaseAction
self.name = 'Mark inactive'
self.message = 'Are you sure you want to mark this user as inactive?'
self.confirm_button_label = 'Mark inactive'
self.cancel_button_label = 'Not yet'
self.no_confirmation = true
```
### Customize the message
You may update the `self.message` class attribute to customize the message if there are no fields present.
#### Callable message
Since version `2.21` you can pass a block to `self.message` where you have access to a baunch of variables.
```ruby
class ReleaseFish < Avo::BaseAction
self.message = -> {
# you have access to:
# - params
# - current_user
# - context
# - view_context
# - request
# - resource
# - record
"Are you sure you want to release the #{record.name}?"
}
end
```
### Customize the buttons
You may customize the labels for the action buttons using `confirm_button_label` and `cancel_button_label`.
### No confirmation actions
You will be prompted by a confirmation modal when you run an action. If you don't want to show the confirmation modal, pass in the `self.no_confirmation = true` class attribute. That will execute the action without showing the modal at all.
## Standalone actions
You may need to run actions that are not necessarily tied to a model. Standalone actions help you do just that. Add `self.standalone` to an existing action or generate a new one using the `--standalone` option (`bin/rails generate avo:action global_action --standalone`).
```ruby{3}
class DummyAction < Avo::BaseAction
self.name = "Dummy action"
self.standalone = true
def handle(**args)
fields, current_user, resource = args.values_at(:fields, :current_user, :resource)
# Do something here
succeed 'Yup'
end
end
```
## Actions visibility
You may want to hide specific actions on screens, like a standalone action on the `Show` screen. You can do that using the `self.visible` attribute.
```ruby{4}
class DummyAction < Avo::BaseAction
self.name = "Dummy action"
self.standalone = true
self.visible = -> { view == :index }
def handle(**args)
fields, current_user, resource = args.values_at(:fields, :current_user, :resource)
# Do something here
succeed 'Yup'
end
end
```
By default, actions are visible on the `Index`, `Show`, and `Edit` views, but you can enable them on the `New` screen, too (from version 2.9.0).
```ruby
self.visible = -> { view == :new }
# Or use this if you want them to be visible on any view
self.visible = -> { true }
```
Inside the visible block you can access the following variables:
```ruby
self.visible = -> do
# You have access to:
# block
# context
# current_user
# params
# parent_resource (can access the parent_model by parent_resource.model)
# resource (can access the model by resource.model)
# view
# view_context
end
```
## Actions authorization
:::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
:::
## Actions arguments
Actions can have different behaviors according to their host resource. In order to achieve that, arguments must be passed like on the example below:
```ruby{9-11}
class FishResource < Avo::BaseResource
self.title = :name
field :id, as: :id
field :name, as: :text
field :user, as: :belongs_to
field :type, as: :text, hide_on: :forms
action DummyAction, arguments: {
special_message: true
}
end
```
Now, the arguments can be accessed inside `DummyAction` ***`handle` method*** and on the ***`visible` block***!
```ruby{4-6,8-14}
class DummyAction < Avo::BaseAction
self.name = "Dummy action"
self.standalone = true
self.visible = -> do
arguments[:special_message]
end
def handle(**args)
if arguments[:special_message]
succeed "I love 🥑"
else
succeed "Success response ✌️"
end
end
end
```
---
# Localization (i18n)
Avo leverages Rails' powerful I18n translations module. When you run `bin/rails avo:install`, Rails will generate for you the `avo.en.yml` translation file. This file 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_resource.rb
class UserResource < 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{7}
# app/avo/resources/project_resource.rb
class ProjectResource < Avo::BaseResource
self.title = :name
field :id, as: :id
# ... other fields
field :files, as: :files, translation_key: 'avo.field_translations.file'
end
```
```yaml{6-10}
# avo.es.yml
es:
avo:
dashboard: 'Dashboard'
# ... other translation keys
field_translations:
file:
zero: 'archivos'
one: 'archivo'
other: 'archivos'
```
## Setting the locale
Setting the locale for Avo is simple. Just use the `config.locale = :en` config attribute. Default is `nil` and will fall back to whatever you have configured 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 the whole app, you can use the `set_locale=pt-BR` param. That will set the default locale 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 while you navigate Avo. Remove that param when you want to go back to your configured `default_locale`.
Check out our guide for multilingual records.
## Re-generate the locale
When updating Avo, please run `bin/rails generate avo:locales` to re-generate the locales file.
## FAQ
If you try to localize your resources and fields and it doesn't seem to work, please be aware of the following.
### Advanced localization is a Pro feature
Localizing strings in Avo will still work using Rails' `I18n` mechanism, but localizing files and resources require a `Pro` or above license.
The reasoning is that deep localization is a more advanced feature that usually falls in the commercial realm. So if you create commercial products or apps for clients and make revenue using Avo, we'd love to get your support to maintain it and ship new features going forward.
### 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'
```
---
# Branding
```ruby
Avo.configure do |config|
config.branding = {
colors: {
background: "248 246 242",
100 => "#C5F1D4",
400 => "#3CD070",
500 => "#30A65A",
600 => "#247D43",
},
chart_colors: ['#FFB435', "#FFA102", "#CC8102", '#FFB435', "#FFA102", "#CC8102"],
logo: "/avo-assets/logo.png",
logomark: "/avo-assets/logomark.png",
placeholder: "/avo-assets/placeholder.svg",
favicon: "/avo-assets/favicon.ico"
}
end
```
Using the branding feature, you can easily change the look of your app. You tweak it inside your `avo.rb` initializer in the `branding` attribute. It takes a hash with a few properties.
## Configure brand color
To customize the primary color of Avo, you must configure the `colors` key with four color variants. `100` for color hints, `500` for the base primary color, and `400` and `600` values for highlights.
```ruby{4-8}
Avo.configure do |config|
config.branding = {
colors: {
background: "248 246 242",
100 => "#C5F1D4",
400 => "#3CD070",
500 => "#30A65A",
600 => "#247D43",
}
}
end
```
You may also customize the color of Avo's background using the `background` key.



:::info
The color format can be hex (starting with `#`) or rgb (three groups split by a space, not a comma).
:::
Avo uses [Tailwinds color system](https://tailwindcss.com/docs/customizing-colors). You can generate your own using the tools below.
- [Palettte](https://palettte.app/)
- [ColorBox](https://colorbox.io/)
- [TailwindInk](https://tailwind.ink/)
Here are a few for you to choose from.
```ruby
config.branding = {
colors: {
# BLUE
100 => "#CEE7F8",
400 => "#399EE5",
500 => "#0886DE",
600 => "#066BB2",
# RED
100 => "#FACDD4",
400 => "#F06A7D",
500 => "#EB3851",
600 => "#E60626",
# GREEN
100 => "#C5F1D4",
400 => "#3CD070",
500 => "#30A65A",
600 => "#247D43",
# ORANGE
100 => "#FFECCC",
400 => "#FFB435",
500 => "#FFA102",
600 => "#CC8102",
}
}
```
## Customize the chart colors
For your dashboard, you can further customize the colors of the charts. You can do that using the `chart_colors` option. Pass in an array of colors, and Avo will do the rest.
```ruby
Avo.configure do |config|
config.branding = {
chart_colors: ['#FFB435', "#FFA102", "#CC8102", '#FFB435', "#FFA102", "#CC8102"],
}
end
```

:::warning
The chart colors should be hex colors. They are forwarded to chart.js
:::
## Customize the logo
We want to make it easy to change the logo for your app, so we added the `logo` and `logomark` options to the branding feature.
The `logo` should be the "big" logo you want to display on the desktop version of your app, and `logomark` should be a squared-aspect image that Avo displays on the mobile version.

## Customize the missing image placeholder
When you view the data in the view in a grid, when the `cover` field does not have an image, an avocado is going to be displayed instead as a placeholder.
You might want to change that to something else using the `placeholder` option.
```ruby
Avo.configure do |config|
config.branding = {
placeholder: "/YOUR_PLACEHOLDER_IMAGE.jpg",
}
end
```
## Customize the favicon
We want to make it easy to change the logo for your app, so we added the `favicon` option to the branding feature.
Overwrite it using an `.ico` file.
---
# 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::Engine => Avo.configuration.root_path
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 ProgressBarField < Avo::Fields::BaseField
def initialize(name, **args, &block)
super(name, **args, &block)
end
end
```
Now you can use your field like so:
```ruby{6}
# app/avo/resources/progress_bar_field.rb
class ProjectResource < Avo::BaseResource
self.title = :name
field :id, as: :id, link_to_resource: true
field :progress, as: :progress_bar
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).
## 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 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,23}
# app/avo/fields/progress_bar_field.rb
class 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/progress_bar_field.rb
class ProjectResource < Avo::BaseResource
self.title = :name
field :id, as: :id, link_to_resource: true
field :progress, as: :progress_bar, step: 10, display_value: true, value_suffix: "%"
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 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 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 %>
class="hidden" <% end %> data-hidden-input-target="content">
<%= @field.value %>
<% end %>
```
---
# 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 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::App.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::App.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_resource.rb
class PostResource < Avo::BaseResource
tool PostInfo, show_on: :edit
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 `FishResource`, 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{2}
class FishResource < Avo::BaseResource
tool FishInformation, show_on: :forms
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 %>
```
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 `FishResource`. 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 FishResource < Avo::BaseResource
self.extra_params = [:fish_type, :something_else, properties: [], information: [:name, :history]]
tool FishInformation, show_on: :forms
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.
---
# 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 CourseResource < Avo::BaseResource
self.stimulus_controllers = "course-resource"
end
```
You can add more and separate them by a space character.
```ruby
class CourseResource < 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 ShowCurrentTime < Avo::BaseAction
self.stimulus_controllers = "city-in-country"
end
```
You can add more and separate them by a space character.
```ruby
class 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
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.
Pass the `style` and `classes` attributes as strings, and the `data` attribute a Hash.
```ruby{4-11}
field :name, as: :text, html: {
edit: {
wrapper: {
style: "background: red; text: white;" # string
classes: "absolute h-[41px] w-full" # string
data: {
action: "input->resource-edit#toggle",
resource_edit_toggle_target_param: "skills_tags_wrapper",
} # Hash
}
}
}
```
### Declare the fields from the outside in
When you add these attributes, you need to think from the outside in. So first the `view` (`index`, `show`, or `edit`), next the element to which you add the attribute (`wrapper`, `label`, `content` or `input`), and then the attribute `style`, `classes`, or `data`.
**The `edit` value will be used for both the `Edit` and `New` views.**
There are two notations through which you can attach the attributes; `object` or `block` notation.
## The `object` notation
This is the simplest way of attaching the attribute. You usually use this when you want to add _static_ content and params.
```ruby{3-9}
field :has_skills,
as: :boolean,
html: {
edit: {
wrapper: {
classes: "hidden"
}
}
}
```
In this example, we're adding the `hidden` class to the field wrapper on the `Edit` and `New` views.
## The `block` notation
You can use the' block' notation if you need to do a more complex transformation to add your attributes. You'll have access to the `params`, `current_user`, `record`, and `resource` variables. It's handy in multi-tenancy scenarios and when you need to scope out the information across accounts.
```ruby{3-18}
field :has_skills,
as: :boolean,
html: -> do
edit do
wrapper do
classes do
"hidden"
end
data do
if current_user.admin?
{
action: "click->admin#do_something_admin"
}
else
{
record: record,
resource: resource,
}
end
end
end
end
end
```
For the `data`, `style`, and `classes` options, you may use the `method` notation alongside the block notation for simplicity.
```ruby{6,7}
field :has_skills,
as: :boolean,
html: -> do
edit do
wrapper do
classes("hidden")
data({action: "click->admin#do_something_admin"})
end
end
end
```
## Where are the attributes added?
You can add attributes to the wrapper element for the `index`, `show`, or `edit` blocks.
## Index field wrapper
```ruby
field :name, as: :text, html: {
index: {
wrapper: {}
}
}
```
## Show field wrapper
```ruby
field :name, as: :text, html: {
show: {
wrapper: {}
}
}
```
## Show label target
```ruby
field :name, as: :text, html: {
show: {
label: {}
}
}
```
## Show content target
```ruby
field :name, as: :text, html: {
show: {
content: {}
}
}
```
## Edit field wrapper
```ruby
field :name, as: :text, html: {
edit: {
wrapper: {}
}
}
```
## Edit label target
```ruby
field :name, as: :text, html: {
edit: {
label: {}
}
}
```
## Edit content target
```ruby
field :name, as: :text, html: {
edit: {
content: {}
}
}
```
## Edit input target
```ruby
field :name, as: :text, html: {
edit: {
input: {}
}
}
```
## 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
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_resource.rb]
# app/avo/resources/course_resource.rb
class CourseResource < Avo::BaseResource
self.stimulus_controllers = "course-resource"
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
```
```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. Your 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`.
## 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
We use TailwindCSS 3.0 with the JIT engine to style Avo, so on release we only pack the used Tailwind classes in our final css file. That's why, when you want to style your custom content (tools, resource tools, fields, or ejected partials), you won't have access to all of Tailwind's utility classes. It's a performance optimization.
But there's an easy way to overcome that limitation. You can add your own TailwindCSS process to watch for your the utility classes you use.
```bash
bin/rails generate avo:tailwindcss:install
```
That command will:
- install `tailwindcss-rails` gem if you haven't installed it yet;
- create a custom `avo.tailwind.css` file where you can further customize your Avo space;
- generate or enhance your `Procfile.dev` with the required compile `yarn avo:tailwindcss --watch` command, as per default `tailwindcss-rails` practices;
- add the resulting file in your `_pre_head.html.erb` file;
- prompt you to add the script your `package.json` file. **This is a manual step you need to do**.
Now, instead of running `bin/rails server`, you can run that Procfile with `bin/dev` or `foreman start -f Procfile.dev`.
:::info
You mileage may vary when running these tasks depending with your setup. The gist is that you need to run `yarn avo:tailwindcss` on deploy0time to compile the css file and `yarn avo:tailwindcss --watch` to watch for changes in development.
:::
:::warning Add rake task to compile the assets in production
This setup works perfectly on your local environment and needs to be run on production too. Add this rake task to have them compiled and ready in production.
```ruby
# lib/tasks/avo_assets.rake
namespace :avo do
desc "Build Avo tailwind assets"
task build_avo_tailwind: [:environment] do
puts "Building Avo tailwind assets..."
`yarn` # this might be optional if you run it before
`yarn avo:tailwindcss`
end
end
Rake::Task["assets:precompile"].enhance(["avo:build_avo_tailwind"])
```
:::
Inside `app/assets/stylesheets` you'll have a new `avo.tailwind.css` file that's waiting for you to customize. The default `tailwind.config.js` file should have the proper paths set up for purging and should be ready to go.
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
/*
@layer components {
.btn-primary {
@apply py-2 px-4 bg-blue-200;
}
}
*/
```
## 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.
:::info
If your project has a `esbuild.config.mjs` file (Jumpstart Pro uses that), please add `avo.custom.js` to the `entryPoints` variable.
:::
## 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 :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`
:::
!Avo and the asset pipeline
### Sprockets and Propshaft
Create `avo.custom.js` and `avo.custom.css` inside `app/javascripts` and `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 :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 :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' %>
```
---
# `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 %>
```
## Slots
The component has a few slots where you customize the content in certain areas.
---
# 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::App.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`.
# 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

---
# 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 Actions
Given this `ReleaseFish`, this is the `spec` that tests it.
```ruby
class ReleaseFish < Avo::BaseAction
self.name = "Release fish"
self.message = "Are you sure you want to release this fish?"
field :message, as: :textarea, help: "Tell the fish something before releasing."
def handle(**args)
args[:models].each do |model|
model.release
end
succeed "#{args[:models].count} fish released with message '#{args[:fields][:message]}'."
end
end
```
```ruby
require 'rails_helper'
RSpec.feature ReleaseFish, type: :feature do
let(:fish) { create :fish }
let(:current_user) { create :user }
let(:resource) { UserResource.new.hydrate model: fish }
it "tests the dummy action" do
args = {
fields: {
message: "Bye fishy!"
},
current_user: current_user,
resource: resource,
models: [fish]
}
action = described_class.new(model: fish, 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
```
---
# `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::Backend
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{5-7,9-11,15-18}
# app/controllers/concerns/multitenancy.rb
module Multitenancy
extend ActiveSupport::Concern
included do
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`.
---
# Evaluation hosts
Avo is a package that does a lot of meta-programming. That means we have a lot of custom functionality passed from the host app to Avo to be executed at different points in time. That functionality can't always be performed in void but requires some pieces of state. We're going to talk all about them below.
You'll probably never be going to implement the hosts yourself, but you'll want to know what they contain and how they work.
Usually, this happens using lambdas. That's why we created the concept of a `Host`.
## What's a host?
A `Host` is an object that holds some pieces of state on which we execute a lambda function.
```ruby
require "dry-initializer"
# This object holds some data that is usually needed to compute blocks around the app.
module Avo
module Hosts
class BaseHost
extend Dry::Initializer
option :context, default: proc { Avo::App.context }
option :params, default: proc { Avo::App.params }
option :view_context, default: proc { Avo::App.view_context }
option :current_user, default: proc { Avo::App.current_user }
# This is optional because we might instantiate the `Host` first and later hydrate it with a block.
option :block, optional: true
delegate :authorize, to: Avo::Services::AuthorizationService
def handle
instance_exec(&block)
end
end
end
end
# Use it like so.
Avo::Hosts::BaseHost.new(block: &some_block).handle
```
## `BaseHost`
The `BaseHost` holds some of the most basic pieces of state like the request `params`, Avo's `context` object, the [`view_context`](https://apidock.com/rails/AbstractController/Rendering/view_context) object, and the `current_user`.
As the name states, this is the base host. All other hosts are inherited from it.
### `params`
The `params` object is the regular [`params`](https://guides.rubyonrails.org/action_controller_overview.html#parameters) object you are used to.
### Avo's `context` object
As you progress throughout building your app, you'll probably configure a `context` object to hold some custom state you need. For example, in `BaseHost` you have access to this object.
### The `view_context` object
The [`view_context`](https://apidock.com/rails/AbstractController/Rendering/view_context) object can be used to create the route helpers you are used to (EX: `posts_path`, `new_comment_path`, etc.).
When dealing with the `view_context` you have to lean on the object to get those paths. Also, because we are operating under an engine (Avo), the paths must be prefixed with the engine name. Rails' is `main_app`. So if you'd like to output a route to your app `/comments/5/edit`, instead of writing `edit_comment_path 5`, you'd write `view_context.main_app.edit_comment_path 5`.
### The current user
Provided that you set up the `:current_user_method`, you'll have access to that output using `current_user` in this block.
## Evaluating the block
We talked about the host and the pieces of state it holds; now, let's talk about how we can use it.
You're not going to use it when building with Avo. Instead, it's used internally when you pass a block to customize the behavior. For example, it's used when declaring the `visibility` block on `dashboards` or when you try pre-filling the suggestions for the `tags` field.
Not all blocks you declare in Avo will be executed in a `Host`. We started implementing `Host`s since v2.0 after the experience gained with v1.0. We plan on refactoring the old block to hosts at some point, but that's going to be a breaking change, so probably in v3.0.
You'll probably be prompted in the docs on each block if it's a `Host` or not.
Different hosts have different pieces of state.
## `RecordHost`
The `RecordHost` inherits from `BaseHost` and has the `record` available. The `record` is the model class instantiated with the DB information (like doing `User.find 1`) in that context.
## `ViewRecordHost`
The `ViewRecordHost` inherits from `RecordHost` and has the `view` object available too.
## `ResourceViewRecordHost`
The `ResourceViewRecordHost` inherits from `ViewRecordHost` and has the `resource` object available too.
## `AssociationScopeHost`
The `AssociationScopeHost` inherits from `BaseHost` and has the `parent` and the `query` objects available. The `parent` is the instantiated model on which the block is given and the `query` is the actual query that is going to run.
---
## Generation Information
- **Generated at:** 2025-07-18T08:51:35.972Z
- **Total sections:** 42
### Source Files
- docs/2.0/index.md
- docs/2.0/rails-and-hotwire.md
- docs/2.0/licensing.md
- docs/2.0/technical-support.md
- docs/2.0/installation.md
- docs/2.0/authentication.md
- docs/2.0/authorization.md
- docs/2.0/cache.md
- docs/2.0/resources.md
- docs/2.0/controllers.md
- docs/2.0/field-options.md
- docs/2.0/records-reordering.md
- docs/2.0/tabs.md
- docs/2.0/resource-sidebar.md
- docs/2.0/customizable-controls.md
- docs/2.0/associations.md
- docs/2.0/associations/belongs_to.md
- docs/2.0/associations/has_one.md
- docs/2.0/associations/has_many.md
- docs/2.0/associations/has_and_belongs_to_many.md
- docs/2.0/dashboards.md
- docs/2.0/cards.md
- docs/2.0/customization.md
- docs/2.0/grid-view.md
- docs/2.0/map-view.md
- docs/2.0/menu-editor.md
- docs/2.0/search.md
- docs/2.0/filters.md
- docs/2.0/actions.md
- docs/2.0/localization.md
- docs/2.0/branding.md
- docs/2.0/custom-tools.md
- docs/2.0/custom-fields.md
- docs/2.0/resource-tools.md
- docs/2.0/stimulus-integration.md
- docs/2.0/custom-asset-pipeline.md
- docs/2.0/native-components/avo-panel-component.md
- docs/2.0/native-field-components.md
- docs/2.0/field-wrappers.md
- docs/2.0/testing.md
- docs/2.0/avo-application-controller.md
- docs/2.0/evaluation-hosts.md