# 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
````
---
# Area
The `Area` field is used to display one or more Polygons on a map.
```ruby
field :center_area, as: :area
```
:::warning
You need to add the `mapkick-rb` (not `mapkick`) gem to your `Gemfile` and have the `MAPBOX_ACCESS_TOKEN` environment variable with a valid [Mapbox](https://account.mapbox.com/auth/signup/) key.
:::
## Description
By default, the area field is attached to a database column of type `:json` that has the Polygon- or Multi-Polygon coordinates stored in a nested Array as specified by the GeoJSON format. On the view you'll get in interactive map and on the edit you'll get one field where you can edit the coordinates.
For Polygons:
```ruby
[[[10.0,11.2], [10.5, 11.9],[10.8, 12.0], [10.0,11.2]]]
```
Or Multi-Polygons:
```ruby
[[[[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0]]], [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]], [[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]]]]
```
## Options
## Options combined
```ruby
field :center_area,
as: :area,
geometry: :polygon,
mapkick_options: {
style: 'mapbox://styles/mapbox/satellite-v9',
controls: true
},
datapoint_options: {
label: 'Paris City Center',
tooltip: 'Bonjour mes amis!',
color: '#009099'
}
```
This will render a map like this:
---
# Badge
The `Badge` field is used to display an easily recognizable status of a record.
```ruby
field :stage,
as: :badge,
options: {
info: [:discovery, :idea],
success: :done,
warning: 'on hold',
danger: :cancelled,
neutral: :drafting
} # The mapping of custom values to badge values.
```
## Description
By default, the badge field supports five value types: `info` (blue), `success` (green), `danger` (red), `warning` (yellow) and `neutral` (gray). We can choose what database values are mapped to which type with the `options` parameter.
The `options` parameter is a `Hash` that has the state as the `key` and your configured values as `value`. The `value` param can be a symbol, string, or array of symbols or strings.
The `Badge` field is intended to be displayed only on **Index** and **Show** views. In order to update the value shown by badge field you need to use another field like [Text](#text) or [Select](#select), in combination with `hide_on: index` and `hide_on: show`.
## Options
## Examples
```ruby
field :stage, as: :select, hide_on: [:show, :index], options: { 'Discovery': :discovery, 'Idea': :idea, 'Done': :done, 'On hold': 'on hold', 'Cancelled': :cancelled, 'Drafting': :drafting }, placeholder: 'Choose the stage.'
field :stage, as: :badge, options: { info: [:discovery, :idea], success: :done, warning: 'on hold', danger: :cancelled, neutral: :drafting }
```
---
# Boolean
The `Boolean` field renders a `input[type="checkbox"]` on **Form** views and a nice green `check` icon/red `X` icon on the **Show** and **Index** views.
```ruby
field :is_published,
as: :boolean,
name: 'Published',
true_value: 'yes',
false_value: 'no'
```
## Options
---
# Boolean Group
The `BooleanGroup` is used to update a `Hash` with `string` keys and `boolean` values in the database.
It's useful when you have something like a roles hash in your database.
```ruby
field :roles, as: :boolean_group, name: 'User roles', options: { admin: 'Administrator', manager: 'Manager', writer: 'Writer' }
```
## Options
## Example DB payload
```ruby
# Example boolean group object stored in the database
{
"admin": true,
"manager": true,
"creator": true,
}
```
---
# Code
The `Code` field generates a code editor using [codemirror](https://codemirror.net/) package. This field is hidden on **Index** view.
```ruby
field :custom_css, as: :code, theme: 'dracula', language: 'css'
```
## Options
---
# Country
`Country` field generates a [Select](#select) field on **Edit** view that includes all [ISO 3166-1](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) countries. The value stored in the database will be the country code, and the value displayed in Avo will be the name of the country.
:::warning
You must manually require the `countries` gem in your `Gemfile`.
```ruby
# All sorts of useful information about every country packaged as convenient little country objects.
gem "countries"
```
:::
```ruby
field :country, as: :country, display_code: true
```
## Options
---
# Date
The `Date` field may be used to display date values.
```ruby
field :birthday,
as: :date,
first_day_of_week: 1,
picker_format: "F J Y",
format: "yyyy-LL-dd",
placeholder: "Feb 24th 1955"
```
## Options
---
# DateTime
The `DateTime` field is similar to the Date field with two new attributes. `time_24hr` tells flatpickr to use 24 hours format and `timezone` to tell it in what timezone to display the time. By default, it uses your browser's timezone.
```ruby
field :joined_at,
as: :date_time,
name: "Joined at",
picker_format: "Y-m-d H:i:S",
format: "yyyy-LL-dd TT",
time_24hr: true,
timezone: "PST"
```
## Options
:::warning
These options may override other options like `time_24hr`.
:::
---
# External image
You may have a field in the database that has the URL to an image, and you want to display that in Avo. That is where the `ExternalImage` field comes in to help.
It will take that value, insert it into an `image_tag`, and display it on the `Index` and `Show` views.
```ruby
field :logo, as: :external_image
```
## Options
## Use computed values
Another common scenario is to use a value from your database and create a new URL using a computed value.
```ruby
field :logo, as: :external_image do |model|
"//logo.clearbit.com/#{URI.parse(model.url).host}?size=180"
rescue
nil
end
```
## Use in the Grid `cover` position
Another common place you could use it is in the grid `:cover` position.
```ruby
cover :logo, as: :external_image, link_to_resource: true do |model|
"//logo.clearbit.com/#{URI.parse(model.url).host}?size=180"
rescue
nil
end
```
---
# File
:::warning
You must manually require `activestorage` and `image_processing` gems in your `Gemfile`.
```ruby
# Active Storage makes it simple to upload and reference files
gem "activestorage"
# High-level image processing wrapper for libvips and ImageMagick/GraphicsMagick
gem "image_processing"
```
:::
The `File` field is the fastest way to implement file uploads in a Ruby on Rails app using [Active Storage](https://edgeguides.rubyonrails.org/active_storage_overview.html).
Avo will use your application's Active Storage settings with any supported [disk services](https://edgeguides.rubyonrails.org/active_storage_overview.html#disk-service).
```ruby
field :avatar, as: :file, is_image: true
```
## Options
## Authorization
:::info
Please ensure you have the `upload_{FIELD_ID}?`, `delete_{FIELD_ID}?`, and `download_{FIELD_ID}?` methods set on your model's **Pundit** policy. Otherwise, the input and download/delete buttons will be hidden.
:::
Related:
- Attachment pundit policies
---
# Files
:::warning
You must manually require `activestorage` and `image_processing` gems in your `Gemfile`.
```ruby
# Active Storage makes it simple to upload and reference files
gem "activestorage"
# High-level image processing wrapper for libvips and ImageMagick/GraphicsMagick
gem "image_processing"
```
:::
The `Files` field is similar to `File` and enables you to upload multiple files at once using the same easy-to-use [Active Storage](https://edgeguides.rubyonrails.org/active_storage_overview.html) implementation.
```ruby
field :documents, as: :files
```
## Options
## Authorization
:::info
Please ensure you have the `upload_{FIELD_ID}?`, `delete_{FIELD_ID}?`, and `download_{FIELD_ID}?` methods set on your model's **Pundit** policy. Otherwise, the input and download/delete buttons will be hidden.
:::
Related:
- Attachment pundit policies
---
# Gravatar
The `Gravatar` field turns an email field from the database into an avatar image if it's found in the [Gravatar](https://en.gravatar.com/site/implement/images/) database.
```ruby
field :email,
as: :gravatar,
rounded: false,
size: 60,
default_url: 'some image url'
```
## Options
## Using computed values
You may also pass in a computed value.
```ruby
field :email, as: :gravatar do |model|
"#{model.google_username}@gmail.com"
end
```
---
# Heading
```ruby
heading "User information"
```
The `Heading` field displays a header that acts as a separation layer between different sections.
`Heading` is not assigned to any column in the database and is only visible on the `Show`, `Edit` and `Create` views.
## Options