# Avo for Ruby on Rails Documentation - Version 4.0
Generated from Avo documentation v4.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
---
# 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
```
---
# Best practices
Due of the dynamic nature of Ruby, Rails, and Avo, you might be tempted to do a few things differently than how we envisioned them to be done.
It's ok if you want to keep doing them like that, but they might not be the most the optimum way of running Avo.
Here's a collection of best practices that we'd like you to know about.
## Avoiding `n+1` using `self.includes`
`n+1` issues happen, but they are pretty simple to mitigate using Avo.
Each resource has the `self.includes` option that helps you eager-load associations.
:::info Detailed documentation
`self.includes`
:::
## Avoid using `if/else` statements in `def fields`
You might be tempted to using `if/else` statements inside the `def fields` method.
This practice is discouraged and we'll try to explain why here.
Because of checks Avo makes during the request lifecycle, we need to know exactly which fields you have defined for your resource, no matter if they should be hidden or not to a user or in a certain scenario.
The alternative is to use the `visible` field option which will add the field on the list, but keep it hidden from the user based on the computed value.
### Example:
```ruby
# Scenario 1
def fields
if params[:special_case].present?
field :special_field, as: :text
else
field :regular_field, as: :text
end
end
# Scenario 2
def fields
field :special_field, as: :text, visible: -> { params[:special_case].present? }
field :regular_field, as: :text, visible: -> { params[:special_case].present? }
end
```
In the first scenario, where we use the `if/else` statements, depending on how the `params` are set, the fields list will be `[special_field]` or `[regular_field]`, but never both.
This will lead to many issues like filters not being visible, params not being properly permitted, and more.
In the second scenario, the field list will always be `[special_field, regular_field]` with different visibility rules.
Now Avo will know they are both there and set up the request and UI properly.
So, please use the `visibility` option and avoid `if/else` in `def fields` whenever possible.
## Add an index on the `created_at` column
Avo, by default, sorts the the record on the view by the `created_at` attribute, so it's a good idea to add an index for that column.
```ruby
# Example migration
class AddIndexOnUsersCreatedAt < ActiveRecord::Migration[7.1]
def change
add_index :users, :created_at
end
end
```
---
# 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
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.
---
# Installation
## Requirements
- Ruby on Rails >= 6.1
- Ruby >= 3.1
- `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.1 or higher defaults.
```ruby
# config/application.rb
config.autoloader = :zeitwerk
config.load_defaults 6.1 # 6.1 or higher, depending on your rails version
```
:::
## Installing Avo
### 1. One-command install
Use [this](https://railsbytes.com/public/templates/zyvsME) app template for a one-liner install process.
Run this command which will run all the required steps to install Avo in your app.
```
bin/rails app:template LOCATION='https://avohq.io/app-template'
```
### 2. Manual, step by step.
1. Add the appropiate Avo gem to the `Gemfile`
```ruby
# Add one of the following in your Gemfile depending on the tier you are on.
# Avo Community
gem "avo", ">= 3.2.1"
# Avo Pro
gem "avo", ">= 3.2.1"
gem "avo-pro", ">= 3.2.0", source: "https://packager.dev/avo-hq/"
# Avo Advanced
gem "avo", ">= 3.2.1"
gem "avo-advanced", ">= 3.2.0", source: "https://packager.dev/avo-hq/"
```
:::info
Please use this guide to find the best authentication strategy for your use-case.
:::
2. Run `bundle install`.
3. Run `bin/rails generate avo:install` to generate the initializer and add Avo to the `routes.rb` file.
4. Generate an Avo Resource
:::info
This will mount the app under `/avo` path. Visit the link to see the result.
:::
### 3. In popular Rails starter kits
We have integrations with the most popular starter kits.
#### Bullet Train
Avo comes pre-installed in all new Bullet Train applications.
I you have a Bullet Train app and you'd like to add Avo, please user [this template](https://avohq.io/templates/bullet-train).
```ruby
bin/rails app:template LOCATION=https://v3.avohq.io/templates/bullet-train.template
```
#### Jumpstart Pro
To install Avo in a Jumpstart Pro app use [this template](https://avohq.io/templates/jumpstart-pro).
```ruby
bin/rails app:template LOCATION=https://v3.avohq.io/templates/jumpstart-pro.template
```
## 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 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.
---
# Gem server authentication
Avo comes in a few tiers. The Community tier which comes as a free gem available on rubygems.org and a few paid tiers which come in private gems hosted on our own private gems server (packager.dev).
In order to have access to the paid gems you must authenticate using the **Gem Server Token** found on your [license page](https://v3.avohq.io/licenses).
There are a few ways to do that, but we will focus on the most important and secure ones for [on the server and CI systems](#on-the-server-and-ci-systems) and [on your local development environment](#on-your-local-development-environment).
:::info
We'll use the `xxx` notiation instead of the actual gem server token.
:::
## On the server and CI systems
:::info Recommendation
This is the recommended way for most use cases.
:::
The best way to do it is to register this environment variable so bundler knows to use it when pulling packages from [`packager.dev`](https://packager.dev).
```bash
export BUNDLE_PACKAGER__DEV=xxx
# or
BUNDLE_PACKAGER__DEV=xxx bundle install
```
Each hosting service will have their own way to add environment variables. Check out how to do it on [Heroku](#Heroku), [Hatchbox](#Hatchbox), [Docker](#docker_and_docker_compose), [Kamal](#Kamal) or [GitHub Actions](#git_hub_actions).
:::warning Warning about using the `.env` file
You might be tempted to add the token to your `.env` file, as you might do with your Rails app.
That will not work because `bundler` will not automatically load those environment variables.
You should add the environment variable through the service dedicated page or by running the `export` command before `bundle install`.
:::
## On your local development environment
For your local development environment you should add the token to the default bundler configuration.
This way `bundler` is aware of it without having to specify it in the `Gemfile`.
```bash
bundle config set --global https://packager.dev/avo-hq/ xxx
```
## Add Avo to your `Gemfile`
Now you are ready to add Avo to your `Gemfile`.
```ruby
# Add one of the following in your Gemfile depending on the tier you are on.
# Avo Community
gem "avo", ">= 3.2.1"
# Avo Pro
gem "avo", ">= 3.2.1"
gem "avo-pro", ">= 3.2.0", source: "https://packager.dev/avo-hq/"
# Avo Advanced
gem "avo", ">= 3.2.1"
gem "avo-advanced", ">= 3.2.0", source: "https://packager.dev/avo-hq/"
```
Now you can run `bundle install` and `bundler` will pick it up and use it to authenticate on the server.
## Bundle without paid gems
If you need to distribute your Rails app without the paid gems you can move them to an optional group.
```bash
RAILS_GROUPS=avo BUNDLE_WITH=avo bundle install
```
```ruby
# Gemfile
gem 'avo',
group :avo, optional: true do
source "https://packager.dev/avo-hq/" do
gem "avo-advanced", "~> 3.17"
end
end
```
## FAQ
Frequently asked questions:
---
# License troubleshooting
There might be times when the configurations isn't up to date and you'd like to troubleshoot it. There are a couple of things you can do to perform a self-diagnostics session.
## Check the license status page
Every Avo app has the license status page where you can see a few things about your license and the response from the license checking server.
Go to `https://yourapp.com/avo/avo_private/status`. If you mounted Avo under a different path (like `admin`) it will be `https://yourapp.com/admin/avo_private/status`.
In order to see that page your user has to be an an admin in Avo. Follow this guide to mark your user as an admin.
This should tell you if the license authenticated correctly, what is your used license key and what was the response from our checking server.
## Frequent issues
---
# Authentication
With Avo, you have the flexibility to build apps either with or without authentication. While Avo has minimal assumptions about its users, a few guidelines still apply:
1. Users can be either authenticated or not. Avo apps can be developed without requiring user authentication.
2. If you choose to implement authentication, you need to [define the current_user](#customize-the-current-user-method).
3. You can assign [lightweight roles](#user-roles) to your users.
4. Any authentication strategy or gem of your choice can be utilized.
:::info Rails 8 authentication scaffold
In essence, the [authentication scaffold](https://github.com/rails/rails/pull/52328) that Rails 8 comes with is custom authentication so we need to do a few things to ensure it's working.
Please follow this guide to enable it.
:::
## 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.
## 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 at: '/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 at: '/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
```
## User roles
There might be cases where you want to signal to Avo that the current user has a role.
Avo roles are very lightweight and that's for a reason. Building with roles in mind requires a bit more integration with the parent app. This is something that you can definitely take upon yourself to build if you need it.
There are two extra roles that you can give to a user, besides the regular user. These roles can be assigned with a check on the `current_user` object.
### Customize the methods that check for roles
You may customize the methods that Avo uses to assign roles in the initializer.
```ruby
# config/avo.rb
Avo.configure do |config|
config.is_admin_method = :is_admin?
config.is_developer_method = :is_developer?
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.
Avo provides a [Pundit](https://github.com/varvet/pundit) client out of the box for authorization that uses a policy system to manage access.
:::info Pundit alternative
Pundit is just the default client. You may plug in your own client using the instructions [here](#custom-authorization-clients).
You can use [this](https://github.com/avo-hq/avo/issues/1922) `action_policy` client as well.
:::
:::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"
```
And update config/initializers/avo.rb with following configuration:
```ruby
# Example of enabling authorization client in Avo configuration
config.authorization_client = :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::Pro::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
# Since Version 3.10.0
def attach_comments?
CommentPolicy.new(user, record).attach?
end
def detach_comments?
CommentPolicy.new(user, record).detach?
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
# The `parent` is the Post instance that the user is seeing. ex: Post.find(1)
# The `query` is the Active Record query being done on the comments. ex: post.comments
field :comments, as: :has_many, scope: -> { Pundit.policy_scope(parent, query) }
```
:::
## 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_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.
## Use Resource's Policy to authorize custom actions
It may be necessary to authorize a specific field or custom action of a resource using a policy class rather than defining the authorization logic directly within the resource class. By doing so, we can delegate control to the policy class, ensuring a cleaner and more maintainable authorization structure.
:::code-group
```ruby [app/resources/product.rb]{8}
field :amount,
as: :money,
currencies: %w[USD],
sortable: true,
filterable: true,
copyable: true,
# define ability to change the amount in policy class instead of doing it here
disabled: -> { !@resource.authorization.authorize_action(:amount?, raise_exception: false) }
```
```ruby [app/policies/product_policy.rb]{2-4}
# Define ability to change the amount in Product Policy
def amount?
user.admin?
end
```
:::
## 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 `Avo::Resources::User` 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_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.
## Logs
Developers have the ability to monitor any unauthorized actions. When a developer user makes a request that triggers an unauthorized action, a log entry similar to the following will be generated:
In development each log entry provides details about the policy class, the action attempted, the global id of the user who made the request, and the global id of the record involved:
```bash
web | [Avo->] Unauthorized action 'reorder?' for 'UserPolicy'
web | user: gid://dummy/User/20
web | record: gid://dummy/User/31
```
To find a record based on its global id you can use `GlobalID::Locator.locate`
```ruby
gid = "gid://dummy/User/20"
user = GlobalID::Locator.locate(gid)
```
In production each log entry provides details only about the policy class and the attempted action:
```bash
web | [Avo->] Unauthorized action 'act_on?' for 'UserPolicy'
```
## 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
# app/avo/resources/photo_comment.rb
class Avo::Resources::PhotoComment < 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.
## Explicit authorization
## Rolify integration
Check out this guide to add rolify role management with Avo.
---
# 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 the Rails models 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.
## Resources from model generation
```bash
bin/rails generate model car make:string mileage:integer
```
Running this command will generate the standard Rails files (model, controller, etc.) and `Avo::Resources::Car` & `Avo::CarsController` for Avo.
The auto-generated resource file will look like this:
```ruby
# app/avo/resources/car.rb
class Avo::Resources::Car < Avo::BaseResource
self.includes = []
# self.search = {
# query: -> { query.ransack(id_eq: params[:q], m: "or").result(distinct: false) }
# }
def fields
field :id, as: :id
field :make, as: :text
field :mileage, as: :number
end
end
```
This behavior can be omitted 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 `Post` resource file in `app/avo/resources/post.rb` with the following code:
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.includes = []
# self.search = {
# query: -> { query.ransack(id_eq: params[:q], m: "or").result(distinct: false) }
# }
def fields
field :id, as: :id
end
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 attributes and associations. In that case, the Avo resource will be generated with the fields attributes and associations.
::: code-group
```ruby [app/models/post.rb]
# == 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
```
```ruby [app/avo/resources/post.rb]
class Avo::Resources::Post < Avo::BaseResource
self.includes = []
# self.search = {
# query: -> { query.ransack(id_eq: params[:q], m: "or").result(distinct: false) }
# }
def fields
field :id, as: :id
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
end
end
```
:::
It's also possible to specify the resource model class. For example, if we want to create a new resource named `MiniPost` resource 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 Avo::Resources::MiniPost < 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.
:::
## Generating resources for all models
To generate Avo resources for all models in your application, run:
```bash
bin/rails generate avo:all_resources
```
### What it does
1. Scans your `app/models` directory for all model files
2. Excludes `ApplicationRecord` from the generation process
3. For each model found, it:
- Generates a corresponding Avo resource using the `avo:resource` generator
- Handles errors gracefully, printing error messages if generation fails for any model
This is particularly useful when:
- Setting up Avo in an existing Rails application
- Ensuring all your models have corresponding Avo resources
## Fields
`Resource` files tell Avo what records should be displayed in the UI, but not what kinds of data they hold. You do that using the `fields` method.
Read more about the fields here.
```ruby{5-17}
class Avo::Resources::Post < Avo::BaseResource
self.title = :id
self.includes = []
def fields
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_record: true
field :is_featured, as: :boolean
field :is_published, as: :boolean do
record.published_at.present?
end
field :user, as: :belongs_to, placeholder: "β"
end
end
```
## Routing
Avo will automatically generate routes based on the resource name when generating a resource.
```
Avo::Resources::Post -> /avo/resources/posts
Avo::Resources::PhotoComment -> /avo/resources/photo_comments
```
If you change the resource name, you should change the generated controller name too.
## Use multiple resources for the same model
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 `User` resource associated with it.
```ruby
# app/models/user.rb
class User < ApplicationRecord
end
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id, link_to_record: true
field :email, as: :gravatar, link_to_record: true, as_avatar: :circle
field :first_name, as: :text, required: true, placeholder: "John"
field :last_name, as: :text, required: true, placeholder: "Doe"
end
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 `Team` resource like so:
```ruby{12}
# app/models/team.rb
class Team < ApplicationRecord
end
# app/avo/resources/team.rb
class Avo::Resources::Team < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id, link_to_record: true
field :name, as: :text
field :users, as: :has_many
end
end
```
From that configuration, Avo will figure out that the `users` field points to the `User` resource 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 `TeamUser` resource and add those fields.
```ruby
# app/avo/resources/team_user.rb
class Avo::Resources::TeamUser < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id, link_to_record: true
field :name, as: :text
field :projects_count, as: :number
end
end
```
We also need to update the `Team` resource to use the new `TeamUser` resource for reference.
```ruby
# app/avo/resources/team.rb
class Avo::Resources::Team < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id, link_to_record: true
field :name, as: :text
field :users, as: :has_many, use_resource: Avo::Resources::TeamUser
end
end
```
But now, if we visit the `Users` page, we will see the fields for the `TeamUser` resource instead of `User` resource, and that's because Avo fetches the resources in an alphabetical order, and `TeamUser` resource is before `User` resource. 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': 'Avo::Resources::User'
}
end
```
That will "shortcircuit" the regular alphabetical search and use the `User` resource 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.
## Namespaced resources
`Resource`s can't be namespaced yet, so they all need to be in the root level of that directory. If you have a model `Super::Dooper::Trooper::Model` you can use `Avo::Resources::SuperDooperTrooperModel` with the `model_class` option.
```ruby
class Avo::Resources::SuperDooperTrooperModel < Avo::BaseResource
self.model_class = "Super::Dooper::Trooper::Model"
end
```
## Views
Please read the detailed views page.
## 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
```
:::warning
You can't use `Avo::BaseController` and `Avo::ResourcesController` as **your base controller**. They are defined inside Avo.
:::
When you generate a new resource or controller in Avo, it won't automatically inherit from the `Avo::BaseResourcesController`. However, you have two approaches to ensure that the new generated controllers inherit from a custom controller:
### `--parent-controller` option on the generators
Both the `avo:controller` and `avo:resource` generators accept the `--parent-controller` option, which allows you to specify the controller from which the new controller should inherit. Here are examples of how to use it:
```bash
rails g avo:controller city --parent-controller Avo::BaseResourcesController
rails g avo:resource city --parent-controller Avo::BaseResourcesController
```
### `resource_parent_controller` configuration option
You can configure the `resource_parent_controller` option in the `avo.rb` initializer. This option will be used to establish the inherited controller if the `--parent-controller` argument is not passed on the generators. Here's how you can do it:
```ruby
Avo.configure do |config|
# ...
config.resource_parent_controller = "Avo::BaseResourcesController" # "Avo::ResourcesController" is default value
# ...
end
```
### Attach concerns to `Avo::BaseController`
Alternatively you can use [this guide](https://avohq.io/blog/safely-extend-a-ruby-on-rails-controller) to attach methods, actions, and hooks to the main `Avo::BaseController` or `Avo::ApplicationController`.
## 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.
If you want to manually load them use the `config.resources` option.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.resources = [
"Avo::Resources::User",
"Avo::Resources::Fish",
]
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.
## Extending `Avo::BaseResource`
we have restructured the `Avo::BaseResource` to enhance user customization capabilities. The existing functionality has been moved to a new base class `Avo::Resources::Base`, and `Avo::BaseResource` is now left empty for user overrides. This allows users to easily add custom methods that all of their resources will inherit, without having to modify the internal base class.
### How to Customize `Avo::BaseResource`
You can customize `Avo::BaseResource` by creating your own version in your application. This custom resource can include methods and logic that you want all your resources to inherit. Here's an example to illustrate how you can do this:
```ruby
# app/avo/base_resource.rb
module Avo
class BaseResource < Avo::Resources::Base
# Example custom method: make all number fields cast their values to float
def field(id, **args, &block)
if args[:as] == :number
args[:format_using] = -> { value.to_f }
end
super(id, **args, &block)
end
end
end
```
All your resources will now inherit from your custom `Avo::BaseResource`, allowing you to add common functionality across your admin interface. For instance, the above example ensures that all number fields in your resources will have their values cast to floats. You can add any other shared methods or customizations here, making it easier to maintain consistent behavior across all resources.
### Your resource files
Your resource file will still look the same as it did before.
```ruby
# app/avo/resources/post_resource.rb
module Avo::Resources::Post < Avo::BaseResource
# Your existing configuration for the Post resource
end
```
## Resource Options
Resources have a few options available for customization.
## Cards
Use the `def cards` method to add some cards to your resource.
Check cards documentation for more details.
```ruby{9-19}
class Avo::Resources::User < Avo::BaseResource
def fields
field :id, as: :id
field :name, as: :text
field :email, as: :text
field :roles, as: :boolean_group, options: {admin: "Administrator", manager: "Manager", writer: "Writer"}
end
def cards
card Avo::Cards::ExampleAreaChart, cols: 3
card Avo::Cards::ExampleMetric, cols: 2
card Avo::Cards::ExampleMetric,
label: "Active users metric",
description: "Count of the active users.",
arguments: { active_users: true },
visible: -> { !resource.view.form? }
end
end
```
---
# Array Resources
## Overview
An **Array Resource** is a flexible resource that can be backed by an **array of hashes** or an **array of Active Record objects**. It is not constrained to an Active Record model and allows dynamic data handling.
:::info Related field
The Array Resource can be used in conjunction with the `Array` field to manage structured array data in your resources.
For more details on using the `Array` field, including examples and hierarchy of data fetching, check out the Array Field documentation.
This integration allows for seamless configuration of dynamic or predefined array-based data within your application.
:::
:::warning β οΈ Limitations
#### Sorting
- The array resource does **not support sorting**.
#### Performance Considerations
- When dealing with large datasets, you might experience suboptimal performance due to inherent architectural constraints.
- **Caching Recommendation:**
- It is advisable to implement caching mechanisms as a viable solution to ameliorate these performance bottlenecks.
- **Note:** These caching mechanisms should ideally be integrated into the methods that fetch data, such as the `def records` method.
**Please note that these caveats are based on the current implementation and may be subject to revisions in future releases.**
:::
## Creating an Array Resource
Generate an **Array Resource** using the `--array` flag:
```bash
bin/rails generate avo:resource Movie --array
```
This sets up a resource designed to work with an array of data.
## Defining the `records` Method
The `records` method serves as the fallback source for data in the resource. It returns an array of hashes or Active Record objects.
### Example
```ruby
def records
[
{
id: 1,
name: "The Shawshank Redemption",
release_date: "1994-09-23"
},
{
id: 2,
name: "The Godfather",
release_date: "1972-03-24",
fun_fact: "The iconic cat in the opening scene was a stray found by director Francis Ford Coppola on the studio lot."
},
{
id: 3,
name: "Pulp Fiction",
release_date: "1994-10-14"
}
]
end
```
## Defining Fields
Array Resources use fields like any other Avo resource. Hereβs an example for a `Movie` resource:
```ruby
class Avo::Resources::Movie < Avo::Resources::ArrayResource
def records
[
{
id: 1,
name: "The Shawshank Redemption",
release_date: "1994-09-23"
},
{
id: 2,
name: "The Godfather",
release_date: "1972-03-24",
fun_fact: "The iconic cat in the opening scene was a stray found by director Francis Ford Coppola on the studio lot."
},
{
id: 3,
name: "Pulp Fiction",
release_date: "1994-10-14"
}
]
end
def fields
main_panel do
field :id, as: :id
field :name, as: :text
field :release_date, as: :date
field :fun_fact, only_on: :index, visible: -> { resource.record.fun_fact.present? } do
record.fun_fact.truncate_words(10)
end
sidebar do
field :fun_fact do
record.fun_fact || "There is no register of a fun fact for #{record.name}"
end
end
end
end
end
```
---
# HTTP Resources
## Overview
An **HTTP Resource** is a flexible resource that can be backed by an **endpoint** request. Unlike traditional resources tied to Active Record models, HTTP Resources allow dynamic interaction with external APIs and non-persistent data sources.
:::warning β οΈ Limitations
- The HTTP Resource does **not support sorting** at this time.
**Please note that these limitations stem from the current implementation and may evolve in future releases.**
:::
## Installing the gem
To enable HTTP Resource functionality in your Avo project, you need to include the `avo-http_resource` gem.
Add it to your Gemfile:
```ruby
gem "avo-http_resource", source: "https://packager.dev/avo-hq/"
```
Then install it:
```bash
bundle install
```
Once the gem is installed, HTTP Resources will be available as a new type of resource, enabling you to connect seamlessly with external APIs and custom data endpoints, no Active Record necessary.
## Creating an HTTP Resource
You can generate an HTTP Resource using the `--http` flag in the generator:
```bash
bin/rails generate avo:resource Author --http
```
## Parsing Data from an Endpoint
To wire an HTTP Resource to a data source, you must configure several attributes. Below is a breakdown of the supported options, each with an illustrative example.
```ruby
# app/avo/resources/author.rb
class Avo::Resources::Author < Avo::Core::Resources::Http
# The base URL for your external API
self.endpoint = "https://api.openalex.org/authors"
# How to extract the list of records from the API response
self.parse_collection = -> {
raise Avo::HttpError.new response["message"] if response["error"].present?
response["results"]
}
# How to extract a single record from the API response
self.parse_record = -> {
raise Avo::HttpError.new response["message"] if response["error"].present?
response
}
# How to extract the total count of records (useful for pagination)
self.parse_count = -> { response["meta"]["count"] }
# Optional: custom method to find a record if the ID is encoded or non-standard
self.find_record_method = -> { query.find Base64.decode64(id) }
# Optional: redefines model behavior to obfuscate the ID via Base64
self.model_class_eval = -> {
define_method :to_param do
Base64.encode64(id)
end
}
# Optional: custom headers to send with the request
self.headers = {
"Authorization" => "Bearer #{ENV.fetch("API_KEY")}"
}
def fields
field :id, as: :id
field :display_name
field :cited_by_count, name: "Total citations"
field :works_count, name: "Total works"
end
end
```
### Response Option Reference
Here's a brief reference for the main configuration options:
| Option | Description |
|---------------------|-----------------------------------------------------------------------------|
| `parse_collection` | Proc that returns the array of records |
| `parse_record` | Proc that returns a single record |
| `parse_count` | Proc that returns the total number of records |
| `model_class_eval` | Optional: proc to define extra model behavior, often used for `to_param` |
All HTTP Resource response options accept a **proc** (i.e., a lambda or block). These procs are executed in a rich runtime context that gives you full access to the HTTP response and metadata around the request.
Within this block, you gain access to all attributes of `Avo::ExecutionContext`, including:
- `raw_response` β the raw `HTTParty::Response` response object
- `response` β the parsed body from `raw_response` (`raw_response.parsed_response`)
- `headers` β the headers from the response, available via `raw_response.headers`
This contextual access empowers you to define your resourceβs behavior with a high degree of precision. Whether you're extracting deeply nested structures or implementing nuanced error handling, the execution context provides all the necessary components to **structure your parsing logic with clarity and control**.
### Request Option Reference
Here's a brief reference for the main request options:
| Option | Description |
|---------------------|-----------------------------------------------------------------------------|
| `endpoint` | Base URL for the external API |
| `headers` | Optional: custom headers to send with the request |
All HTTP Resource request options accept a **proc** (i.e., a lambda or block). These procs are executed in a rich runtime context that gives you full access to all attributes of `Avo::ExecutionContext`
## Handling API Errors Gracefully
When interacting with external APIs, it's important to handle error responses gracefully. Avo provides a custom exception, `Avo::HttpError`, for this exact purpose.
You can raise this error within your parsing procs like so:
```ruby
raise Avo::HttpError.new response["message"] if response["error"].present?
```
This signals to Avo that the API returned an error, and the HTTP controller will automatically **rescue** the exception and **display the message as a flash error in the UI**. This allows you to surface meaningful error feedback to users without breaking the experience or having to manually handle exceptions across the interface.
This pattern ensures your integration remains **resilient** and **intuitive**, providing a seamless user experience even when interacting with unreliable or unpredictable external data sources.
## Controlling Create, Update, and Destroy Behavior
By default, the HTTP Resource controller provides built-in methods to handle **creation**, **updates**, and **deletion** of records through your API client. These methods are designed to be flexible and easy to override when you need custom behavior.
### Default Implementation
```ruby
class Avo::Core::Controllers::Http
def save_record
# Perform either a create or update request based on the current controller action
response = @resource.client.send(action_name, @record)
# Should return true if the operation succeeded, false otherwise
response.success?
end
def destroy_model
# Perform a DELETE request to remove the record via the external API
@resource.client.delete(@record.id)
end
end
```
### Customizing the Behavior
If your external API requires additional parameters, or conditional logic, you can override these methods in your custom controller.
- `save_record` should return a **boolean**, indicating whether the create or update operation was successful.
- You can determine if the operation is a **create** or an **update** by inspecting the `action_name`, which will be `"create"` or `"update"` respectively.
### Example Override
```ruby
# app/controllers/avo/authors_controller.rb
class Avo::AuthorsController < Avo::Core::Controllers::Http
def save_record
auth_headers = {
"Authorization" => "Bearer #{ENV.fetch("API_KEY")}"
}
if action_name == "create"
response = MyCustomApi.post("/authors", body: @record.as_json, headers: auth_headers)
else
response = MyCustomApi.put("/authors/#{@record.id}", body: @record.as_json, headers: auth_headers)
end
response.status == 200
end
end
```
This approach grants you complete control over how HTTP Resources interact with your external services, allowing seamless integration, even with APIs that have unconventional or highly specific requirements.
---
# Fields
Fields are the backbone of a `Resource`.
Through fields you tell Avo what to fetch from the database and how to display it on the , , and views.
Avo ships with various simple fields like `text`, `textarea`, `number`, `password`, `boolean`, `select`, and more complex ones like `markdown`, `key_value`, `trix`, `tags`, and `code`.
## Declaring fields
You add fields to a resource through the `fields` method using the `field DATABASE_COLUMN, as: FIELD_TYPE, **FIELD_OPTIONS` notation.
```ruby
def fields
field :name, as: :text
end
```
The `name` property is the column in the database where Avo looks for information or a property on your model.
That will add a few fields in your new Avo app.
On the and views, we'll get a new text column of that record's database value.
Finally, on the and views, we will get a text input field that will display & update the `name` field on that model.
### Specific methods for each view
The `fields` method in your resource is invoked whenever non-specific view methods are present. To specify fields for each view or a group of views, you can use the following methods:
`index` view -> `index_fields`
`show` view -> `show_fields`
`edit` / `update` views -> `edit_fields`
`new` / `create` views -> `new_fields`
You can also register fields for a specific group of views as follows:
`index` / `show` views -> `display_fields`
`edit` / `update` / `new` / `create` views -> `form_fields`
When specific view fields are defined, they take precedence over view group fields. If neither specific view fields nor view group fields are defined, the fields will be retrieved from the `fields` method.
The below example use two custom helpers methods to organize the fields through `display_fields` and `form_fields`
:::code-group
```ruby [display_fields]
def display_fields
base_fields
tool_fields
end
```
```ruby [form_fields]
def form_fields
base_fields
tool_fields
tool Avo::ResourceTools::CityEditor, only_on: :forms
end
```
```ruby [tool_fields (helper method)]
# Notice that even if those fields are hidden on the form, we still include them on `form_fields`.
# This is because we want to be able to edit them using the tool.
# When submitting the form, we need this fields declared on the resource in order to know how to process them and fill the record.
def tool_fields
with_options hide_on: :forms do
field :name, as: :text, help: "The name of your city", filterable: true
field :population, as: :number, filterable: true
field :is_capital, as: :boolean, filterable: true
field :features, as: :key_value
field :image_url, as: :external_image
field :tiny_description, as: :markdown
field :status, as: :badge, enum: ::City.statuses
end
end
```
```ruby [base_fields (helper method)]
def base_fields
field :id, as: :id
field :coordinates, as: :location, stored_as: [:latitude, :longitude]
field :city_center_area,
as: :area,
geometry: :polygon,
mapkick_options: {
style: "mapbox://styles/mapbox/satellite-v9",
controls: true
},
datapoint_options: {
label: "Paris City Center",
tooltip: "Bonjour mes amis!",
color: "#009099"
}
field :description,
as: :trix,
attachment_key: :description_file,
visible: -> { resource.params[:show_native_fields].blank? }
field :metadata,
as: :code,
format_using: -> {
if view.edit?
JSON.generate(value)
else
value
end
},
update_using: -> do
ActiveSupport::JSON.decode(value)
end
field :created_at, as: :date_time, filterable: true
end
```
:::
:::warning In some scenarios fields require presence even if not visible
In certain situations, fields must be present in your resource configuration, even if they are hidden from view. Consider the following example where `tool_fields` are included within `form_fields` despite being wrapped in a `with_options hide_on: :forms do ... end` block.
For instance, when using `tool Avo::ResourceTools::CityEditor, only_on: :forms`, it will render the `features` field, which is of type `key_value`. When the form is submitted, Avo relies on the presence of the `features` field to determine its type and properly parse the submitted value.
If you omit the declaration of `field :features, as: :key_value, hide_on: :forms`, Avo will be unable to update that specific database column.
:::
## Field conventions
When we declare a field, we pinpoint the specific database row for that field. Usually, that's a snake case value.
Each field has a label. Avo will convert the snake case name to a humanized version.
In the following example, the `is_available` field will render the label as *Is available*.
```ruby
field :is_available, as: :boolean
```
:::info
If having the fields stacked one on top of another is not the right layout, try the resource-sidebar.
:::
### A more complex example
```ruby
class Avo::Resources::User < Avo::BaseResource
def fields
field :id, as: :id
field :first_name, as: :text
field :last_name, as: :text
field :email, as: :text
field :active, as: :boolean
field :cv, as: :file
field :is_admin?, as: :boolean
end
end
```
The `fields` method is already hydrated with the `current_user`, `params`, `request`, `view_context`, and `context` variables so you can use them to conditionally show/hide fields
```ruby
class Avo::Resources::User < Avo::BaseResource
def fields
field :id, as: :id
field :first_name, as: :text
field :last_name, as: :text
field :email, as: :text
field :is_admin?, as: :boolean
field :active, as: :boolean
if current_user.is_admin?
field :cv, as: :file
end
end
end
```
## Field Types
---
# Field options
Avo fields are dynamic and can be configured using field options.
There are quite a few **common field options** described on this page that will work with most fields (but some might not support them), and some **custom field options** that only some fields respond to that are described on each field page.
### Common field option example
```ruby
# disabled will disable the field on the `Edit` view
field :name, as: :text, disabled: true
field :status, as: :select, disabled: true
```
### Custom field option example
```ruby
# options will set the dropdown options for a select field
field :status, as: :select, options: %w[first second third]
```
## Change field name
To customize the label, you can use the `name` property to pick a different label.
```ruby
field :is_available, as: :boolean, name: "Availability"
```
## Showing / Hiding fields on different views
There will be cases where you want to show fields on different views conditionally. For example, you may want to display a field in the and views and hide it on the and views.
For scenarios like that, you may use the visibility helpers `hide_on`, `show_on`, `only_on`, and `except_on` methods. Available options for these methods are: `:new`, `:edit`, `:index`, `:show`, `:forms` (both `:new` and `:edit`) and `:all` (only for `hide_on` and `show_on`).
Version 3 introduces the `:display` option that is the opposite of `:forms`, referring to both, `:index` and `:show`
Be aware that a few fields are designed to override those options (ex: the `id` field is hidden in and ).
```ruby
field :body, as: :text, hide_on: [:index, :show]
```
Please read the detailed views page for more info.
## Field Visibility
You might want to restrict some fields to be accessible only if a specific condition applies. For example, hide fields if the user is not an admin.
You can use the `visible` block to do that. It can be a `boolean` or a lambda.
Inside the lambda, we have access to the `context` object and the current `resource`. The `resource` has the current `record` object, too (`resource.record`).
```ruby
field :is_featured, as: :boolean, visible: -> { context[:user].is_admin? } # show field based on the context object
field :is_featured, as: :boolean, visible: -> { resource.name.include? 'user' } # show field based on the resource name
field :is_featured, as: :boolean, visible: -> { resource.record.published_at.present? } # show field based on a record attribute
```
:::warning
On form submissions, the `visible` block is evaluated in the `create` and `update` controller actions. That's why you have to check if the `resource.record` object is present before trying to use it.
:::
```ruby
# `resource.record` is nil when submitting the form on resource creation
field :name, as: :text, visible -> { resource.record.enabled? }
# Do this instead
field :name, as: :text, visible -> { resource.record&.enabled? }
```
## Computed Fields
You might need to show a field with a value you don't have in a database row. In that case, you may compute the value using a block that receives the `record` (the actual database record), the `resource` (the configured Avo resource), and the current `view`. With that information, you can compute what to show on the field in the and views.
```ruby
field 'Has posts', as: :boolean do
record.posts.present?
rescue
false
end
```
:::info
Computed fields are displayed only on the and views.
:::
This example will display a boolean field with the value computed from your custom block.
## Fields Formatter
Sometimes you will want to process the database value before showing it to the user.
There are several ways to format fields.
### Common formatting options
In all cases, you have access to a bunch of variables inside this block, all the defaults that `Avo::ExecutionContext` provides plus `value`, `record`, `resource`, `view` and `field`.
## Formatting with Rails helpers
You can also format using Rails helpers like `number_to_currency` (note that `view_context` is used to access the helper):
```ruby
field :price, as: :number, format_using: -> { view_context.number_to_currency(value) }
```
## Parse value before update
When it's necessary to parse information before storing it in the database, the `update_using` option proves to be useful. Inside the block you can access the raw `value` from the form, and the returned value will be saved in the database.
```ruby
field :metadata,
as: :code,
update_using: -> do
ActiveSupport::JSON.decode(value)
end
```
## Sortable fields
One of the most common operations with database records is sorting the records by one of your fields. For that, Avo makes it easy using the `sortable` option.
Add it to any field to make that column sortable in the view.
```ruby
field :name, as: :text, sortable: true
```
**Related:**
- Add an index on the `created_at` column
## Custom sortable block
When using computed fields or `belongs_to` associations, you can't set `sortable: true` to that field because Avo doesn't know what to sort by. However, you can use a block to specify how the records should be sorted in those scenarios.
```ruby{4-7}
class Avo::Resources::User < Avo::BaseResource
field :is_writer,
as: :text,
sortable: -> {
# Order by something else completely, just to make a test case that clearly and reliably does what we want.
query.order(id: direction)
},
hide_on: :edit do
record.posts.to_a.size > 0 ? "yes" : "no"
end
end
```
The block receives the `query` and the `direction` in which the sorting should be made and must return back a `query`.
In the example of a `Post` that `has_many` `Comment`s, you might want to order the posts by which one received a comment the latest.
You can do that using this query.
::: code-group
```ruby{5} [app/avo/resources/post.rb]
class Avo::Resources::Post < Avo::BaseResource
field :last_commented_at,
as: :date,
sortable: -> {
query.includes(:comments).order("comments.created_at #{direction}")
}
end
```
```ruby{4-6} [app/models/post.rb]
class Post < ApplicationRecord
has_many :comments
def last_commented_at
comments.last&.created_at
end
end
```
:::
## Placeholder
Some fields support the `placeholder` option, which will be passed to the inputs on and views when they are empty.
```ruby
field :name, as: :text, placeholder: 'John Doe'
```
## Required
To indicate that a field is mandatory, you can utilize the `required` option, which adds an asterisk to the field as a visual cue.
Avo automatically examines each field to determine if the associated attribute requires a mandatory presence. If it does, Avo appends the asterisk to signify its mandatory status. It's important to note that this option is purely cosmetic and does not incorporate any validation logic into your model. You will need to manually include the validation logic yourself, such as (`validates :name, presence: true`).
```ruby
field :name, as: :text, required: true
```
You may use a block as well. It will be executed in the `Avo::ExecutionContext` and you will have access to the `view`, `record`, `params`, `context`, `view_context`, and `current_user`.
```ruby
field :name, as: :text, required: -> { view == :new } # make the field required only on the new view and not on edit
```
## Disabled
When you need to prevent the user from editing a field, the `disabled` option will render it as `disabled` on and views and the value will not be passed to that record in the database. This prevents a bad actor to go into the DOM, enable that field, update it, and then submit it, updating the record.
```ruby
field :name, as: :text, disabled: true
```
### Disabled as a block
You may use a block as well. It will be executed in the `Avo::ExecutionContext` and you will have access to the `view`, `record`, `params`, `context`, `view_context`, and `current_user`.
```ruby
field :id, as: :number, disabled: -> { view == :edit } # make the field disabled only on the new edit view
```
## Readonly
When you need to prevent the user from editing a field, the `readonly` option will render it as `disabled` on and views. This does not, however, prevent the user from enabling the field in the DOM and send an arbitrary value to the database.
```ruby
field :name, as: :text, readonly: true
```
## Default Value
When you need to give a default value to one of your fields on the view, you may use the `default` block, which takes either a fixed value or a block.
```ruby
# using a value
field :name, as: :text, default: 'John'
# using a callback function
field :level, as: :select, options: { 'Beginner': :beginner, 'Advanced': :advanced }, default: -> { Time.now.hour < 12 ? 'advanced' : 'beginner' }
```
## 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.'
```
## Nullable
When a user uses the **Save** button, Avo stores the value for each field in the database. However, there are cases where you may prefer to explicitly instruct Avo to store a `NULL` value in the database row when the field is empty. You do that by using the `nullable` option, which converts `nil` and empty values to `NULL`.
You may also define which values should be interpreted as `NULL` using the `null_values` method.
```ruby
# using default options
field :updated_status, as: :status, failed_when: [:closed, :rejected, :failed], loading_when: [:loading, :running, :waiting], nullable: true
# using custom null values
field :body, as: :textarea, nullable: true, null_values: ['0', '', 'null', 'nil', nil]
```
## Link to record
Sometimes, on the view, you may want a field in the table to be a link to that resource so that you don't have to scroll to the right to click on the icon. You can use `link_to_record` to change a table cell to be a link to that record.
```ruby
# for id field
field :id, as: :id, link_to_record: true
# for text field
field :name, as: :text, link_to_record: true
# for gravatar field
field :email, as: :gravatar, link_to_record: true
```
You can add this property on `id`, `text`, and `gravatar` fields.
Optionally you can enable the global config `id_links_to_resource`. More on that on the id links to resource docs page.
**Related:**
- ID links to resource
- Resource controls on the left side
## Align text on Index view
It's customary on tables to align numbers to the right. You can do that using the `html` option.
```ruby{2}
class Avo::Resources::Project < Avo::BaseResource
field :users_required, as: :number, html: {index: {wrapper: {classes: "text-right"}}}
end
```
## Stacked layout
For some fields, it might make more sense to use all of the horizontal area to display it. You can do that by changing the layout of the field wrapper using the `stacked` option.
```ruby
field :meta, as: :key_value, stacked: true
```
#### `inline` layout (default)
#### `stacked` layout
## Global `stacked` layout
You may also set all the fields to follow the `stacked` layout by changing the `field_wrapper_layout` initializer option from `:inline` (default) to `:stacked`.
```ruby
Avo.configure do |config|
config.field_wrapper_layout = :stacked
end
```
Now, all fields will have the stacked layout throughout your app.
## Field options
---
# Field Discovery
`discover_columns` and `discover_associations` automatically detect and configure fields for your Avo resources based on your model's database structure.
```rb{6-7}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
# ...
def fields
discover_columns
discover_associations
end
end
```
## Options
## Examples
### Basic Discovery
```rb{6-7}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
# ...
def fields
discover_columns
discover_associations
end
end
```
### Custom Field Options
This will add the provided options to every discovered field or association. This is particularly useful when having duplicative configurations across many fields.
```rb{6-7}
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
# ...
def fields
discover_columns help: "Automatically discovered fields"
discover_associations searchable: false
end
end
```
### Combining Manual and Discovered Fields
```rb{6,8-9,11}
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
# ...
def fields
field :custom_field, as: :text
discover_columns except: [:custom_field]
discover_associations
field :another_custom_field, as: :boolean
end
end
```
## Automatic Type Mapping
Field discovery maps database column types to Avo field types automatically.
e.g.
- `string` β `:text`
- `integer` β `:number`
- `float` β `:number`
- `datetime` β `:datetime`
- `boolean` β `:boolean`
- `json/jsonb` β `:code`
The full, up-to-date list can be found [here](https://github.com/avo-hq/avo/blob/main/lib/avo/mappings.rb)
## Association Discovery
The following associations are automatically configured:
- `belongs_to` β `:belongs_to`
- `has_one` β `:has_one`
- `has_many` β `:has_many`
- `has_one_attached` β `:file`
- `has_many_attached` β `:files`
- `has_rich_text` β `:trix`
- `acts-as-taggable-on :tags` β `:tags`
The full, up-to-date list can be found [here](https://github.com/avo-hq/avo/blob/main/lib/avo/mappings.rb)
---
# Resource 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.
---
# Record previews
:::warning
This section is a work in progress.
:::
To use record previews add the `preview` field on your resource and add `show_on: :preview` to the fields you'd like to have visible on the preview popover.
```ruby{3,7,11,14}
class Avo::Resources::Team < Avo::BaseResource
def fields
field :preview, as: :preview
field :name,
as: :text,
sortable: true,
show_on: :preview
field :color,
as: Avo::Fields::ColorPickerField,
hide_on: :index,
show_on: :preview
field :description,
as: :textarea,
show_on: :preview
end
end
```
---
# Scopes
:::warning
This section is a work in progress.
:::
Sometimes you might need to segment your data beyond just a few filters. You might have an `User` resource but you frequently need to see all the **Active users** or **Admin users**. You can use a filter for that or add a scope.
## Generating scopes
```bash
bin/rails generate avo:scope admins
```
```ruby
# app/avo/scopes/admins.rb
class Avo::Scopes::Admins < Avo::Advanced::Scopes::BaseScope
self.name = "Admins" # Name displayed on the scopes bar
self.description = "Admins only" # This is the tooltip value
self.scope = :admins # valid scope on the model you're using it
self.visible = -> { true } # control the visibility
end
# app/models/user.rb
class User < ApplicationRecord
scope :admins, -> { where role: :admin } # This is used in the scope file above
end
```
## Registering scopes
Because scopes are re-utilizable, you must manually add that scope to a resource using the `scope` method inside the `scopes` method.
```ruby{4}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def scopes
scope Avo::Scopes::Admins
end
end
```
## Options
### Execution Context
All options can be configured using static values or procs. The procs are executed using the Avo::ExecutionContext, which provides access to all default methods and attributes available in Avo's execution context. Each option has access to:
- `query`
- `resource`
- `scope`
- `scoped_query` (check below Performance Note)
:::warning Performance Note
Inside each proc, you can call `scoped_query`, but use it with caution as it executes the scope. If the scope takes a while to execute, this could impact performance.
:::
---
---
---
---
## Full example
```ruby
# app/avo/scopes/even_id.rb
class Avo::Scopes::EvenId < Avo::Advanced::Scopes::BaseScope
# Please see the performance note above if you're using `scoped_query`
self.name = -> { "Even (#{scoped_query.count})" }
# This will compute the description based on the resource name
self.description = -> {
"Only #{resource.name.downcase.pluralize} that have an even ID"
}
# This will scope the query to only even IDs
self.scope = -> { query.where("#{resource.model_key}.id % 2 = ?", "0") }
# Only show this scope to admins
self.visible = -> { current_user.admin? }
end
```
---
# 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 `acts_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 Avo::Resources::CourseLink < 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 `acts_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 Avo::Resources::CourseLink < 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 have the order buttons on the view or a resource. That's the default value for the `visible_on` option.
```ruby{3}
class Avo::Resources::CourseLink < Avo::BaseResource
self.ordering = {
visible_on: :index,
}
end
```
## Display the button on a `has_many` association
Another 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 view but only in the association section (in a has many association).
To control that, you can use the `visible_on` option and set it to `:association`.
```ruby{3}
class Avo::Resources::CourseLink < Avo::BaseResource
self.ordering = {
visible_on: :association,
}
end
```
### Possible values
The possible values for the `visible_on` option 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 [`index_query`](https://docs.avohq.io/3.0/customization.html#custom-query-scopes) to alter the query in Avo.
```ruby{2-4}
class Avo::Resources::CourseLink < Avo::BaseResource
self.index_query = -> {
query.order(position: :asc)
}
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
```
## Reorder using drag and drop
Sometimes just picking up a record and dropping it in the position that you'd like it to be. That's exactly what this feature does.
It's disabled by default but you can enable it by adding `drag_and_drop: true` and `insert_at` options to the `self.ordering` hash.
```ruby{5,11}
self.ordering = {
display_inline: true,
visible_on: %i[index association], # :index or :association or both
# position: -> { record.position },
drag_and_drop: true,
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 },
insert_at: -> { record.insert_at position }
}
}
```
### Custom `position` attribute
Using the `position` option you can specify the record's `position` attribute. The default is `record.position`.
```ruby{4}
self.ordering = {
display_inline: true,
visible_on: %i[index association], # :index or :association or both
position: -> { record.position_in_list },
drag_and_drop: true,
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 },
insert_at: -> { record.insert_at position }
}
}
```
## Authorization
If you're using the authorization feature please ensure you give the proper permissions using the `reorder?` method.
```ruby
class CourseLinkPolicy < ApplicationPolicy
def reorder? = edit?
# or a custom permission
def reorder?
user.can_reorder_items?
end
# other policy methods
end
```
---
# Discreet Information
Sometimes you need to have some information available on the record page, but not necesarily front-and-center.
This is where the Discreet Information option is handy.
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.discreet_information = [
:timestamps,
{
tooltip: -> { sanitize("Product is #{record.published_at ? "published" : "draft"}", tags: %w[strong]) },
icon: -> { "heroicons/outline/#{record.published_at ? "eye" : "eye-slash"}" }
},
{
label: -> { record.published_at ? "π" : "π¬" },
url: -> { "https://avohq.io" },
url_target: :_blank
}
]
end
```
## Display the `id`
To save field space, you can use the discreet information area to display the id of the current record.
Set the option to the `:id` value and the id will be added next to the title.
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.discreet_information = :id
# fields and other resource configuration
end
```
You can alternatively use `:id_badge` to display the id as a badge.
## Display the `created_at` and `updated_at` timestamps
The reason why we built this feature was that we wanted a place to display the created and updated at timestamps but didn't want to use up a whole field for it.
That's why this is the most simple thing to add.
Set the option to the `:timestamps` value and a new icon will be added next to the title. When the user hovers over the icon, they will see the record's default timestamps.
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.discreet_information = :timestamps
# fields and other resource configuration
end
```
If the record doesn't have the `created_at` or `updated_at` attributes, they will be ommited.
You can alternatively use `:timestamps_badge` to display the timestamps as a badge.
## Options
You may fully customize the discreet information item by taking control of different options.
To do that, you can set it to a `Hash` with various keys.
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.discreet_information = {
tooltip: -> { "Product is #{record.published_at ? "published" : "draft"}" },
icon: -> { "heroicons/outline/#{record.published_at ? "eye" : "eye-slash"}" }
url: -> { main_app.post_path record }
}
end
```
## Display multiple pieces of information
You can use it to display one or more pieces of information.
## Information properties
Each piece of information has a fe
## Full configuration
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.discreet_information = [
:timestamps,
{
tooltip: -> { sanitize("Product is #{record.published_at ? "published" : "draft"}", tags: %w[strong]) },
icon: -> { "heroicons/outline/#{record.published_at ? "eye" : "eye-slash"}" }
},
{
label: -> { record.published_at ? "β " : "π" },
url: -> { "https://avohq.io" },
url_target: :_blank
}
]
# fields and other resource configuration
end
```
---
# Customizable controls
One of the things that we wanted to support from day one is customizable controls on resource pages, and now, Avo supports customizable controls on , , and views and for the table row.
## Default controls
By default, Avo displays a few buttons (controls) for the user to use on the , , and views which you can override using the appropriate resource options.
## Customize the controls
You can take over and customize them all using the `index_controls`, `show_controls`, `edit_controls`, and `row_controls` class attributes.
## 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 WARNING (**NOT** applicable for versions greater than )
When you use the `action` helper in any customizable block it will act only as a shortcut to display the action button, it will not also register it to the resource.
You must manually register it with the `action` declaration.
```ruby{6-8,13-15}
class Avo::Resources::Fish < Avo::BaseResource
self.title = :name
self.show_controls = -> do
# In order to use it here
action Avo::Actions::ReleaseFish, style: :primary, color: :fuchsia, arguments: {
action_on_show_controls: "Will use this arguments"
}
end
# π Also declare it here π
def actions
action Avo::Actions::ReleaseFish, arguments: {
action_from_list: "Will use this arguments"
}
end
end
```
:::
## Control Options
Some controls take options. Not all controls take all options.
Example: The `link_to` control is the only one that will take the `target` option, but most other controls use the `class` option.
## Default values
If you're curious what are the default controls Avo adds for each block, here they are:
```ruby
# show controls
back_button
delete_button
detach_button
actions_list
edit_button
# form (edit & new) controls
back_button
delete_button
actions_list
save_button
# index controls
attach_button
actions_list
create_button
# row controls
order_controls
show_button
edit_button
detach_button
delete_button
```
## 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 you can use regular `if`/`else` statements because the action declaration is wrapped in a block.
```ruby{6-8}
class Avo::Resources::Fish < Avo::BaseResource
self.show_controls = -> do
back_button label: "", title: "Go back now"
# visibility conditional
if record.something?
action Avo::Actions::ReleaseFish, style: :primary, color: :fuchsia, icon: "heroicons/outline/globe"
end
edit_button label: ""
end
end
```
---
# Cover and Profile photos
It's common to want to display the information in different ways than just "key" and "value". That's why Avo has rich fields like `key_value`, `trix`, `tip_tap`, `files`, and more.
Avo now also has the Cover and Profile photo areas where you can customize the experience even more.
The APIs used are pretty similar and easy to use.
## Profile photo
The `profile_photo` option takes two arguments: `visible_on` and `source`.
```ruby
self.profile_photo = {
source: -> {
if view.index?
# We're on the index page and don't have a record to reference
DEFAULT_IMAGE
else
# We have a record so we can reference it's profile_photo
record.profile_photo
end
}
}
```
## Cover photo
The `cover_photo` option takes three arguments: `size`, `visible_on`, and `source`.
```ruby
self.cover_photo = {
size: :md, # :sm, :md, :lg
visible_on: [:show, :forms], # can be :show, :index, :edit, or a combination [:show, :index]
source: -> {
if view.index?
# We're on the index page and don't have a record to reference
DEFAULT_IMAGE
else
# We have a record so we can reference it's cover_photo
record.cover_photo
end
}
}
```
---
# Array
The `Array` field in allows you to display and manage structured array data. This field supports flexibility in fetching and rendering data, making it suitable for various use cases.
:::tip Important
To use the `Array` field, you must create a resource specifically for it. Refer to the Array Resource documentation for detailed instructions.
For example, to use `field :attendees, as: :array`, you can generate an array resource by running the following command:
```bash
rails generate avo:resource Attendee --array
```
This step ensures the proper setup of your array field within the Avo framework.
:::
### Example 1: Array field with a block
You can define array data directly within a block. This is useful for static or pre-configured data:
```ruby{3-8}
class Avo::Resources::Course < Avo::BaseResource
def fields
field :attendees, as: :array do
[
{ id: 1, name: "John Doe", role: "Software Developer", organization: "TechCorp" },
{ id: 2, name: "Jane Smith", role: "Data Scientist", organization: "DataPros" }
]
end
end
end
```
:::warning Authorization
The `array` field internally inherits many behaviors from `has_many`, including authorization. If you are using authorization and the array field is not rendering, it is most likely not authorized.
To explicitly authorize it, define the following method in the resource's policy:
```ruby{3}
# app/policies/course_policy.rb
class CoursePolicy < ApplicationPolicy
def view_attendees? = true
end
```
For more details, refer to the view_{association}? documentation.
:::
### Example 2: Array field fetching data from the model's method
If no block is defined, Avo will attempt to fetch data by calling the corresponding method on the model:
```ruby
class Course < ApplicationRecord
def attendees
User.all.first(6) # Example fetching first 6 users
end
end
```
Here, the `attendees` field will use the `attendees` method from the `Course` model to render its data dynamically.
### Example 3: Fallback to the `records` method
If neither the block nor the model's method exists, Avo will fall back to the `records` method defined in the resource used to render the array field. This is useful for providing a default dataset.
When neither a block nor a model's method is defined, Avo will fall back to the `records` method in the resource used to render the field. This is a handy fallback for providing default datasets:
```ruby
class Avo::Resources::Attendee < Avo::Resources::ArrayResource
def records
[
{ id: 1, name: "Default Attendee", role: "Guest", organization: "DefaultOrg" }
]
end
end
```
## Summary of Data Fetching Hierarchy
When using `has_many` with `array: true`, Avo will fetch data in the following order:
1. Use data returned by the **block** provided in the field.
2. Fetch data from the **associated model method** (e.g., `Course#attendees`).
3. Fall back to the **`records` method** defined in the resource.
This hierarchy provides maximum flexibility and ensures seamless integration with both dynamic and predefined datasets.
---
# 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.
### DB payload example
An example of a boolean group object stored in the database:
```ruby
{
"admin": true,
"manager": true,
"writer": true,
}
```
### Field declaration example
Below is an example of declaring a `boolean_group` field for roles that matches the DB value from the example above:
```ruby
field :roles,
as: :boolean_group,
name: "User roles",
options: {
admin: "Administrator",
manager: "Manager",
writer: "Writer"
}
```
## Updates
Before version Avo would override the whole attribute with only the payload sent from the client.
```json
// Before update.
{
"feature_enabled": true,
"another_feature_enabled": false,
"something_else": "some_value" // this will disappear
}
// After update.
{
"feature_enabled": true,
"another_feature_enabled": false,
}
```
will only update the keys that you send from the client.
```json
// Before update.
{
"feature_enabled": true,
"another_feature_enabled": false,
"something_else": "some_value" // this will be kept
}
// After update.
{
"feature_enabled": true,
"another_feature_enabled": false,
"something_else": "some_value"
}
```
---
# Code
The `Code` field generates a code editor using [codemirror](https://codemirror.net/) package. This field is hidden on **Index** view.
```ruby
field :custom_css, as: :code, theme: 'dracula', language: 'css'
```
## Options
---
# 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`.
:::
---
# EasyMDE
:::info
Before Avo 3.17 this field was called `markdown`. It was renamed to `easy_mde` so we can add our own implementation with `markdown`.
:::
The `easy_mde` field renders a [EasyMDE Markdown Editor](https://github.com/Ionaru/easy-markdown-editor) and is associated with a text or textarea column in the database.
`easy_mde` field converts text within the editor into raw Markdown text and stores it back in the database.
```ruby
field :description, as: :easy_mde
```
:::info
The `easy_mde` field is hidden from the **Index** view.
:::
## Options
---
# External image
You may have a field in the database that has the URL to an image, and you want to display that in Avo. That is where the `ExternalImage` field comes in to help.
It will take that value, insert it into an `image_tag`, and display it on the `Index` and `Show` views.
```ruby
field :logo, as: :external_image
```
## Options
All options can be static values or procs that are executed within Avo's execution context. When using procs, you have access to all the defaults that `Avo::ExecutionContext` provides plus:
- `record`
- `resource`
- `view`
- `field`
## Conditional sizing based on view
You can use procs to set different image dimensions and styling based on the current view:
```ruby
field :logo, as: :external_image,
width: -> { view.index? ? 40 : 150 },
height: -> { view.index? ? 40 : 150 },
radius: -> { view.index? ? 4 : 12 }
```
This example will display smaller, slightly rounded images on the index view (40x40px with 4px radius) and larger, more rounded images on the show view (150x150px with 12px radius).
## Use computed values
Another common scenario is to use a value from your database and create a new URL using a computed value.
```ruby
field :logo, as: :external_image do
"//logo.clearbit.com/#{URI.parse(record.url).host}?size=180"
rescue
nil
end
```
## Use in the Grid `cover` position
Another common place you could use it is in the grid `:cover` position.
```ruby
cover :logo, as: :external_image, link_to_record: true do
"//logo.clearbit.com/#{URI.parse(record.url).host}?size=180"
rescue
nil
end
```
---
# File
:::warning
You must manually require `activestorage` and `image_processing` gems in your `Gemfile`.
```ruby
# Active Storage makes it simple to upload and reference files
gem "activestorage"
# High-level image processing wrapper for libvips and ImageMagick/GraphicsMagick
gem "image_processing"
```
:::
The `File` field is the fastest way to implement file uploads in a Ruby on Rails app using [Active Storage](https://edgeguides.rubyonrails.org/active_storage_overview.html).
Avo will use your application's Active Storage settings with any supported [disk services](https://edgeguides.rubyonrails.org/active_storage_overview.html#disk-service).
```ruby
field :avatar, as: :file, is_image: true
```
## Authorization
:::info
Please ensure you have the `upload_{FIELD_ID}?`, `delete_{FIELD_ID}?`, and `download_{FIELD_ID}?` methods set on your model's **Pundit** policy. Otherwise, the input and download/delete buttons will be hidden.
:::
**Related:**
- Attachment pundit policies
## Variants
When using the `file` field to display an image, you can opt to show a processed variant of that image. This can be achieved using the `format_using` option.
### Example:
```ruby{3-5}
field :photo,
as: :file,
format_using: -> {
value.variant(resize_to_limit: [150, 150]).processed.image
}
```
## Options
---
# 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
"#{record.google_username}@gmail.com"
end
```
---
# Heading
:::code-group
```ruby [Field id]
field :user_information, as: :heading
```
```ruby [Label]
field :some_id, as: :heading, label: "user information"
```
```ruby [Computed]
field :some_id, as: :heading do
"user information"
end
```
:::
The `Heading` field displays a header that acts as a separation layer between different sections.
`Heading` is not assigned to any column in the database and is only visible on the `Show`, `Edit` and `Create` views.
:::warning Computed heading
The computed fields are not rendered on form views, same with heading field, if computed syntax is used it will not be rendered on the form views. Use `label` in order to render it on **all** views.
:::
## Options
---
# Hidden
There are scenarios where in order to be able to submit a form, an input should be present but inaccessible to the user. An example of this might be where you want to set a field by default without the option to change, or see it. `Hidden` will render a `` element on the `Edit` and `New` page.
> Hidden will only render on the `Edit` and `New` views.
### Example usage:
```ruby
# Basic
field :group_id, as: :hidden
# With default
field :user_id, as: :hidden, default: -> { current_user.id }
# If the current_user is a admin
# 1. Allow them to see and select a user.
# 2. Remove the user_id field to prevent user_id it from overriding the user selection.
# Otherwise set the user_id to the current user and hide the field.
field :user, as: :belongs_to, visible: -> { context[:current_user].admin? }
field :user_id, as: :hidden, default: -> { current_user.id }, visible: -> { !context[:current_user].admin? }
```
---
# ID
The `id` field is used to show the record's id. By default, it's visible only on the `Index` and `Show` views. That is a good field to add the `link_to_record` option to make it a shortcut to the record `Show` page.
```ruby
field :id, as: :id
```
## Options
---
# KeyValue
The `KeyValue` field makes it easy to edit flat key-value pairs stored in `JSON` format in the database.
```ruby
field :meta, as: :key_value
```
## Options
## Customizing the labels
You can easily customize the labels displayed in the UI by mentioning custom values in `key_label`, `value_label`, `action_text`, and `delete_text` properties when defining the field.
```ruby
field :meta, # The database field ID
as: :key_value, # The field type.
key_label: "Meta key", # Custom value for key header. Defaults to 'Key'.
value_label: "Meta value", # Custom value for value header. Defaults to 'Value'.
action_text: "New item", # Custom value for button to add a row. Defaults to 'Add'.
delete_text: "Remove item" # Custom value for button to delete a row. Defaults to 'Delete'.
```
## Enforce restrictions
You can enforce some restrictions by removing the ability to edit the field's key or value by setting `disable_editing_keys` or `disable_editing_values` to `true` respectively. If `disable_editing_keys` is set to `true`, be aware that this option will also disable adding rows as well. You can separately remove the ability to add a new row by setting `disable_adding_rows` to `true`. Deletion of rows can be enforced by setting `disable_deleting_rows` to `true`.
```ruby
field :meta, # The database field ID
as: :key_value, # The field type.
disable_editing_keys: false, # Option to disable the ability to edit keys. Implies disabling to add rows. Defaults to false.
disable_editing_values: false, # Option to disable the ability to edit values. Defaults to false.
disable_adding_rows: false, # Option to disable the ability to add rows. Defaults to false.
disable_deleting_rows: false # Option to disable the ability to delete rows. Defaults to false.
```
Setting `disabled: true` enforces all restrictions by disabling editing keys, editing values, adding rows, and deleting rows collectively.
```ruby
field :meta, # The database field ID
as: :key_value, # The field type.
disabled: true, # Option to disable editing keys, editing values, adding rows, and deleting rows. Defaults to false.
```
`KeyValue` is hidden on the `Index` view.
---
# Location
The `Location` field is used to display a point on a map.
```ruby
field :coordinates, as: :location
```
:::warning
You need to add the `mapkick-rb` (not `mapkick`) gem to your `Gemfile` and have the `MAPBOX_ACCESS_TOKEN` environment variable with a valid [Mapbox](https://account.mapbox.com/auth/signup/) key.
:::
## Description
By default, the location field is attached to one database column that has the coordinates in plain text with a comma `,` joining them (`latitude,longitude`).
Ex: `44.427946,26.102451`
Avo will take that value, split it by the comma and use the first element as the `latitude` and the second one as the `longitude`.
On the view you'll get in interactive map and on the edit you'll get one field where you can edit the coordinates.
## Options
---
# Markdown
:::info
In Avo 3.17 we renamed the `markdown` field `easy_mde` and introduced this custom one based on the [Marksmith editor](https://github.com/avo-hq/marksmith).
Please read the docs on the repo for more information on how it works.
:::
This field is inspired by the wonderful GitHub editor we all love and use.
It supports applying styles to the markup, dropping files in the editor, and using the Media Library.
The uploaded files will be taken over by Rails and persisted using Active Storage.
```ruby
field :body, as: :markdown
```
:::warning
Please ensure you have these gems in your `Gemfile`.
```ruby
gem "marksmith"
gem "commonmarker"
```
:::
## Supported features
- [x] ActiveStorage file attachments
- [x] Media Library integration
- [x] Preview panel
- [x] [Ready-to-use renderer](https://github.com/avo-hq/marksmith#built-in-preview-renderer)
- [x] Text formatting
- [x] Lists
- [x] Links
- [x] Images
- [x] Tables
- [x] Code blocks
- [x] Headings
## Customize the renderer
There are two places where we parse the markdown into the HTML you see.
1. In the controller
2. In the field component
You may customize the renderer by overriding the model.
```ruby
# app/models/marksmith/renderer.rb
module Marksmith
class Renderer
def initialize(body:)
@body = body
end
def render
if Marksmith.configuration.parser == "commonmarker"
render_commonmarker
elsif Marksmith.configuration.parser == "kramdown"
render_kramdown
else
render_redcarpet
end
end
def render_commonmarker
# commonmarker expects an utf-8 encoded string
body = @body.to_s.dup.force_encoding("utf-8")
Commonmarker.to_html(body)
end
def render_redcarpet
::Redcarpet::Markdown.new(
::Redcarpet::Render::HTML,
tables: true,
lax_spacing: true,
fenced_code_blocks: true,
space_after_headers: true,
hard_wrap: true,
autolink: true,
strikethrough: true,
underline: true,
highlight: true,
quote: true,
with_toc_data: true
).render(@body)
end
def render_kramdown
body = @body.to_s.dup.force_encoding("utf-8")
Kramdown::Document.new(body).to_html
end
end
end
```
---
# Money
The `Money` field is used to display a monetary value.
```ruby
field :price, as: :money, currencies: %w[EUR USD RON PEN]
```
## Money Field Example
You can explore the implementation of the money field in [avodemo](https://main.avodemo.com/avo/resources/products/new) and it's corresponding code on GitHub [here](https://github.com/avo-hq/main.avodemo.com/blob/main/app/avo/resources/product.rb)
### Example on new
### Example on show with currencies USD
### Example on show with currencies RON
### Example on index
## Installation
This field is a standalone gem.
You have to add it to your `Gemfile` alongside the `money-rails` gem.
:::info Add this field to the `Gemfile`
```ruby
# Gemfile
gem "avo-money_field"
gem "money-rails", "~> 1.12"
```
:::
:::warning Important: Monetization Requirement
In order to fully utilize the money field's features, you must monetize the associated attribute at the model level using the `monetize` method from the `money-rails` gem. ([Usage example](https://github.com/RubyMoney/money-rails?tab=readme-ov-file#usage-example))
For example:
```ruby
monetize :price_cents
```
Without this step, the money field may not behave as expected, and the field might not render.
:::
## Options
---
# Number
The `number` field renders a `input[type="number"]` element.
```ruby
field :age, as: :number
```
## Options
## Examples
```ruby
field :age, as: :number, min: 0, max: 120, step: 5
```
---
# Password
The `Password` field renders a `input[type="password"]` element for that field. By default, it's visible only on the `Edit` and `New` views.
```ruby
field :password, as: :password
```
#### Revealable
You can set the `revealable` to true to show an "eye" icon that toggles the password between hidden or visible.
**Related:**
- Devise password optional
---
# Preview
The `Preview` field adds a tiny icon to each row on the view that, when hovered, it will display a preview popup with more information regarding that record.
```ruby
field :preview, as: :preview
```
## Define the fields
The fields shown in the preview popup are configured similarly to how you configure the visibility in the different views.
When you want to display a field in the preview popup simply call the `show_on :preview` option on the field.
```ruby
field :name, as: :text, show_on :preview
```
## Authorization
Since version the preview request authorization is controller with the `preview?` policy method.
---
# Progress bar
The `ProgressBar` field renders a `progress` element on `Index` and `Show` views and and a `input[type=range]` element on `Edit` and `New` views.
```ruby
field :progress, as: :progress_bar
```
## Options
## Examples
```ruby
field :progress,
as: :progress_bar,
max: 150,
step: 10,
display_value: true,
value_suffix: "%"
```
---
# Radio
The `Radio` field is used to render radio buttons. It's useful when only one value can be selected in a given options group.
### Field declaration example
Below is an example of declaring a `radio` field for a role:
```ruby
field :role,
as: :radio,
name: "User role",
options: {
admin: "Administrator",
manager: "Manager",
writer: "Writer"
}
```
---
# Record link
Sometimes you just need to link to a field. That's it!
This is what this field does. You give it a record and it will link to it.
That record can come off an association a method or any kind of property on the record instance.
:::info Add this field to the `Gemfile`
```ruby
# Gemfile
gem "avo-record_link_field"
```
:::
:::warning
That record you're pointing to should have a resource configured.
:::
```ruby{14,19}
class Comment < ApplicationRecord
# Your model must return an instance of a record
has_one :post
# or
belongs_to :post
# or
def post
# trivially find a post
Post.find 42
end
end
# Calling the method like so will give us an instance of a Post
Comment.first.post => #
class Avo::Resources::Comment < Avo::BaseResource
def fields
# This will run `record.post` and try to display whatever is returned.
field :post, as: :record_link
end
end
```
## Options
Besides some of the default options, there are a few custom ones.
## Using computed values
Of course you can take full control of this field and use your computed values too.
In order to do that, open a block and run some ruby query to return an instance of a record.
#### Example
```ruby
field :post, as: :record_link do
# This will generate a link similar to this
# https://example.com/avo/resources/posts/42
Post.find 42
end
# or
field :creator, as: :record_link, add_via_params: false do
user_id = SomeService.new(comment: record).fetch_user_id # returns 31
# This will generate a link similar to this
# https://example.com/avo/resources/users/31
User.find user_id
end
# or
field :creator, as: :record_link, use_resource: "AdminUser", add_via_params: false do
user_id = SomeService.new(comment: record).fetch_user_id # returns 31
# This will generate a link similar to this
# https://example.com/avo/resources/admin_users/31
User.find user_id
end
```
---
# Rhino
The wonderful [Rhino Editor](https://rhino-editor.vercel.app/) built by [Konnor Rogers](https://www.konnorrogers.com/) is available and fully integrated with Avo.
```ruby
field :body, as: :rhino
```
:::info
Add this line to your application's `Gemfile`:
```ruby
gem "avo-rhino_field"
```
:::
Rhino is based on [TipTap](https://tiptap.dev/) which is a powerful and flexible WYSIWYG editor.
It supports [ActiveStorage](https://guides.rubyonrails.org/active_storage_overview.html) file attachments, [ActionText](https://guides.rubyonrails.org/action_text_overview.html), and seamlessly integrates with the Media Library.
## Options
## Contributing
You can contribute to the Rhino editor by visiting the [GitHub repository](https://github.com/avo-hq/avo-rhino_field).
---
# Select
The `Select` field renders a `select` field.
```ruby
field :type, as: :select, options: { 'Large container': :large, 'Medium container': :medium, 'Tiny container': :tiny }, display_value: true, placeholder: 'Choose the type of the container.'
```
---
# Status
Displays the status of a record in three ways; `loading`, `failed`, `success`, or `neutral`.
You may select the `loading`, `failed`, and `success` state values, and everything else will fall back to `neutral`.
```ruby
field :progress,
as: :status,
failed_when: [:closed, :rejected, :failed],
loading_when: [:loading, :running, :waiting, "in progress"],
success_when: [:done],
```
## Options
---
# Tags field
Adding a list of things to a record is something we need to do pretty frequently; that's why having the `tags` field is helpful.
```ruby
field :skills, as: :tags
```
## Options
## PostgreSQL array fields
You can use the tags field with the PostgreSQL array field.
```ruby{11}
# app/avo/resources/course.rb
class Avo::Resources::Course < Avo::BaseResource
def fields
field :skills, as: :tags
end
end
# db/migrate/add_skills_to_courses.rb
class AddSkillsToCourses < ActiveRecord::Migration[6.0]
def change
add_column :courses, :skills, :text, array: true, default: []
end
end
```
## Acts as taggable on
One popular gem used for tagging is [`acts-as-taggable-on`](https://github.com/mbleigh/acts-as-taggable-on). The tags field integrates very well with it.
You need to add `gem 'acts-as-taggable-on', '~> 9.0'` in your `Gemfile`, add it to your model `acts_as_taggable_on :tags`, and use `acts_as_taggable_on` on the field.
```ruby{6}
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
def fields
field :tags,
as: :tags,
acts_as_taggable_on: :tags,
close_on_select: false,
placeholder: 'add some tags',
suggestions: -> { Post.tags_suggestions },
enforce_suggestions: true,
help: 'The only allowed values here are `one`, `two`, and `three`'
end
end
# app/models/post.rb
class Post < ApplicationRecord
acts_as_taggable_on :tags
end
```
That will let Avo know which attribute should be used to fill with the user's tags.
:::info Related
You can set up the tags as a resource using this guide.
:::
## Array fields
We haven't tested all the scenarios, but the tags field should play nicely with any array fields provided by Rails.
```ruby{10-12,14-16}
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
def fields
field :items, as: :tags
end
end
# app/models/post.rb
class Post < ApplicationRecord
def items=(items)
puts ["items->", items].inspect
end
def items
%w(1 2 3 4)
end
end
```
---
# Text
The `Text` field renders a regular `` element.
```ruby
field :title, as: :text
```
## Options
## Customization
You may customize the `Text` field with as many options as you need.
```ruby
field :title, # The database field ID
as: :text, # The field type
name: 'Post title', # The label you want displayed
required: true, # Display it as required
readonly: true, # Display it disabled
as_html: true # Should the output be parsed as html
placeholder: 'My shiny new post', # Update the placeholder text
format_using: -> { value.truncate 3 } # Format the output
```
---
# Textarea
The `textarea` field renders a `` element.
:::tip
By default, the `textarea` field don't have a component for the Index view. For this reason, on the Index view the field is not even visible.
Follow the Generating a custom component for a field guide to add a component to the index view for this field.
:::
```ruby
field :body, as: :textarea
```
## Options
---
# Time
The `Time` field is similar to the DateTime field and uses the time picker of flatpickr (without the calendar). You can use the `time_24hr` option for flatpickr to use the 24-hour format. Add the option `relative: false` if you want the time to stay absolute and not change based on the browser's timezone.
```ruby
field :starting_at,
as: :time,
picker_format: 'H:i',
format: "HH:mm",
relative: true,
picker_options: {
time_24hr: true
}
```
---
# Tip Tap
The `TipTap` field is deprecated in favor of the Rhino field.
The Rhino field is a fork of the TipTap editor with some additional features and improvements.
The Rhino field is fully integrated with Avo and provides a seamless experience for managing rich text content using the [ActiveStorage](https://guides.rubyonrails.org/active_storage_overview.html) integration and the Media Library.
---
# Trix
```ruby
field :body, as: :trix
```
The `Trix` field renders a [WYSIWYG Editor](https://trix-editor.org/) and can be associated with a `string` or `text` column in the database. The value stored in the database will be the editor's resulting `HTML` content.
It supports [ActiveStorage](https://guides.rubyonrails.org/active_storage_overview.html) file attachments, [ActionText](https://guides.rubyonrails.org/action_text_overview.html), and seamlessly integrates with the Media Library.
Trix field is hidden from the `Index` view.
## Options
## File attachments
:::warning
You must manually require `activestorage` and `image_processing` gems in your `Gemfile`.
```ruby
# Active Storage makes it simple to upload and reference files
gem "activestorage"
# High-level image processing wrapper for libvips and ImageMagick/GraphicsMagick
gem "image_processing"
```
:::
Trix supports drag-and-drop file attachments. To enable **Active Storage** integration, you must add the `attachment_key` option to your Trix field.
```ruby
field :body, as: :trix, attachment_key: :trix_attachments
```
That `attachment_key` has to have the same name as the model.
```ruby{2}
class Post < ApplicationRecord
has_many_attached :trix_attachments
end
```
Now, when you upload a file in the Trix field, Avo will create an Active Record attachment.
## Disable attachments
You may want to use Trix only as a text editor and disable the attachments feature. Adding the `attachments_disabled` option will hide the attachments button (paperclip icon).
```ruby
field :body, as: :trix, attachments_disabled: true
```
## Remove attachment attributes
By default, Trix will add some meta-data in the editor (filename, filesize, and URL) when adding an attachment. You might not need those to be present in the document. You can hide them using `hide_attachment_filename`, `hide_attachment_filesize`, and `hide_attachment_url`.
## Active Storage
Trix integrates seamlessly with Active Storage. When you use it with a plain database column on a record table (not with Action Text) you have to set the `attachment_key` option (documented above).
## Action Text
Trix integrates seamlessly with Action Text. It will automatically work with Action Text as well and it won't require you to add an `attachment_key`.
## Demo app
We prepared a [demo](https://trix.avodemo.com/) to showcase Trix's abilities to work with Action Text and Active Storage.
## Javascript Alert Messages
You can customize the javascript alert messages for various actions in the Trix editor. Below are the default messages that can be translated or modified:
```yml
avo:
this_field_has_attachments_disabled: This field has attachments disabled.
you_cant_upload_new_resource: You can't upload files into the Trix editor until you save the resource.
you_havent_set_attachment_key: You haven't set an `attachment_key` to this Trix field.
```
Refer to the [default](https://github.com/avo-hq/avo/blob/main/lib/generators/avo/templates/locales/avo.en.yml) for more details.
---
# Associations
One of the most amazing things about Ruby on Rails is how easy it is to create [Active Record associations](https://guides.rubyonrails.org/association_basics.html) between models. We try to keep the same simple approach in Avo too.
:::warning
It's important to set the `inverse_of` as often as possible to your model's association attribute.
:::
- Belongs to
- Has one
- Has many
- Has many through
- Has and belongs to many
## Single Table Inheritance (STI)
When you have models that share behavior and fields with STI, Rails will cast the model as the final class no matter how you query it.
```ruby
# app/models/user.rb
class User < ApplicationRecord
end
# app/models/super_user.rb
class SuperUser < User
end
# User.all.map(&:class) => [User, SuperUser]
```
For example, when you have two models, `User` and `SuperUser` with STI, when you call `User.all`, Rails will return an instance of `User` and an instance of `SuperUser`. That confuses Avo in producing the proper resource of `User`. That's why when you deal with STI, the final resource `Avo::Resources::SuperUser` should receive the underlying `model_class` so Avo knows which model it represents.
```ruby{5}
# app/avo/resources/super_user.rb
class Avo::Resources::SuperUser < Avo::BaseResource
self.title = :name
self.includes = []
self.model_class = "SuperUser"
def fields
field :id, as: :id
field :name, as: :text
end
end
```
## Link to child resource when using STI
Let's take another example. We have a `Person` model and `Sibling` and `Spouse` models that inherit from it.
You may want to use the `Avo::Resources::Person` to list all the records, but when your user clicks on a person, you want to use the inherited resources (`Avo::Resources::Sibiling` and `Avo::Resources::Spouse`) to display the details. The reason is that you may want to display different fields or resource tools for each resource type.
There are two ways you can use this:
1. `self.link_to_child_resource = true` Declare this option on the parent resource. When a user is on the view of your the `Avo::Resources::Person` and clicks on the view button of a `Person` they will be redirected to a `Child` or `Spouse` resource instead of a `Person` resource.
2. `field :peoples, as: :has_many, link_to_child_resource: false` Use it on a `has_many` field. On the `Avo::Resources::Person` you may want to show all the related people on the page, but when someone click on a record, they are redirected to the inherited `Child` or `Spouse` resource.
## Add custom labels to the associations' pages
You might want to change the name that appears on the association page. For example, if you're displaying a `team_members` association, your users will default see `Team members` as the title, but you'd like to show them `Members`.
You can customize that using fields localization.
---
# Belongs to
```ruby
field :user, as: :belongs_to
```
You will see three field types when you add a `BelongsTo` association to a model.
## Options
:::warning
The `attach_scope` will not filter the records in the listing from `has_many` or `has_and_belongs_to_many` associations.
Use [`scope`](#scope) or a Pundit policy `Scope` for that.
:::
```ruby-vue{3}
field :members,
as: :{{ $frontmatter.field_type }},
attach_scope: -> { query.where.not(team_id: parent.id) }
```
In this example, in the `attach_scope`, we ensure that when attaching members to a team, only those who are not already members will appear in the list of options.
## Overview
On the `Index` and `Show` views, Avo will generate a link to the associated record containing the `self.title` value of the target resource.
On the `Edit` and `New` views, Avo will generate a dropdown element with the available records where the user can change the associated model.
## Polymorphic `belongs_to`
To use a polymorphic relation, you must add the `polymorphic_as` and `types` properties.
```ruby{13}
class Avo::Resources::Comment < Avo::BaseResource
self.title = :id
def fields
field :id, as: :id
field :body, as: :textarea
field :excerpt, as: :text, show_on: :index do
ActionView::Base.full_sanitizer.sanitize(record.body).truncate 60
rescue
""
end
field :commentable, as: :belongs_to, polymorphic_as: :commentable, types: [::Post, ::Project]
end
end
```
## Polymorphic help
When displaying a polymorphic association, you will see two dropdowns. One selects the polymorphic type (`Post` or `Project`), and one for choosing the actual record. You may want to give the user explicit information about those dropdowns using the `polymorphic_help` option for the first dropdown and `help` for the second.
```ruby{17-18}
class Avo::Resources::Comment < Avo::BaseResource
self.title = :id
def fields
field :id, as: :id
field :body, as: :textarea
field :excerpt, as: :text, show_on: :index do
ActionView::Base.full_sanitizer.sanitize(record.body).truncate 60
rescue
""
end
field :reviewable,
as: :belongs_to,
polymorphic_as: :reviewable,
types: [::Post, ::Project, ::Team],
polymorphic_help: "Choose the type of record to review",
help: "Choose the record you need."
end
end
```
## Searchable `belongs_to`
There might be the case that you have a lot of records for the parent resource, and a simple dropdown won't cut it. This is where you can use the `searchable` option to get a better search experience for that resource.
```ruby{8}
class Avo::Resources::Comment < Avo::BaseResource
self.title = :id
def fields
field :id, as: :id
field :body, as: :textarea
field :user, as: :belongs_to, searchable: true
end
end
```
`searchable` works with `polymorphic` `belongs_to` associations too.
```ruby{8}
class Avo::Resources::Comment < Avo::BaseResource
self.title = :id
def fields
field :id, as: :id
field :body, as: :textarea
field :commentable, as: :belongs_to, polymorphic_as: :commentable, types: [::Post, ::Project], searchable: true
end
end
```
:::info
Avo uses the search feature behind the scenes, so **make sure the target resource has the `query` option configured inside the `search` block**.
:::
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.search = {
query: -> {
query.ransack(id_eq: params[:q], name_cont: params[:q], body_cont: params[:q], m: "or").result(distinct: false)
}
}
end
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
self.search = {
query: -> {
query.ransack(id_eq: params[:q], name_cont: params[:q], country_cont: params[:q], m: "or").result(distinct: false)
}
}
end
```
## Belongs to attach scope
When you edit a record that has a `belongs_to` association, on the edit screen, you will have a list of records from which you can choose a record to associate with.
For example, a `Post` belongs to a `User`. So on the post edit screen, you will have a dropdown (or a search field if it's [searchable](#searchable-belongs-to)) with all the available users. But that's not ideal. For example, maybe you don't want to show all the users in your app but only those who are not admins.
You can use the `attach_scope` option to keep only the users you need in the `belongs_to` dropdown field.
You have access to the `query` that you can alter and return it and the `parent` object, which is the actual record where you want to assign the association (the true `Post` in the below example).
```ruby
# app/models/user.rb
class User < ApplicationRecord
scope :non_admins, -> { where "(roles->>'admin')::boolean != true" }
end
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
def fields
field :user, as: :belongs_to, attach_scope: -> { query.non_admins }
end
end
```
For scenarios where you need to add a record associated with that resource (you create a `Post` through a `Category`), the `parent` is unavailable (the `Post` is not persisted in the database). Therefore, Avo makes the `parent` an instantiated object with its parent populated (a `Post` with the `category_id` populated with the parent `Category` from which you started the creation process) so you can better scope out the data (you know from which `Category` it was initiated).
## Allow detaching via the association
When you visit a record through an association, that `belongs_to` field is disabled. There might be cases where you'd like that field not to be disabled and allow your users to change that association.
You can instruct Avo to keep that field enabled in this scenario using `allow_via_detaching`.
```ruby{12}
class Avo::Resources::Comment < Avo::BaseResource
self.title = :id
def fields
field :id, as: :id
field :body, as: :textarea
field :commentable,
as: :belongs_to,
polymorphic_as: :commentable,
types: [::Post, ::Project],
allow_via_detaching: true
end
end
```
---
# Has One
:::warning
It's important to set the `inverse_of` as often as possible to your model's association attribute.
:::
# Has One
The `HasOne` association shows the unfolded view of your `has_one` association. It's like peaking on the `Show` view of that associated record. The user can also access the `Attach` and `Detach` buttons.
```ruby
field :admin, as: :has_one
```
## Options
:::warning
The `attach_scope` will not filter the records in the listing from `has_many` or `has_and_belongs_to_many` associations.
Use [`scope`](#scope) or a Pundit policy `Scope` for that.
:::
```ruby-vue{3}
field :members,
as: :{{ $frontmatter.field_type }},
attach_scope: -> { query.where.not(team_id: parent.id) }
```
In this example, in the `attach_scope`, we ensure that when attaching members to a team, only those who are not already members will appear in the list of options.
## Show on edit screens
By default, the `{{ $frontmatter.field_type }}` field is only visible in the show view. To make it available in the edit view as well, include the `show_on: :edit` option. This ensures that the `{{ $frontmatter.field_type }}` show view component is also rendered within the edit view.
## Nested in Forms
You can use ["Show on edit screens"](#show-on-edit-screens) to make the `{{ $frontmatter.field_type }}` field available in the edit view. However, this will render it using the show view component.
To enable nested creation for the `{{ $frontmatter.field_type }}` field, allowing it to be created and / or edited alongside its parent record within the same form, use the `nested` option which is a hash with configurable option.
Keep in mind that this will display the fieldβs resource as it appears in the edit view.
---
# 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
:::warning
The `attach_scope` will not filter the records in the listing from `has_many` or `has_and_belongs_to_many` associations.
Use [`scope`](#scope) or a Pundit policy `Scope` for that.
:::
```ruby-vue{3}
field :members,
as: :{{ $frontmatter.field_type }},
attach_scope: -> { query.where.not(team_id: parent.id) }
```
In this example, in the `attach_scope`, we ensure that when attaching members to a team, only those who are not already members will appear in the list of options.
## Search query scope
If the resource used for the `has_many` association has the `search` block configured with a `query`, Avo will use that to scope out the search query to that association.
For example, if you have a `Team` model that `has_many` `User`s, now you'll be able to search through that team's users instead of all of them.
You can target that search using `params[:via_association]`. When the value of `params[:via_association]` is `has_many`, the search has been mad inside a has_many association.
For example, if you want to show the records in a different order, you can do this:
```ruby
self.search = {
query: -> {
if params[:via_association] == 'has_many'
query.ransack(id_eq: params[:q], m: "or").result(distinct: false).order(name: :asc)
else
query.ransack(id_eq: params[:q], m: "or").result(distinct: false)
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, the `{{ $frontmatter.field_type }}` field is only visible in the show view. To make it available in the edit view as well, include the `show_on: :edit` option. This ensures that the `{{ $frontmatter.field_type }}` show view component is also rendered within the edit view.
## Nested in Forms
You can use ["Show on edit screens"](#show-on-edit-screens) to make the `{{ $frontmatter.field_type }}` field available in the edit view. However, this will render it using the show view component.
To enable nested creation for the `{{ $frontmatter.field_type }}` field, allowing it to be created and / or edited alongside its parent record within the same form, use the `nested` option which is a hash with configurable option.
Keep in mind that this will display the fieldβs resource as it appears in the edit view.
## Add scopes to associations
When displaying `has_many` associations, you might want to scope out some associated records. For example, a user might have multiple comments, but on the user's `Show` page, you don't want to display all the comments, but only the approved ones.
```ruby{5,16,22}
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :user, optional: true
scope :approved, -> { where(approved: true) }
end
# app/models/user.rb
class User < ApplicationRecord
has_many :comments
end
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
field :comments, as: :has_many, scope: -> { query.approved }
end
end
```
The `comments` query on the user `Index` page will have the `approved` scope attached.
With version 2.5.0, you'll also have access to the `parent` record so that you can use that to scope your associated models even better.
Starting with version 3.12, access to `resource` and `parent_resource` was additionally provided.
All the `has_many` associations have the `attach_scope` option available too.
## Show/hide buttons
You will want to control the visibility of the attach/detach/create/destroy/actions buttons visible throughout your app. You can use the policy methods to do that.
Find out more on the authorization page.
---
# Has And Belongs To Many
The `HasAndBelongsToMany` association works similarly to `HasMany`.
```ruby
field :users, as: :has_and_belongs_to_many
```
## Options
:::warning
The `attach_scope` will not filter the records in the listing from `has_many` or `has_and_belongs_to_many` associations.
Use [`scope`](#scope) or a Pundit policy `Scope` for that.
:::
```ruby-vue{3}
field :members,
as: :{{ $frontmatter.field_type }},
attach_scope: -> { query.where.not(team_id: parent.id) }
```
In this example, in the `attach_scope`, we ensure that when attaching members to a team, only those who are not already members will appear in the list of options.
## Search query scope
If the resource used for the `has_many` association has the `search` block configured with a `query`, Avo will use that to scope out the search query to that association.
For example, if you have a `Team` model that `has_many` `User`s, now you'll be able to search through that team's users instead of all of them.
You can target that search using `params[:via_association]`. When the value of `params[:via_association]` is `has_many`, the search has been mad inside a has_many association.
For example, if you want to show the records in a different order, you can do this:
```ruby
self.search = {
query: -> {
if params[:via_association] == 'has_many'
query.ransack(id_eq: params[:q], m: "or").result(distinct: false).order(name: :asc)
else
query.ransack(id_eq: params[:q], m: "or").result(distinct: false)
end
}
}
```
## Show on edit screens
By default, the `{{ $frontmatter.field_type }}` field is only visible in the show view. To make it available in the edit view as well, include the `show_on: :edit` option. This ensures that the `{{ $frontmatter.field_type }}` show view component is also rendered within the edit view.
## Nested in Forms
You can use ["Show on edit screens"](#show-on-edit-screens) to make the `{{ $frontmatter.field_type }}` field available in the edit view. However, this will render it using the show view component.
To enable nested creation for the `{{ $frontmatter.field_type }}` field, allowing it to be created and / or edited alongside its parent record within the same form, use the `nested` option which is a hash with configurable option.
Keep in mind that this will display the fieldβs resource as it appears in the edit view.
### Searchable `has_and_belongs_to_many`
Similar to `belongs_to`, the `has_many` associations support the `searchable` option.
## Add scopes to associations
When displaying `has_many` associations, you might want to scope out some associated records. For example, a user might have multiple comments, but on the user's `Show` page, you don't want to display all the comments, but only the approved ones.
```ruby{5,16,22}
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :user, optional: true
scope :approved, -> { where(approved: true) }
end
# app/models/user.rb
class User < ApplicationRecord
has_many :comments
end
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
field :comments, as: :has_many, scope: -> { query.approved }
end
end
```
The `comments` query on the user `Index` page will have the `approved` scope attached.
With version 2.5.0, you'll also have access to the `parent` record so that you can use that to scope your associated models even better.
Starting with version 3.12, access to `resource` and `parent_resource` was additionally provided.
All the `has_many` associations have the `attach_scope` option available too.
## Show/hide buttons
You will want to control the visibility of the attach/detach/create/destroy/actions buttons visible throughout your app. You can use the policy methods to do that.
Find out more on the authorization page.
---
# Resource panels
Panels are the backbone of Avo's display infrastructure. Most of the information that's on display is wrapped inside a panel. They help maintain a consistent design throughout Avo's pages. They are also available as a view component `Avo::PanelComponent` for custom tools, and you can make your own pages using it.
When using the fields DSL for resources, all fields declared in the root will be grouped into a "main" panel, but you can add your panels.
```ruby
class Avo::Resources::User < Avo::BaseResource
def fields
field :id, as: :id, link_to_record: true
field :email, as: :text, name: "User Email", required: true
panel name: "User information", description: "Some information about this user" do
field :first_name, as: :text, required: true, placeholder: "John"
field :last_name, as: :text, required: true, placeholder: "Doe"
field :active, as: :boolean, name: "Is active", show_on: :show
end
end
end
```
You can customize the panel `name` and panel `description`.
## What is the Main Panel?
The Main Panel is the primary container for fields in a resource. It typically includes the resource's title, action buttons, and fields that are part of the resource's core data. You can think of it as the central hub for managing and displaying the resource's information.
The Main Panel is automatically created by Avo based on your resource's field definitions. However, you can also customize it to meet your specific requirements.
## How does Avo compute panels?
By default Avo's field organization occurs behind the scenes, leveraging multiple panels to simplify the onboarding process and reduce complexity when granular customization is not needed.
When retrieving the fields, the first step involves categorizing them based on whether or not they have their own panel. Fields without their own panels are referred to as "standalone" fields. Notably, most association fields, such as `field :users, as: :has_many`, automatically have their dedicated panels.
During the Avo's grouping process, we ensure that the fields maintain the order in which they were declared.
Once the groups are established, we check whether the main panel has been explicitly declared within the resource. If it has been declared, this step is skipped. However, if no main panel declaration exists, we compute a main panel and assign the first group of standalone fields to it. This ensures that the field arrangement aligns with your resource's structure and maintains the desired order.
## Computed panels vs Manual customization
Let's focus on the `fields` method for the next examples. In these examples, we demonstrate how to achieve the same field organization using both computed panels and manual customization. Each example have the code that makes Avo compute the panels and also have an example on how to intentionally declare the panels in order to achieve the same result.
:::code-group
```ruby [Computed]
def fields
field :id, as: :id
field :name, as: :text
field :user, as: :belongs_to
field :type, as: :text
end
```
```ruby [Customized]
def fields
main_panel do
field :id, as: :id
field :name, as: :text
field :user, as: :belongs_to
field :type, as: :text
end
end
```
:::
On this example Avo figured out that a main panel was not declared and it computes one with all standalone fields.
Now let's add some field that is not standalone between `name` and `user` fields.
:::code-group
```ruby{5} [Computed]
def fields
field :id, as: :id
field :name, as: :text
field :reviews, as: :has_many
field :user, as: :belongs_to
field :type, as: :text
end
```
```ruby [Customized]
def fields
main_panel do
field :id, as: :id
field :name, as: :text
end
field :reviews, as: :has_many
panel do
field :user, as: :belongs_to
field :type, as: :text
end
end
```
:::
Since the field that has it owns panel was inserted between a bunch of standalone fields Avo will compute a main panel for the first batch of standalone fields (`id` and `name`) and will compute a simple panel for the remaining groups of standalone fields (`user` and `type`)
With these rules on mind we have the ability to keep the resource simple and also to fully customize it, for example, if we want to switch the computed main panel with the computed panel we can declare them in the desired order.
```ruby
def fields
panel do
field :user, as: :belongs_to
field :type, as: :text
end
field :reviews, as: :has_many
main_panel do
field :id, as: :id
field :name, as: :text
end
end
```
By using the `main_panel` and `panel` method, you can manually customize the organization of fields within your resource, allowing for greater flexibility and control.
## Index view fields
By default, only the fields declared in the root and the fields declared inside `main_panel` will be visible on the `Index` view.
```ruby{4-8}
class Avo::Resources::User < Avo::BaseResource
def fields
# Only these fields will be visible on the `Index` view
field :id, as: :id, link_to_record: true
field :email, as: :text, name: "User Email", required: true
field :name, as: :text, only_on: :index do
"#{record.first_name} #{record.last_name}"
end
# These fields will be hidden on the `Index` view
panel name: "User information", description: "Some information about this user" do
field :first_name, as: :text, required: true, placeholder: "John"
field :last_name, as: :text, required: true, placeholder: "Doe"
field :active, as: :boolean, name: "Is active", show_on: :show
end
end
end
```
---
# Several fields in a cluster
:::info
To fully understand this section, you should be familiar with the `stacked` field option and `resource panels`. These concepts will help you structure and customize your fields effectively.
:::
The `cluster` DSL allows you to group multiple fields horizontally within a `panel`. This is useful for organizing related fields in a structured layout.
To enhance readability and maintain a well-organized UI, it is recommended to use the `stacked` option for fields inside clusters.
```ruby{4-18}
# app/avo/resources/person.rb
class Avo::Resources::Person < Avo::BaseResource
def fields
panel "Address" do
cluster do
field :street_address, stacked: true do
"1234 Elm Street"
end
field :city, stacked: true do
"Los Angeles"
end
field :zip_code, stacked: true do
"15234"
end
end
end
end
end
```
---
# Resource Sidebar
By default, all declared fields are going to be stacked vertically in the main area. But there are some fields with information that needs to be displayed in a smaller area, like boolean, date, and badge fields.
Those fields don't need all that horizontal space and can probably be displayed in a different space.
That's we created the **resource sidebar**.
## Adding fields to the sidebar
Using the `sidebar` block on a resource you may declare fields the same way you would do on the root level. Notice that the sidebar should be declared inside a panel. Each resource can have several panels or main panels and each panel can have it's own sidebars.
```ruby
class Avo::Resources::User < Avo::BaseResource
def fields
main_panel do
field :id, as: :id, link_to_record: true
field :first_name, as: :text, placeholder: "John"
field :last_name, as: :text, placeholder: "Doe"
# We can also add custom resource tools
tool UserTimeline
sidebar do
field :email, as: :gravatar, link_to_record: true, only_on: :show
field :active, as: :boolean, name: "Is active", only_on: :show
end
end
end
end
```
The fields will be stacked in a similar way in a narrower area on the side of the main panel. You may notice that inside each field, the tabel and value zones are also stacked one on top of the other to allow for a larger area to display the field value.
---
# Tabs
Once your Avo resources reach a certain level of complexity, you might feel the need to better organize the fields, associations, and resource tools into groups. You can already use the `heading` to separate the fields inside a panel, but maybe you'd like to do more.
Tabs are a new layer of abstraction over panels. They enable you to group panels and tools together under a single pavilion and toggle between them.
```ruby
class Avo::Resources::User < Avo::BaseResource
def fields
field :id, as: :id, link_to_record: true
field :email, as: :text, name: "User Email", required: true
tabs do
tab "User information", description: "Some information about this user" do
panel do
field :first_name, as: :text, required: true, placeholder: "John"
field :last_name, as: :text, required: true, placeholder: "Doe"
field :active, as: :boolean, name: "Is active", show_on: :show
end
end
field :teams, as: :has_and_belongs_to_many
field :people, as: :has_many
field :spouses, as: :has_many
field :projects, as: :has_and_belongs_to_many
end
end
end
```
To use tabs, you need to open a `tabs` group block. Next, you add your `tab` block where you add fields and panels like you're used to on resource root. Most fields like `text`, `number`, `gravatar`, `date`, etc. need to be placed in a `panel`. However, the `has_one`, `has_many`, and `has_and_belongs_to_many` have their own panels, and they don't require a `panel` or a `tab`.
The tab `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 page, if you have a lot of `has_many` type of fields or tools, they won't load right away, making it a bit more lightweight for your Rails app. Instead, they will lazy-load only when they are displayed.
## Tabs on Edit view
All visibility rules still apply on , meaning that `has_*` fields will be hidden by default. However, you can enable them by adding `show_on: :edit`. All other fields will be loaded and hidden on page load. This way, when you submit a form, if you have validation rules in place requiring a field that's in a hidden tab, it will be present on the page on submit-time.
## Durable and "Bookmarkable"
Tabs remain durable within views, meaning that when switch between views, each tab group retains the selected tab. This ensures a consistent UX, allowing for seamless navigation without losing context.
Moreover, you have the ability to bookmark a link with a personalized tab selection.
This functionalities relies on the unique tab group ID. To take full advantage of this feature, it's important to assign a unique ID to each tab group defined in your application.
```ruby {1}
tabs id: :some_random_uniq_id do
field :posts, as: :has_many, show_on: :edit
end
```
## Display counter indicator on tabs switcher
Check this recipe on how to enhance your tabs switcher with a counter for each association tab.
## Visibility control
Both `tabs` and individual `tab` components support a `visible` option that allows you to dynamically control their visibility based on certain conditions. For example, you might want to hide a tab if the user doesn't have the necessary permissions to view its content.
---
# Views
The Avo CRUD feature generates with four main views for each resource.
## Preview
The fields marked with `show_on :preview`, will be show in the preview field popup.
By default, all fields are hidden in `:preview`.
## Checking the current view
The `view` object, available in the code, is an instance of the `Avo::ViewInquirer` class.
This enables you to examine the existing `view` status through expressions such as `view.show?` and `view.index?`.
Essentially, these are equivalent to asserting whether view equals `show` or `index`.
## Multiple ways to check
```ruby
view == "edit" # Check against a string
view == :edit # Check against a symbol
view.edit? # Ask if it's a view
view.form? # Ask if it's a collection of views
view.in? [:edit, :new] # Check against an array of symbols
view.in? ["edit", "new"] # Check against an array of strings
```
::: code-group
```ruby [Ask]
if view.show?
# Code for the "show" view
elsif view.index?
# Code for the "index" view
elsif view.edit?
# Code for the "edit" view
elsif view.new?
# Code for the "new" view
elsif view.form?
# Code for the "new" or "edit" views
elsif view.display?
# Code for the "index or "show" views
end
```
```ruby [Symbol comparator]
if view == :show
# Code for the "show" view
elsif view == :index
# Code for the "index" view
elsif view == :edit
# Code for the "edit" view
elsif view == :new
# Code for the "new" view
end
```
```ruby [String comparator]
if view == "show"
# Code for the "show" view
elsif view == "index"
# Code for the "index" view
elsif view == "edit"
# Code for the "edit" view
elsif view == "new"
# Code for the "new" view
end
```
:::
It's also possible to check if the view is on a `form` (`new`, `edit`) or `display` (`index`, `show`).
::: code-group
```ruby [Ask]
if view.form?
# Code for the "new" and "edit" views
elsif view.display?
# Code for the "show" and "index" views
end
```
```ruby [Symbol comparator]
if view.in? [:new, :edit]
# Code for the "new" and "edit" views
elsif view.in? [:show, :index]
# Code for the "show" and "index" views
end
```
```ruby [String comparator]
if view.in? ["new", "edit"]
# Code for the "new" and "edit" views
elsif view.in? ["show", "index"]
# Code for the "show" and "index" views
end
```
:::
---
# Table View
The table view is the default way to display resources in Avo. It provides a powerful, tabular layout that supports searching, sorting, filtering, and pagination out of the box.
## Row controls configuration
:::info
The configuration options for row controls depend on the version of Avo you are using.
**If you are using a version earlier than **, refer to the following pages for guidance:
- How to adjust resource controls globally for all resources
- Customize the placement of controls for individual resources
:::
By default, resource controls are positioned on the right side of record rows. However, if the table contains many columns, these controls may become obscured. In such cases, you may prefer to move the controls to the left side for better visibility.
Avo provides configuration options that allow you to customize row controls placement, floating behavior, and visibility on hover either globally or individually for each resource.
## Global configuration
`resource_row_controls_config` defines the default settings for row controls across all resources. These global configurations will apply to each resource unless explicitly overridden.
This option can be configured on `config/initializers/avo.rb` and defaults to the following:
```ruby{3-7}
# config/initializers/avo.rb
Avo.configure do |config|
config.resource_row_controls_config = {
placement: :right,
float: false,
show_on_hover: false
}
end
```
## Resource configuration
`row_controls_config` allows you to customize the row controls for a specific resource, overriding the global configuration.
This option can be configured individually for each resource and defaults to the global configuration value defined in `resource_row_controls_config`.
```ruby{3-7}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
self.row_controls_config = {
placement: :right,
float: false,
show_on_hover: false,
}
end
```
---
# Grid view
Some resources are best displayed in a grid view. We can do that with Avo using a `cover_url`, a `title`, and a `body`.
## Enable grid view
To enable grid view for a resource, you need to configure the `grid_view` class attribute on the resource. That will add the grid view to the view switcher on the view.
```ruby{2-13}
class Avo::Resources::Post < Avo::BaseResource
self.grid_view = {
card: -> do
{
cover_url:
if record.cover_photo.attached?
main_app.url_for(record.cover_photo.url)
end,
title: record.name,
body: record.truncated_body
}
end
}
end
```
## 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{2}
class Avo::Resources::Post < Avo::BaseResource
self.default_view_type = :grid
end
```
## Custom style
You may want to customize the card a little bit. That's possible using the `html` option.
```ruby{13-37}
class Avo::Resources::Post < Avo::BaseResource
self.grid_view = {
card: -> do
{
cover_url:
if record.cover_photo.attached?
main_app.url_for(record.cover_photo.url)
end,
title: record.name,
body: record.truncated_body
}
end,
html: -> do
{
title: {
index: {
wrapper: {
classes: "bg-blue-50 rounded-md p-2"
}
}
},
body: {
index: {
wrapper: {
classes: "bg-gray-50 rounded-md p-1"
}
}
},
cover: {
index: {
wrapper: {
classes: "blur-sm"
}
}
}
}
end
}
end
```
## Grid Item Badge
One common scenario is to show a badge on top of your grid items. Avo enables you to do that pretty easy using these three options.
---
# Map view
Some resources that contain geospatial data can benefit from being displayed on a map. For
resources to be displayed to the map view they require a `coordinates` field, but that's customizable.
## Enable map view
To enable map view for a resource, you need to add the `map_view` class attribute to a resource. That will add the view switcher to the view.
```ruby
class Avo::Resources::City < Avo::BaseResource
# ...
self.map_view = {
mapkick_options: {
controls: true
},
record_marker: -> {
{
latitude: record.coordinates.first,
longitude: record.coordinates.last,
tooltip: record.name
}
},
table: {
visible: true,
layout: :right
}
}
end
```
:::warning
You need to add the `mapkick-rb` (not `mapkick`) gem to your `Gemfile` and have the `MAPBOX_ACCESS_TOKEN` environment variable with a valid [Mapbox](https://account.mapbox.com/auth/signup/) key.
:::
## Make it the default view
To make the map view the default way of viewing a resource on , we have to use the `default_view_type` class attribute.
```ruby{7}
class Avo::Resources::City < Avo::BaseResource
self.default_view_type = :map
end
```
---
# Customization options
## Change the app name
On the main navbar next to the logo, Avo generates a link to the homepage of your app. The label for the link is usually computed from your Rails app name. You can customize that however, you want using `config.app_name = 'Avocadelicious'`.
The `app_name` option is also callable using a block. This is useful if you want to reference a `I18n.t` method or something more dynamic.
```ruby
Avo.configure do |config|
config.app_name = -> { I18n.t "app_name" }
end
```
## Timezone and Currency
Your data-rich app might have a few fields where you reference `date`, `datetime`, and `currency` fields. You may customize the global timezone and currency with `config.timezone = 'UTC'` and `config.currency = 'USD'` config options.
## Resource Index view
There are a few customization options to change how resources are displayed in the **Index** view.
### Resources per page
You may customize how many resources you can view per page with `config.per_page = 24`.
### Per page steps
Similarly customize the per-page steps in the per-page picker with `config.per_page_steps = [12, 24, 48, 72]`.
### Resources via per page
For `has_many` associations you can control how many resources are visible in their `Index view` with `config.via_per_page = 8`.
### Default view type
The `ResourceIndex` component supports two view types `:table` and `:grid`. You can change that by `config.default_view_type = :table`. Read more on the grid view configuration page.
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 or both sides
:::warning
`resource_controls_placement` option is **obsolete**.
Check row controls configuration on table view instead
:::
By default, the resource controls are located on the right side of the record rows, which might be hidden if there are a lot of columns. You might want to move the controls to the left side in that situation using the `resource_controls_placement` option.
```ruby{3}
# config/initializers/avo.rb
Avo.configure do |config|
config.resource_controls_placement = :left
end
```
You're able to render the controls on both sides
```ruby{3}
# config/initializers/avo.rb
Avo.configure do |config|
config.resource_controls_placement = :both
end
```
## Container width
```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{3-6}
Avo.configure do |config|
config.set_context do
{
foo: 'bar',
params: request.params,
}
end
end
```
:::warning `_current_user`
It's recommended you don't store your current user here but using the `current_user_method` config.
:::
You can access the context data with `::Avo::Current.context` object.
## Eject
This section has moved.
## Breadcrumbs
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_dashboards.dashboard_path(:dashy) }
end
```
When you configure the `home_path` option, the `Get started` sidebar item will be hidden in the development environment.
Now, users will be redirected to `/avo/dashboard` whenever they click the logo. You can use this configuration option alongside the `set_initial_breadcrumbs` option to create a more cohesive experience.
```ruby{2-5}
Avo.configure do |config|
config.home_path = "/avo/dashboard"
config.set_initial_breadcrumbs do
add_breadcrumb "Dashboard", "/avo/dashboard"
end
end
```
## Mount Avo under a nested path
You may need to mount Avo under a nested path, something like `/uk/admin`. In order to do that, you need to consider a few things.
1. Move the engine mount point below any route for custom tools.
```ruby{7,10}
Rails.application.routes.draw do
# other routes
authenticate :user, ->(user) { user.is_admin? } do
scope :uk do
scope :admin do
get "dashboard", to: "avo/tools#dashboard" # custom tool added before engine
end
mount_avo # engine mounted last
end
end
end
```
2. The `root_path` configuration should only be the last path segment.
```ruby
# π« Don't add the scope to the root_path
Avo.configure do |config|
config.root_path = "/uk/admin"
end
# β Do this instead
Avo.configure do |config|
config.root_path = "/admin"
end
```
3. Use full paths for other configurations.
```ruby
Avo.configure do |config|
config.home_path = "/uk/admin/dashboard"
config.set_initial_breadcrumbs do
add_breadcrumb "Dashboard", "/uk/admin/dashboard"
end
end
```
## Custom `view_component` path
You may not keep your view components under `app/components` and want the generated field `view_component`s to be generated in your custom directory. You can change that using the `view_component_path` configuration key.
```ruby
Avo.configure do |config|
config.view_component_path = "app/frontend/components"
end
```
## Custom query scopes
You may want to change Avo's queries to add sorting or use gems like [friendly](https://github.com/norman/friendly_id).
You can do that using `index_query` for multiple records and `find_record_method` when fetching one record.
### Custom scope for `Index` page
Using `index_query` you tell Avo how to fetch the records for the `Index` view.
```ruby
class Avo::Resources::User < Avo::BaseResource
self.index_query = -> {
query.order(last_name: :asc)
}
end
```
### Custom find method for `Show` and `Edit` pages
Using `find_record_method` you tell Avo how to fetch one record for `Show` and `Edit` views and other contexts where a record needs to be fetched from the database.
This is very useful when you use something like `friendly` gem, custom `to_param` methods on your model, and even the wonderful `prefix_id` gem.
#### Custom `to_param` method
The following example shows how you can update the `to_param` (to use the post name) method on the `User` model to use a custom attribute and then update the `Avo::Resources::User` so it knows how to search for that model.
::: code-group
```ruby [app/avo/resources/post.rb]
class Avo::Resource::Post < Avo::BaseResource
self.find_record_method = -> {
# When using friendly_id, we need to check if the id is a slug or an id.
# If it's a slug, we need to use the find_by_slug method.
# If it's an id, we need to use the find method.
# If the id is an array, we need to use the where method in order to return a collection.
if id.is_a?(Array)
id.first.to_i == 0 ? query.where(slug: id) : query.where(id: id)
else
id.to_i == 0 ? query.find_by_slug(id) : query.find(id)
end
}
end
```
```ruby [app/models/post.rb]
class Post < ApplicationRecord
before_save :update_slug
def to_param
slug || id
end
def update_slug
self.slug = name.parameterize
end
end
```
:::
#### Using the `friendly` gem
::: code-group
```ruby [app/avo/resources/user.rb]
class Avo::Resources::User < Avo::BaseResource
self.find_record_method = -> {
if id.is_a?(Array)
query.where(slug: id)
else
# We have to add .friendly to the query
query.friendly.find id
end
}
end
```
```ruby [app/models/user.rb]
class User < ApplicationRecord
extend FriendlyId
friendly_id :name, use: :slugged
end
```
:::
#### Using `prefixed_ids` gem
You really don't have to do anything on Avo's side for this to work. You only need to add the `has_prefix_id` the model as per the documentation. Avo will know how to search for the record.
```ruby
class Course < ApplicationRecord
has_prefix_id :course
end
```
## 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
```
`disabled_features` become callable. Within this block, you gain access to all attributes of `Avo::ExecutionContext`
```ruby{3}
# config/initializers/avo.rb
Avo.configure do |config|
config.disabled_features = -> { current_user.is_admin? ? [] : [: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.skip_show_view = true` to your `avo.rb` configuration file will tell Avo to skip it and use the view as the default resource view.
```ruby{3}
# config/initializers/avo.rb
Avo.configure do |config|
config.skip_show_view = true
end
```
## Logger
You may want to set a different output stream for avo logs, you can do that by returning it on a `config.logger` Proc
```ruby
## == Logger ==
config.logger = -> {
file_logger = ActiveSupport::Logger.new(Rails.root.join("log", "avo.log"))
file_logger.datetime_format = "%Y-%m-%d %H:%M:%S"
file_logger.formatter = proc do |severity, time, progname, msg|
"[Avo] #{time}: #{msg}\n".tap do |i|
puts i
end
end
file_logger
}
```
## Associations lookup list limit
---
# Eject
If you want to change one of Avo's built-in views, you can eject it, update it and use it in your admin panel.
:::warning
Once ejected, the views will not receive updates on new Avo releases. You must maintain them yourself.
:::
---
# Custom view types
By default, Avo displays all the configured view types on the view switcher. For example, if you have `map_view` and `grid_view` configured, both of them, along with the `table_view`, will be available on the view switcher.
However, there might be cases where you only want to make a specific view type available without removing the configurations for other view types. This can be achieved using the `view_types` class attribute on the resource. Note that when only one view type is available, the view switcher will not be displayed.
```ruby{3}
class Avo::Resources::City < Avo::BaseResource
# ...
self.view_types = :table
#...
end
```
If you want to make multiple view types available, you can use an array. The icons on the view switcher will follow the order in which they are declared in the configuration.
```ruby{3}
class Avo::Resources::City < Avo::BaseResource
# ...
self.view_types = [:table, :grid]
#...
end
```
You can also dynamically restrict the view types based on user roles, params, or other business logic. To do this, assign a block to the `view_types` attribute. Within the block, you'll have access to `resource`, `record`, `params`, `current_user`, and other default accessors provided by `ExecutionContext`.
```ruby{3-9}
class Avo::Resources::City < Avo::BaseResource
# ...
self.view_types = -> do
if current_user.is_admin?
[:table, :grid]
else
:table
end
end
#...
end
```
---
# 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
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, want to add a very custom form or more items to the profile menu. For that we prepared the `_profile_menu_extra.html.erb` partial for you.
```bash
bin/rails generate avo:eject --partial :profile_menu_extra
```
This will eject the partial and you can add whatever custom content you might need.
```erb
<%# Example link below %>
<%#= render Avo::ProfileItemComponent.new label: 'Profile', path: '/profile', icon: 'user-circle' %>
```
---
# Search
Finding what you're looking for fast is essential. That's why Avo 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.
:::
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 configure the `search` class attribute to the resource file.
```ruby{2-4}
class Avo::Resources::User < Avo::BaseResource
self.search = {
query: -> { query.ransack(name_eq: params[:q]).result(distinct: false) }
}
end
```
The `query` block passes over the `params` object that holds the `q` param, the actual query string. It also provides the `query` 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).
:::
### Custom search provider
You can use custom search providers like Elasticsearch.
In such cases, or when you want to have full control over the search results, the `query` block should return an array of hashes. Each hash should follow the structure below:
```ruby
{
_id: 1,
_label: "The label",
_url: "The URL",
_description: "Some description about the record", # only with Avo Pro and above
_avatar: "URL to an image that represents the record", # only with Avo Pro and above
_avatar_type: :rounded # or :circle or :square; only with Avo Pro and above
}
```
Example:
```ruby{2-10}
class Avo::Resources::Project < Avo::BaseResource
self.search = {
query: -> do
[
{ _id: 1, _label: "Record One", _url: "https://example.com/1" },
{ _id: 2, _label: "Record Two", _url: "https://example.com/2" },
{ _id: 3, _label: "Record Three", _url: "https://example.com/3" }
]
end
}
end
```
:::warning
Results count will not be available with custom search providers.
:::
## Authorize search
Search is authorized in policy files using the `search?` method.
```ruby
class UserPolicy < ApplicationPolicy
def search?
true
end
end
```
If the `search?` method returns false, the search operation for that resource is not going to show up in the global search and the search box on index is not going to be displayed.
If you're using `search?` already in your policy file, you can alias it to some other method in you initializer using the `config.authorization_methods` config. More about that on the authorization page.
```ruby
Avo.configure do |config|
config.authorization_methods = {
search: 'avo_search?',
}
end
```
## Configure the search result
## Resource search
When a resource has the `search` attribute with a valid configuration, 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` attribute with a valid configuration.
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
```
Since version `disabled_features` become callable. Within this block, you gain access to all attributes of `Avo::ExecutionContext`
```ruby{3}
# config/initializers/avo.rb
Avo.configure do |config|
config.disabled_features = -> { current_user.is_admin? ? [] : [:global_search] }
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
class Avo::Resources::Order < Avo::BaseResource
self.search = {
query: -> {
if params[:global]
# Perform global search
query.ransack(id_eq: params[:q], m: "or").result(distinct: false)
else
# Perform resource search
query.ransack(id_eq: params[:q], details_cont: params[:q], m: "or").result(distinct: false)
end
}
}
end
```
## Searching within associations
In some cases, you might need to search for records based on attributes of associated models. This can be achieved by adding a few things to the search query. Here's an example of how to do that:
Assuming you have two models, `Application` and `Client`, with the following associations:
```ruby{3,8}
# app/models/application.rb
class Application < ApplicationRecord
belongs_to :client
end
# app/models/client.rb
class Client < ApplicationRecord
has_many :applications
end
```
You can perform a search on `Application` records based on attributes of the associated `Client`. For example, searching by the client's email, name, or phone number:
```ruby{6,11-15}
# app/avo/resources/application.rb
class Avo::Resources::Application < Avo::BaseResource
self.search = {
query: -> {
query
.joins(:client)
.ransack(
id_eq: params[:q],
name_cont: params[:q],
workflow_name_cont: params[:q],
client_id_eq: params[:q],
client_first_name_cont: params[:q],
client_last_name_cont: params[:q],
client_email_cont: params[:q],
client_phone_number_cont: params[:q],
m: 'or'
).result(distinct: false)
}
}
end
```
In the above example, ransack is used to search for `Application` records based on various attributes of the associated `Client`, such as `client_email_cont` and `client_phone_number_cont`. The joins method is used to join the applications table with the clients table to perform the search efficiently.
This approach allows for flexible searching within associations, enabling you to find records based on related model attributes.
## Results count
By default, Avo displays 8 search results whenever you search. You can change the number of results displayed by configuring the `search_results_count` option:
```ruby
Avo.configure do |config|
config.search_results_count = 16
end
```
You can also change the number of results displayed on individual resources:
```ruby{3}
class Avo::Resources::User < Avo::BaseResource
self.search = {
results_count: 5
query: -> {},
}
end
```
You can also assign a lambda to dynamically set the value.
```ruby{3}
class Avo::Resources::User < Avo::BaseResource
self.search = {
results_count: -> { user.admin? ? 30 : 10 }
}
end
```
If you configure `results_count` by specifying it in the resource file then that number takes precedence over the global [`search_results_count`](#results-count) for that resource.
---
# Localization (i18n)
Avo leverages Rails' powerful `I18n` translations module.
:::warning Multi-language URL Support
If you're serving Avo using multiple languages and you're using the locale in your routes (`/en/resources/users`, `/de/resources/users`), check out this guide.
:::
When you run `bin/rails avo:install`, Rails will not generate for you the `avo.en.yml` translation file. This file is already loaded will automatically be injected into the I18n translations module.
## Localizing resources
Let's say you want to localize a resource. All you need to do is add a `self.translation_key` class attribute in the `Resource` file. That will tell Avo to use that translation key to localize this resource. That will change the labels of that resource everywhere in Avo.
```ruby{4}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
self.title = :name
self.translation_key = 'avo.resource_translations.user'
end
```
```yaml{6-10}
# avo.es.yml
es:
avo:
dashboard: 'Dashboard'
# ... other translation keys
resource_translations:
user:
zero: 'usuarios'
one: 'usuario'
other: 'usuarios'
```
## Localizing fields
Similarly, you can even localize fields. All you need to do is add a `translation_key:` option on the field declaration.
```ruby{8}
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id
# ... other fields
field :files, as: :files, translation_key: 'avo.field_translations.file'
end
end
```
```yaml{6-10}
# avo.es.yml
es:
avo:
dashboard: 'Dashboard'
# ... other translation keys
field_translations:
file:
zero: 'archivos'
one: 'archivo'
other: 'archivos'
```
## Localizing buttons label
The `avo.save` configuration applies to all save buttons. If you wish to customize the localization for a specific resource, such as `Avo::Resources::Product`, you can achieve this by:
```yml
---
en:
avo:
resource_translations:
product:
save: "Save the product!"
```
## Setting the locale
Setting the locale for Avo is pretty simple. Just use the `config.locale = :en` config attribute. Default is `nil` and will fall back to whatever you have configured in as `config.i18n.default_locale` in `application.rb`.
```ruby{2}
Avo.configure do |config|
config.locale = :en # default is nil
end
```
That will change the locale only for Avo requests. The rest of your app will still use your locale set in `application.rb`. If you wish to change the locale for Avo, you can use the `set_locale=pt-BR` param. That will set the default locale for Avo until you restart your server.
Suppose you wish to change the locale only for one request using the `force_locale=pt-BR` param. That will set the locale for that request and keep the `force_locale` param in all links while you navigate Avo. Remove that param when you want to go back to your configured `default_locale`.
Related:
- Check out our guide for multilingual records.
## Customize the locale
If there's anything in the locale files that you would like to change, run `bin/rails generate avo:locales` to generate the locale files.
These provide a guide for you for when you want to add more languages.
If you do translate Avo in a new language please consider contributing it to the [main repo](https://github.com/avo-hq/avo). Thank you
## FAQ
If you try to localize your resources and fields and it doesn't seem to work, please be aware of the following.
### The I18n.t method defaults to the name of that field/resource
Internally the localization works like so `I18n.t(translation_key, count: 1, default: default)` where the `default` is the computed field/resource name. So check the structure of your translation keys.
```yaml
# config/locales/avo.pt-BR.yml
pt-BR:
avo:
field_translations:
file:
zero: 'arquivos'
one: 'arquivo'
other: 'arquivos'
resource_translations:
user:
zero: 'usuΓ‘rios'
one: 'usuΓ‘rio'
other: 'usuΓ‘rios'
```
### Using a Route Scope for Localization
To implement a route scope for localization within Avo, refer to this guide. It provides step-by-step instructions on configuring your routes to include a locale scope, enabling seamless localization handling across your application.
---
# Branding
```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.
---
# Routing
We stick to Rails defaults in terms of routing just to make working with Avo as straightforward as possible.
Avo's functionality is distributed across multiple gems, each encapsulating its own engine. By default, these engines are mounted under Avo's scope within your Rails application.
Each engine registers itself with Avo.
### Default Mounting Behavior
When the `mount_avo` method is invoked, Avo and all the associated engines are mounted at a common entry point. By default, this mounting point corresponds to `Avo.configuration.root_path`, but you can customize it using the `at` argument:
```ruby{4,7}
# config/routes.rb
Rails.application.routes.draw do
# Mounts Avo at Avo.configuration.root_path
mount_avo
# Mounts Avo at `/custom_path` instead of the default
mount_avo at: "custom_path"
end
```
If no custom path is specified, Avo is mounted at the default configuration root path.
## Mount Avo under a scope
In this example, we'll demonstrate how to add a `:locale` scope to your routes.
The `:locale` scope is just an example. If your objective is to implement a route scope for localization within Avo, there's a detailed recipe available. Check out this guide for comprehensive instructions.
```ruby{4-6}
# config/routes.rb
Rails.application.routes.draw do
scope ":locale" do
mount_avo
end
end
```
:::info
To guarantee that the `locale` scope is included in the `default_url_options`, you must explicitly add it to the Avo configuration.
Check this documentation section for details on how to configure `default_url_options` setting.
:::
## Add your own routes
You may want to add your own routes inside Avo so you can access different custom actions that you might have set in the Avo resource controllers.
You can do that in your app's `routes.rb` file by opening up the Avo routes block and append your own.
```ruby
# routes.rb
Rails.application.routes.draw do
mount_avo
# your other app routes
end
if defined? ::Avo
Avo::Engine.routes.draw do
# new route in new controller
put "switch_accounts/:id", to: "switch_accounts#update", as: :switch_account
scope :resources do
# append a route to a resource controller
get "courses/cities", to: "courses#cities"
end
end
end
# app/controllers/avo/switch_accounts_controller.rb
class Avo::SwitchAccountsController < Avo::ApplicationController
def update
session[:tenant_id] = params[:id]
redirect_back fallback_location: root_path
end
end
```
---
# Multitenancy
Multitenancy is a very talked-about subject. We're not going to go very deep into how to achieve it on the database level, but will talk a little bit about how it's supported in Avo.
## Breakdown
Usually, with multitenancy you add a new layer just one level below authentication. You don't have just a user to think about, but now that user might act on the behalf of a tenant. That tenant can be an `Account` or a `Team`, or any other model you design in your database.
So now, the mission is to pinpoint which tenant is the user acting for. Because Avo has such an integrated experience and we use our own `ApplicationController`, you might think it's difficult to add that layer, when in fact it's really not. There are a couple of steps to do.
:::info
We'll use the `foo` tenant id from now on.
:::
## Route-based tenancy
There are a couple of strategies here, but the a common one is to use route-based tenancy. That means that your user uses a URL like `https://example.com/foo/` and the app should know to scope everything to that `foo` tenant.
We need to do a few things:
#### 1. Set the proper routing pattern
Mount Avo under the `tenant_id` scope
```ruby
# config/routes.rb
Rails.application.routes.draw do
scope "/:tenant_id" do
mount_avo
end
end
```
#### 2. Set the tenant for each request
:::code-group
```ruby [config/initializers/avo.rb]{6}
Avo.configure do |config|
# configuration values
end
Rails.configuration.to_prepare do
Avo::ApplicationController.include Multitenancy
end
```
```ruby [app/controllers/concerns/multitenancy.rb]
module Multitenancy
extend ActiveSupport::Concern
included do
prepend_before_action :set_tenant
end
def set_tenant
Avo::Current.tenant_id = params[:tenant_id]
Avo::Current.tenant = Account.find params[:tenant_id]
end
end
```
:::
Now, whenever you navigate to `https://example.com/lol` the tenant the `tenant_id` will be set to `lol`.
## Session-based tenancy
Using a session-based tenancy strategy is a bit simpler as we don't meddle with the routing.
:::warning
The code below shows how it's possible to do session-based multitenancy but your use-case or model names may vary a bit.
:::
We need to do a few things:
#### 1. Set the tenant for each request
:::code-group
```ruby [config/initializers/avo.rb]{6}
Avo.configure do |config|
# configuration values
end
Rails.configuration.to_prepare do
Avo::ApplicationController.include Multitenancy
end
```
```ruby [app/controllers/concerns/multitenancy.rb]
module Multitenancy
extend ActiveSupport::Concern
included do
prepend_before_action :set_tenant
end
def set_tenant
Avo::Current.tenant = Account.find session[:tenant_id] || current_user.accounts.first
end
end
```
:::
#### 2. Add an account switcher
Somewhere in a view on a navbar or sidebar add an account switcher.
:::code-group
```erb [app/views/avo/session_switcher.html.erb]
<% current_user.accounts.each do |account| %>
<%= link_to account.name, switch_account_path(account.id), class: class_names({"underline": session[:tenant_id].to_s == account.id.to_s}), data: {turbo_method: :put} %>
<% end %>
```
```ruby [app/controllers/avo/switch_accounts_controller.rb]
class Avo::SwitchAccountsController < Avo::ApplicationController
def update
# set the new tenant in session
session[:tenant_id] = params[:id]
redirect_back fallback_location: root_path
end
end
```
:::
---
# Actions Overview
Actions in Avo are powerful tools that transform the way you interact with your data. They enable you to perform operations on one or multiple records simultaneously, extending your interface with custom functionality that goes beyond basic CRUD operations.
## What Are Actions?
Think of Actions as custom operations you can trigger from your admin interface. They're like specialized commands that can:
- Process single records or work in batch mode
- Collect additional information through customizable forms
- Trigger background jobs
- Generate reports or export data
- Modify record states
- Send notifications
- And much more...
## Key Benefits
### 1. Streamlined Workflows
Instead of building custom interfaces for common operations, Actions provide a standardized way to perform complex tasks right from your admin panel.
### 2. Flexibility
Actions can be as simple or as complex as you need:
- Simple toggles for changing record states
- Multi-step processes with user input on each step
- Background job triggers for heavy operations
- API integrations with external services
### 3. Batch Operations
Save time by performing operations on multiple records at once. Whether you're updating statuses, sending notifications, or processing data, batch actions have you covered.
### 4. User Input Forms
When additional information is needed, Actions can present custom forms to collect data before execution. These forms are fully customizable and support various field types.
## Common Use Cases
- **User Management**: Activate/deactivate accounts, reset passwords, or send welcome emails
- **Content Moderation**: Approve/reject content, flag items for review
- **Data Processing**: Generate reports, export data, or trigger data transformations
- **Communication**: Send notifications, emails, or SMS messages
- **State Management**: Change status, toggle features, or update permissions
- **Batch Updates**: Modify multiple records with consistent changes
- **Integration Triggers**: Connect with external APIs or services
Common use cases include managing user states, sending notifications, and automating data processing. Their flexibility makes them essential for building robust interfaces, streamlining workflows, and managing data efficiently.
---
# Action Generator
Avo provides a powerful Rails generator to create action files quickly and efficiently.
## Basic Generator Usage
Generate a new action file using the Rails generator:
```bash
bin/rails generate avo:action toggle_inactive
```
This command creates a new action file at `app/avo/actions/toggle_inactive.rb` with the following structure:
```ruby
# app/avo/actions/toggle_inactive.rb
class Avo::Actions::ToggleInactive < Avo::BaseAction
self.name = "Toggle Inactive"
# self.visible = -> do
# true
# end
# def fields
# # Add Action fields here
# end
def handle(query:, fields:, current_user:, resource:, **args)
query.each do |record|
# Do something with your records.
end
end
end
```
## Generator Options
### `--standalone`
By default, actions require at least one record to be selected before they can be triggered, unless specifically configured as standalone actions.
The `--standalone` option creates an action that doesn't require record selection. This is particularly useful for:
- Generating reports
- Exporting all records
- Running global operations
```bash
bin/rails generate avo:action export_users --standalone
```
You can also make an existing action standalone by manually setting `self.standalone = true` in the action class:
```ruby{5}
# app/avo/actions/export_users.rb
class Avo::Actions::ExportUsers < Avo::BaseAction
self.name = "Export Users"
self.standalone = true
# ... rest of the action code
end
```
## Best Practices
When generating actions, consider the following:
1. Use descriptive names that reflect the action's purpose (e.g., `toggle_published`, `send_newsletter`, `archive_records`)
2. Follow Ruby naming conventions (snake_case for file names)
3. Group related actions in namespaces using subdirectories
4. Use the `--standalone` flag when the action doesn't operate on specific records
## Examples
```bash
# Generate a regular action
bin/rails generate avo:action mark_as_featured
# Generate a standalone action
bin/rails generate avo:action generate_monthly_report --standalone
# Generate an action in a namespace
bin/rails generate avo:action admin/approve_user
```
---
# Registration
Actions are registered within a resource by using the resource's `actions` method. This method defines which actions are available for that specific resource.
## `action`
The `action` method is used to register an action within the `actions` block. It accepts the action class as its first argument and optional configuration parameters like `arguments` and `icon`
```ruby{5}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def actions
# Basic registration
action Avo::Actions::ToggleInactive
end
end
```
:::warning
Using the Pundit policies, you can restrict access to actions using the `act_on?` method. If you think you should see an action on a resource and you don't, please check the policy method.
More info here
:::
Once attached, the action will appear in the **Actions** dropdown menu. By default, actions are available on all views.
:::info
You may use the customizable controls feature to show the actions outside the dropdown.
:::
---
## `divider`
Action dividers allow you to organize and separate actions into logical groups, improving the overall layout and usability.
This will create a visual separator in the actions dropdown menu, helping you group related actions together.
```ruby{8}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def actions
# User status actions
action Avo::Actions::ActivateUser
action Avo::Actions::DeactivateUser
divider
# Communication actions
action Avo::Actions::SendWelcomeEmail
action Avo::Actions::SendPasswordReset
end
end
```
---
# Execution flow
When a user triggers an action in Avo, the following flow occurs:
1. Record selection phase:
- This phase can be bypassed by setting `self.standalone = true`
- For bulk actions on the index page, Avo collects all the records selected by the user
- For actions on the show page or row controls, Avo uses that record as the target of the action
2. The action is initiated by the user through the index page (bulk actions), show page (single record actions), or resource controls (custom action buttons)
3. Form display phase (optional):
- This phase can be bypassed by setting `self.no_confirmation = true`
- By default, a modal is displayed where the user can confirm or cancel the action
- If the action has defined fields, they will be shown in the modal for the user to fill out
- The user can then choose to run the action or cancel it
- If the user cancels, the execution stops here
4. Action execution:
- The `handle` method processes selected records, form values, current user, and resource details
- Your custom business logic is executed within the `handle` method
- User feedback is configured ([`succeed`](#succeed), [`warn`](#warn), [`inform`](#inform), [`error`](#error), or [`silent`](#silent))
- Response type is configured ([`redirect_to`](#redirect_to), [`reload`](#reload), [`keep_modal_open`](#keep_modal_open), and [more](#response-types))
## The `handle` method
The `handle` method is where you define what happens when your action is executed. This is the core of your action's business logic and receives the following arguments:
- `query` Contains the selected record(s). Single records are automatically wrapped in an array for consistency
- `fields` Contains the values submitted through the action's form fields
- `current_user` The currently authenticated user
- `resource` The Avo resource instance that triggered the action
```ruby{10-23}
# app/avo/actions/toggle_inactive.rb
class Avo::Actions::ToggleInactive < Avo::BaseAction
self.name = "Toggle Inactive"
def fields
field :notify_user, as: :boolean
field :message, as: :textarea
end
def handle(query:, fields:, current_user:, resource:, **args)
query.each do |record|
# Toggle the inactive status
record.update!(inactive: !record.inactive)
# Send notification if requested
if fields[:notify_user]
# Assuming there's a notify method
record.notify(fields[:message])
end
end
succeed "Successfully toggled status for #{query.count}"
end
end
```
## Feedback notifications
After an action runs, you can respond to the user with different types of notifications or no feedback at all. The default feedback is an `Action ran successfully` message of type `inform`.
All feedback notification methods (`succeed`, `warn`, `inform`, `error`) support an optional `timeout` parameter to control how long the notification remains visible:
```ruby
# Display notification for 5 seconds
succeed 'Task completed successfully', timeout: 5000
# Keep notification open indefinitely, until the user dismisses it
warn 'Important warning - requires attention', timeout: :forever
# Use default timeout (falls back to global configuration)
inform 'Action completed'
```
:::info
Set the `timeout` to `:forever` to keep the notification open indefinitely until the user dismisses it.
The default timeout is set to `config.alert_dismiss_time` in the Avo configuration.
:::
:::info
You can show multiple notifications at once by calling multiple feedback methods (`succeed`, `warn`, `inform`, `error`) in your action's `handle` method. Each notification will be displayed in sequence.
:::
```ruby{4-7}
# app/avo/actions/toggle_inactive.rb
class Avo::Actions::ToggleInactive < Avo::BaseAction
def handle(**args)
succeed "Success response βοΈ"
warn "Warning response βοΈ"
inform "Info response βοΈ"
error "Error response βοΈ"
end
end
```
## Response types
After an action completes, you can control how the UI responds through various response types. These powerful responses give you fine-grained control over the user experience by allowing you to:
- **Navigate**: Reload pages or redirect users to different parts of your application
- **Manipulate UI**: Control modals, update specific page elements, or refresh table rows
- **Handle Files**: Trigger file downloads and handle data exports
- **Show Feedback**: Combine with notification messages for clear user communication
You can use these responses individually or combine them to create sophisticated interaction flows. Here are all the available action responses:
---
# Customization
Actions can be customized in several ways to enhance the user experience. You can modify the action's display name, confirmation message, button labels, and confirmation behavior between other things.
There are 2 types of customization, visual and behavioral.
## Visual customization
Visual customization is the process of modifying the action's appearance. This includes changing the action's name, message and button labels.
All visual customization options can be set as a string or a block.
The blocks are executed using `Avo::ExecutionContext`. Within these blocks, you gain access to:
- All attributes of `Avo::ExecutionContext`
- `resource` - The current resource instance
- `record` - The current record
- `view` - The current view
- `arguments` - Any passed arguments
- `query` - The current query parameters
## Behavioral customization
Behavioral customization is the process of modifying the action's behavior. This includes changing the action's confirmation behavior and authorization.
---
# WIP
this section is under construction
## Helpers
### `link_arguments`
The `link_arguments` method is used to generate the arguments for an action link.
You may want to dynamically generate an action link. For that you need the action class and a resource instance (with or without record hydrated). Call the action's class method `link_arguments` with the resource instance as argument and it will return the `[path, data]` that are necessary to create a proper link to a resource.
Let's see an example use case:
```ruby{4-,16} [Current Version]
# app/avo/resources/city.rb
class Avo::Resources::City < Avo::BaseResource
field :name, as: :text, name: "Name (click to edit)", only_on: :index do
path, data = Avo::Actions::City::Update.link_arguments(
resource: resource,
arguments: {
cities: Array[resource.record.id],
render_name: true
}
)
link_to resource.record.name, path, data: data
end
end
```
:::tip
#### Generate an Action Link Without a Resource Instance
Sometimes, you may need to generate an action link without having access to an instantiated resource.
#### Scenario
Imagine you want to trigger an action from a custom partial card on a dashboard, but there is no resource instance available.
#### Solution
In this case, you can create a new resource instance (with or without record) and use it as follows:
```ruby
path, data = Avo::Actions::City::Update.link_arguments(
resource: Avo::Resources::City.new(record: city)
)
link_to "Update city", path, data: data
```
:::
## Guides
### StimulusJS
Please follow our extended StimulusJS guides for more information.
### Passing Params to the Action Show Page
When navigation to an action from a resource or views, it's sometimes useful to pass parameters to an action.
One particular example is when you'd like to populate a field in that action with some particular value based on that param.
```ruby
class Action
def fields
field :some_field, as: :hidden, default: -> { if previous_param == yes ? :yes : :no}
end
end
```
Consider the following scenario:
1. Navigate to `https://main.avodemo.com/avo/resources/users`.
2. Add the parameter `hey=ya` to the URL: `https://main.avodemo.com/avo/resources/users?hey=ya`
3. Attempt to run the dummy action.
4. After triggering the action, verify that you can access the `hey` parameter.
5. Ensure that the retrieved value of the `hey` parameter is `ya`.
**Implementation**
To achieve this, we'll reference the `request.referer` object and extract parameters from the URL. Here is how to do it:
```ruby
class Action
def fields
# Accessing the parameters passed from the parent view
field :some_field, as: :hidden, default: -> {
# Parsing the request referer to extract parameters
parent_params = URI.parse(request.referer).query.split("&").map { |param| param.split("=")}.to_h.with_indifferent_access
# Checking if the `hei` parameter equals `ya`
if parent_params[:hey] == 'ya'
:yes
else
:no
end
}
end
end
```
Parse the `request.referer` to extract parameters using `URI.parse`.
Split the query string into key-value pairs and convert it into a hash.
Check if the `hey` parameter equals `ya`, and set the default value of `some_field` accordingly.
---
# Dashboards
:::warning
You must manually require the `chartkick` gem in your `Gemfile`.
```ruby
# Create beautiful JavaScript charts with one line of Ruby
gem "chartkick"
```
:::
There comes the point in your app's life when you need to display the data in an aggregated form like a metric or chart. That's what Avo's Dashboards are all about.
## Generate a dashboard
Run `bin/rails g avo:dashboard my_dashboard` to get a shiny new dashboard.
```ruby
class Avo::Dashboards::MyDashboard < Avo::Dashboards::BaseDashboard
self.id = 'my_dashboard'
self.name = 'Dashy'
self.description = 'The first dashbaord'
self.grid_cols = 3
def cards
card Avo::Cards::ExampleMetric
card Avo::Cards::ExampleAreaChart
card Avo::Cards::ExampleScatterChart
card Avo::Cards::PercentDone
card Avo::Cards::AmountRaised
card Avo::Cards::ExampleLineChart
card Avo::Cards::ExampleColumnChart
card Avo::Cards::ExamplePieChart
card Avo::Cards::ExampleBarChart
divider label: "Custom partials"
card Avo::Cards::ExampleCustomPartial
card Avo::Cards::MapCard
end
end
```
## Settings
Each dashboard is a file. It holds information about itself like the `id`, `name`, `description`, and how many columns its grid has.
The `id` field has to be unique. The `name` is what the user sees in big letters on top of the page, and the `description` is some text you pass to give the user more details regarding the dashboard.
Using the ' grid_cols ' parameter, you may organize the cards in a grid with `3`, `4`, `5`, or `6` columns using the `grid_cols` parameter. The default is `3`.
## Cards
This section has moved.
### Override card arguments from the dashboard
We found ourselves in the position to add a few cards that were the same card but with a slight difference. Ex: Have one `Users count` card and another `Active users count` card. They both count users, but the latter has an `active: true` condition applied.
Before, we'd have to duplicate that card and modify the `query` method slightly but end up with duplicated boilerplate code.
For those scenarios, we created the `arguments` attribute. It allows you to send arbitrary arguments to the card from the parent.
```ruby{7-9}
class Avo::Dashboards::Dashy < Avo::Dashboards::BaseDashboard
self.id = "dashy"
self.name = "Dashy"
def cards
card Avo::Cards::UsersCount
card Avo::Cards::UsersCount, arguments: {
active_users: true
}
end
end
```
Now we can pick up that option in the card and update the query accordingly.
```ruby{9-11}
class Avo::Cards::UsersCount < Avo::Cards::MetricCard
self.id = "users_metric"
self.label = "Users count"
# You have access to context, params, range, current parent, and current card
def query
scope = User
if arguments[:active_users].present?
scope = scope.active
end
result scope.count
end
end
```
That gives you an extra layer of control without code duplication and the best developer experience.
#### Control the base settings from the parent
Evidently, you don't want to show the same `label`, `description`, and other details for that second card from the first card.
Therefore, you can control the `label`, `description`, `cols`, `rows`, `visible`, and `refresh_every` arguments from the parent declaration.
```ruby{8-16}
class Avo::Dashboards::Dashy < Avo::Dashboards::BaseDashboard
self.id = "dashy"
self.name = "Dashy"
def cards
card Avo::Cards::UsersCount
card Avo::Cards::UsersCount,
label: "Active users",
description: "Active users count",
cols: 2,
rows: 2,
visible: -> { true }
refresh_every: 2.minutes,
arguments: {
active_users: true
}
end
end
```
## Dashboards visibility
You might want to hide specific dashboards from certain users. You can do that using the `visible` option. The option can be a boolean `true`/`false` or a block where you have access to the `params`, `current_user`, `context`, and `dashboard`.
If you don't pass anything to `visible`, the dashboard will be available for anyone.
```ruby{5-11}
class Avo::Dashboards::ComplexDash < Avo::Dashboards::BaseDashboard
self.id = "complex_dash"
self.name = "Complex dash"
self.description = "Complex dash description"
self.visible = -> do
current_user.is_admin?
# or
params[:something] == 'something else'
# or
context[:your_param] == params[:something_else]
end
def cards
card Avo::Cards::UsersCount
end
end
```
## Dashboards authorization
You can set authorization rules for dashboards using the `authorize` block.
```ruby{3-6}
class Avo::Dashboards::Dashy < Avo::Dashboards::BaseDashboard
self.id = 'dashy'
self.authorize = -> do
# You have access to current_user, params, request, context, adn view_context.
current_user.is_admin?
end
end
```
---
# Cards
Cards are one way of quickly adding custom content for your users.
Cards can be used on dashboards or resources, we'll refer to both of them as "parent" since they're hosting the cards.
You can add three types of cards to your parent: `partial`, `metric`, and `chartkick`.
## Base settings
All cards have some standard settings like `id`, which must be unique, `label` and `description`. The `label` will be the title of your card, and `description` will show a tiny question mark icon on the bottom right with a tooltip with that description.
Each card has its own `cols` and `rows` settings to control the width and height of the card inside the parent's grid. They can have values from `1` to `6`.
All this settings can be called as an lambda.
The lambda will be executed using `Avo::ExecutionContext`. Within this blocks, you gain access to all attributes of `Avo::ExecutionContext` along with the `parent`, `resource`, `dashboard` and `card`.
```ruby{2-7}
class Avo::Cards::UsersMetric < Avo::Cards::MetricCard
self.id = "users_metric"
self.label = -> { "Users count" }
self.description = -> { "Users description" }
self.cols = 1
self.rows = 1
self.display_header = true
end
```
## Ranges
#### Control the aggregation using ranges
You may also want to give the user the ability to query data in different ranges. You can control what's passed in the dropdown using the' ranges' attribute. The array passed here will be parsed and displayed on the card. All integers are transformed to days, and other string variables will be passed as they are.
You can also set a default range using the `initial_range` attribute.
The ranges have been changed a bit since **version 2.8**. The parameter you pass to the `range` option will be directly passed to the [`options_for_select`](https://apidock.com/rails/v5.2.3/ActionView/Helpers/FormOptionsHelper/options_for_select) helper, so it behaves more like a regular `select_tag`.
```ruby{4-15}
class Avo::Cards::UsersMetric < Avo::Cards::MetricCard
self.id = 'users_metric'
self.label = 'Users count'
self.initial_range = 30
self.ranges = {
"7 days": 7,
"30 days": 30,
"60 days": 60,
"365 days": 365,
Today: "TODAY",
"Month to date": "MTD",
"Quarter to date": "QTD",
"Year to date": "YTD",
All: "ALL"
}
end
```
## Keep the data fresh
If the parent is something that you keep on the big screen, you need to keep the data fresh at all times. That's easy using `refresh_every`. You pass the number of seconds you need to be refreshed and forget about it. Avo will do it for you.
```ruby{3}
class Avo::Cards::UsersMetric < Avo::Cards::MetricCard
self.id = 'users_metric'
self.refresh_every = 10.minutes
end
```
## Hide the header
In cases where you need to embed some content that should fill the whole card (like a map, for example), you can choose to hide the label and ranges dropdown.
```ruby{3}
class Avo::Cards::UsersMetric < Avo::Cards::MetricCard
self.id = 'users_metric'
self.display_header = false
end
```
## Format
Option `self.format` is useful when you want to format the data that `result` returns from `query`.
Example without format:
```ruby
class Avo::Cards::AmountRaised < Avo::Cards::MetricCard
self.id = "amount_raised"
self.label = "Amount raised"
self.prefix = "$"
def query
result 9001
end
end
```
Example with format:
```ruby
class Avo::Cards::AmountRaised < Avo::Cards::MetricCard
self.id = "amount_raised"
self.label = "Amount raised"
self.prefix = "$"
self.format = -> {
number_to_social value, start_at: 1_000
}
def query
result 9001
end
end
```
## Metric card
The metric card is your friend when you only need to display a simple big number. To generate one run `bin/rails g avo:card users_metric --type metric`.
#### Calculate results
To calculate your result, you may use the `query` method. After you make the query, use the `result` method to store the value displayed on the card.
In the `query` method you have access to a few variables like `context` (the App context), `params` (the request params), `range` (the range that was requested), `dashboard`, `resource` or `parent` (the current dashboard or resource the card is on), and current `card`.
```ruby{23-47,36}
class Avo::Cards::UsersMetric < Avo::Cards::MetricCard
self.id = 'users_metric'
self.label = 'Users count'
self.description = 'Some tiny description'
self.cols = 1
# self.rows = 1
# self.initial_range = 30
# self.ranges = {
# "7 days": 7,
# "30 days": 30,
# "60 days": 60,
# "365 days": 365,
# Today: "TODAY",
# "Month to date": "MTD",
# "Quarter to date": "QTD",
# "Year to date": "YTD",
# All: "ALL",
# }
# self.prefix = '$'
# self.suffix = '%'
# self.refresh_every = 10.minutes
def query
from = Date.today.midnight - 1.week
to = DateTime.current
if range.present?
if range.to_s == range.to_i.to_s
from = DateTime.current - range.to_i.days
else
case range
when 'TODAY'
from = DateTime.current.beginning_of_day
when 'MTD'
from = DateTime.current.beginning_of_month
when 'QTD'
from = DateTime.current.beginning_of_quarter
when 'YTD'
from = DateTime.current.beginning_of_year
when 'ALL'
from = Time.at(0)
end
end
end
result User.where(created_at: from..to).count
end
end
```
### Decorate the data using `prefix` and `suffix`
Some metrics might want to add a `prefix` or a `suffix` to display the data better.
```ruby{3,4}
class Avo::Cards::UsersMetric < Avo::Cards::MetricCard
self.id = 'users_metric'
self.prefix = '$'
self.suffix = '%'
end
```
`prefix` and `suffix` became callable options.
The blocks are executed using `Avo::ExecutionContext`. Within this blocks, you gain access to all attributes of `Avo::ExecutionContext` along with the `parent`.
```ruby{3,4}
class Avo::Cards::UsersMetric < Avo::Cards::MetricCard
self.id = 'users_metric'
self.prefix = -> { params[:prefix] || parent.prefix }
self.suffix = -> { params[:suffix] || parent.suffix }
end
```
## Chartkick card
A picture is worth a thousand words. So maybe a chart a hundred? Who knows? But creating charts in Avo is very easy with the help of the [chartkick](https://github.com/ankane/chartkick) gem.
You start by running `bin/rails g avo:card users_chart --type chartkick`.
```ruby
class Avo::Cards::UserSignups < Avo::Cards::ChartkickCard
self.id = 'user_signups'
self.label = 'User signups'
self.chart_type = :area_chart
self.description = 'Some tiny description'
self.cols = 2
# self.rows = 1
# self.chart_options = { library: { plugins: { legend: { display: true } } } }
# self.flush = true
# self.legend = false
# self.scale = false
# self.legend_on_left = false
# self.legend_on_right = false
def query
points = 16
i = Time.new.year.to_i - points
base_data =
Array
.new(points)
.map do
i += 1
[i.to_s, rand(0..20)]
end
.to_h
data = [
{ name: 'batch 1', data: base_data.map { |k, v| [k, rand(0..20)] }.to_h },
{ name: 'batch 2', data: base_data.map { |k, v| [k, rand(0..40)] }.to_h },
{ name: 'batch 3', data: base_data.map { |k, v| [k, rand(0..10)] }.to_h }
]
result data
end
end
```
### Chart types
Using the `self.chart_type` class attribute you can change the type of the chart. Supported types are `line_chart`, `pie_chart`, `column_chart`, `bar_chart`, `area_chart`, and `scatter_chart`.
### Customize chart
Because the charts are being rendered with padding initially, we offset that before rendering to make the chart look good on the card. To disable that, you can set `self.flush = false`. That will set the chart loose for you to customize further.
After you set `flush` to `false`, you can add/remove the `scale` and `legend`. You can also place the legend on the left or right using `legend_on_left` and `legend_on_right`.
These are just some of the predefined options we provide out of the box, but you can send different [chartkick options](https://github.com/ankane/chartkick#options) to the chart using `chart_options`.
If you'd like to use [Groupdate](https://github.com/ankane/groupdate), [Hightop](https://github.com/ankane/hightop), and [ActiveMedian](https://github.com/ankane/active_median) you should require them in your `Gemfile`. Only `chartkick` is required by default.
`chart.js` is supported for the time being. So if you need support for other types, please reach out or post a PR (π PRs are much appreciated).
`self.chartkick_options` accepts callable blocks:
```ruby
class Avo::Cards::ExampleAreaChart < Avo::Cards::ChartkickCard
self.chart_options: -> do
{
library: {
plugins: {
legend: {display: true}
}
}
}
end
end
```
`chartkick_options` can also be declared when registering the card:
```ruby
class Avo::Dashboards::Dashy < Avo::Dashboards::BaseDashboard
def cards
card Avo::Cards::ExampleAreaChart,
chart_options: {
library: {
plugins: {
legend: {display: true}
}
}
}
# OR
card Avo::Cards::ExampleAreaChart,
chart_options: -> do
{
library: {
plugins: {
legend: {display: true}
}
}
}
end
end
end
```
The blocks are executed using `Avo::ExecutionContext`. Within this blocks, you gain access to all attributes of `Avo::ExecutionContext` along with the `parent`, `arguments` and `result_data`.
## Partial card
You can use a partial card to add custom content to a card. Generate one by running `bin/rails g avo:card custom_card --type partial`. That will create the card class and the partial for it.
```ruby{5}
class Avo::Cards::ExampleCustomPartial < Avo::Cards::PartialCard
self.id = "users_custom_card"
self.cols = 1
self.rows = 4
self.partial = "avo/cards/custom_card"
# self.display_header = true
end
```
You can embed a piece of content from another app using an iframe. You can hide the header using the `self.display_header = false` option. That will render the embedded content flush to the container.
```ruby{5}
# app/avo/cards/map_card.rb
class Avo::Cards::MapCard < Avo::Cards::PartialCard
self.id = "map_card"
self.label = "Map card"
self.partial = "avo/cards/map_card"
self.display_header = false
self.cols = 2
self.rows = 4
end
```
```html
```
## Cards visibility
It's common to show the same card to multiple types of users (admins, regular users). In that scenario you might want to hide some cards for the regular users and show them just to the admins.
You can use the `visible` option to do that. It can be a `boolean` or a `block` where you can access the `params`, `current_user`, `context`, `parent`, and `card` object.
```ruby{4-11}
class Avo::Cards::UsersCount < Avo::Cards::MetricCard
self.id = "users_metric"
self.label = "Users count"
self.visible = -> do
# You have access to:
# context
# params
# parent (the current dashboard or resource)
# dashboard (will be nil when parent is resource)
# resource (will be nil when parent is dashboard)
# current card
true
end
def query
result User.count
end
end
```
You may also control the visibility from the parent class.
:::code-group
```ruby [On Dashboards]
class Avo::Dashboards::Dashy < Avo::Dashboards::BaseDashboard
def cards
card Avo::Cards::UsersCount, visible: -> { true }
end
end
```
```ruby [On Resources]
class Avo::Resources::User < Avo::BaseResource
def cards
card Avo::Cards::UsersCount, visible: -> { true }
end
end
```
:::
## Dividers
You may want to separate the cards. You can use dividers to do that.
```ruby [On Dashboards]
class Avo::Dashboards::Dashy < Avo::Dashboards::BaseDashboard
def cards
card Avo::Cards::ExampleColumnChart
card Avo::Cards::ExamplePieChart
card Avo::Cards::ExampleBarChart
divider label: "Custom partials"
card Avo::Cards::ExampleCustomPartial
card Avo::Cards::MapCard
end
end
```
Dividers can be a simple line between your cards or have some text on them that you control using the `label` option.
When you don't want to show the line, you can enable the `invisible` option, which adds the divider but does not display a border or label.
## Dividers visibility
You might want to conditionally show/hide a divider based on a few factors. You can do that using the `visible` option.
```ruby
divider label: "Custom partials", visible: -> {
# You have access to:
# context
# params
# parent (the current dashboard or resource)
# dashboard (will be nil when parent is resource)
# resource (will be nil when parent is dashboard)
true
}
```
---
# Kanban boards
:::warning
The feature and docs are both work in progress. Please read the `info` sections below.
:::
Having a kanban board is a great way to organize your work and keep track of your records.
## Overview
The Kanban Board feature is a way to create a kanban board for your resources. They support multiple resources. Think about GitHub's Projects. You can have Issues, PRs, and simple tasks on them.
The boards and columns and items are database backed and you can create them on the fly.
## Requirements
Some of these requirements might change over time.
- We tested this on an app with Avo Advanced license
- [`acts_as_list`](https://github.com/brendon/acts_as_list) gem (comes automatically as a requirement)
- [`hotwire_combobox`](https://github.com/josefarias/hotwire_combobox) gem (comes automatically as a requirement)
## Installation
To install the `avo-kanban` gem, follow the steps below:
1. Add the following line to your Gemfile:
```ruby
gem "avo-kanban", source: "https://packager.dev/avo-hq/"
```
2. Run the `bundle install` command to install the gem:
```bash
bundle install
```
3. Generate the necessary resources and controllers by running:
```bash
rails generate avo:kanban install
```
This command will create pre-configured resources and controllers for managing boards, columns, and items in your application. You can further customize the generated code to suit your needs.
This command will also generate the item's partial and a migration.
4. Run the migration to apply the database changes:
```bash
rails db:migrate
```
## DB schema
`Avo::Kanban::Board` -> has_many `Avo::Kanban::Column` -> has_many `Avo::Kanban::Item`
The `Avo::Kanban::Column` has a polymorphic `belongs_to` association with any other model you might have in your app.
## Create a kanban board
We can create a kanban board by going to the Boards resource and clicking on the `Create board` button.
Once you create the board, add it to the menu using the `link_to` option (for now. we'll add `board` soon).
## Create columns
For now you can create the columns from the resource view.
By default, each column will have a `name` and `value` assigned to it. It will also have a `position` that you can use to sort the columns.
The `value` is what is being used to update the record when it's dropped into a new column.
## Configure the board
Each board has a configuration attached to it.
We can configure what kind of resources can be added to the board.
Similar we can change the column names and the value from the settings screen.
## Adding items to the board
This is best done on the board. Under each column you'll find the new field. This will search throught the resources that you've selected in the configuration.
It will use the `self.search[:query]` block to search for the records. It will send two `for_kanban_board` and `board_id` arguments to the block so you can customize the query.
When an item is added to the a column, it will have an `Avo::Kanban::Item` record created for it. This `Item` record is responsible for keeping track of the board, column, position properties and more.
When an item is added to the a column it will update the property on the record to the column's `value`. More on what this means in the next section.
## How does it work?
Each board updates one `property` on the `record`, and each column represents a `value`.
The record is the actual record from the database (User, Project, To Do, etc.).
Let's say we are replicating the GitHub Projects boards.
### `Board` and `Column`s
We should have a `Board` record with the following columns:
- `No status` with an empty string as value
- `Backlog` with the value `backlog`
- `In progress` with the value `in_progress`
- `Done` with the value `done`
The board has the `property` option set to `status` so we ensure that the `status` property of the record is updated when we move the item to a new column.
### `Resource`s and `Item`s
We should have `Issue`, `PullRequest`, and `ToDo` models and resources. The resources should have the `self.search[:query]` block configured.
Each resource must have the `self.title` method configured. This title will be used as a label to identify records throughout the kanban board, including in the search box and on individual entries.
Next in our board we should select these resources as allowed from the board settings.
### Add items to the board
At the bottom of the `No status` column we can search for an `Issue`. When we select that issue, an `Avo::Kanban::Item` record will be created for it with references to the board, column, and record (that issue).
This automatically triggers the issue to change the status to an empty string because we added it to the `No status` column which has the `value` set to an empty string.
If we were to add it to the `Backlog` column, it would change the status to `backlog`.
### Move items between columns
Now, if we move the item to the `In progress` column, it will change the status to `in_progress`.
### Items without that property
Some models might belong on the same board but have different properties to show the status.
Some models might use a timestamp like `published_at` to show the status.
Or some models might belong to a a status but that isn't dictated by a single property but a collection of properties.
In order to mitigate that we can create virtual properties on the model.
Let's imagine that a new board that displays the posts in columns based on their "published" status. the board uses the `status` property to but the `Post` model doesn't have the `status` property as a column in the database.
We can create a virtual property on the model.
```ruby
class Post < ApplicationRecord
def status
if published_at.present?
"published"
elsif published_status == "draft"
"draft"
else
"private"
end
end
def status=(value)
if value == "published"
published_at = Time.now
published_status = "draft"
elsif value == "draft"
published_at = nil
published_status = "draft"
elsif value == "draft"
published_at = nil
published_status = nil
end
save!
end
end
```
## Customize the card
:::warning
This might change in the future.
:::
In order to customize the card, you can eject the `Avo::Kanban::Items::ItemComponent` component.
```bash
rails generate avo:eject --component Avo::Kanban::Items::ItemComponent
```
Then customize it at `app/components/avo/kanban/items/item_component.html.erb`
```erb
<%= item.record.name %>
```
The `item` is the `Avo::Kanban::Item` and the `record` is the actual record from the database.
## Authorization
This section assumes that you have already set up authorization in your application using Pundit.
1. Generate a policy for the `Board` resource by running:
```bash
rails generate pundit:policy board
```
### Authorization Methods
You can control access to various parts of the Kanban board by defining the following methods in your `BoardPolicy`:
- `manage_column?`
Controls the visibility of the three-dot menu on each column (used for column management).
- `edit?`
Controls the "Edit board" button on the board itself.
:::warning
Also controls the ability to edit the board in the resource view.
:::
- `add_item?`
Controls the visibility of the "Add item" button on the board, which allows users to add new items to a column.
:::warning
Doesn't impact the ability to add items via the bottom of each column.
:::
## Customizing kanban models for your business logic
Sometimes, the default kanban models aren't quite enough for your specific use case. Let's say you're building a project management system where kanban boards need to belong to specific teams, and each column represents a workflow stage that needs to track additional metadata like SLA targets or approval requirements.
In this scenario, you might need to extend the kanban models to add custom associations, validations, or callbacks that align with your business logic. Here's how you can safely extend the `Avo::Kanban::Board`, `Avo::Kanban::Column`, and `Avo::Kanban::Item` models:
```ruby{6-53}
# config/initializers/avo.rb
Avo.configure do |config|
config.root_path = '/admin'
# ... other config options ...
Rails.configuration.to_prepare do
Avo::Kanban::Board.class_eval do
belongs_to :team, optional: true
has_many :board_watchers, dependent: :destroy
validates :name, presence: true, uniqueness: { scope: :team_id }
end
Avo::Kanban::Column.class_eval do
belongs_to :workflow_stage, optional: true
has_one :sla_config, dependent: :destroy
after_update :notify_stage_change
private
def notify_stage_change
# Custom logic to notify team members of stage changes
BoardNotificationService.new(self).notify_stage_update
end
end
Avo::Kanban::Item.class_eval do
has_many :item_comments, dependent: :destroy
belongs_to :assignee, class_name: 'User', optional: true
after_destroy :cleanup_item_data
before_update :track_movement_history
private
def cleanup_item_data
# Clear any business-specific property when item is removed
record.update!("#{board.property}": nil)
end
def track_movement_history
if column_id_changed?
ItemMovementTracker.create!(
item: self,
from_column_id: column_id_was,
to_column_id: column_id,
moved_at: Time.current
)
end
end
end
end
end
```
This approach allows you to seamlessly integrate the kanban functionality with your existing domain models while maintaining the flexibility to add custom business logic as your application grows.
---
# Collaboration
Keep your team in sync with built-in comments and status updates. No more scattered communication across multiple tools.
:::info
Docs are a work in progress
:::
---
# Mount Avo API
This document explains how to mount and configure the Avo API in your Rails application.
## Overview
The `mount_avo_api` method is a convenient Rails route helper that mounts the Avo API engine into your application's routing system. It provides a RESTful API for all your Avo resources, allowing external applications to interact with your data programmatically.
:::warning IMPORTANT
There is a caveat when mounting the API that requires attention:
If you have `mount_avo` inside an `authenticate` block (like `authenticate :user`), you **must** mount the API outside and before that authentication block.
**Why?** When the API is mounted inside an authentication block, all API endpoints will require the same authentication as your web interface, which breaks API functionality for external clients using API tokens.
**This do not mean that API can't use authentication, check the Authentication page for more information.**
**Correct setup:**
```ruby
# config/routes.rb
Rails.application.routes.draw do
# Mount Avo API FIRST - outside any authentication blocks
mount_avo_api
# Mount Avo web interface with authentication
authenticate :user do
mount_avo
end
end
```
**Incorrect setups:**
```ruby
# config/routes.rb
Rails.application.routes.draw do
# β Don't do this - API will require web authentication
authenticate :user do
mount_avo_api # This breaks API token authentication
mount_avo
end
end
```
```ruby
# config/routes.rb
Rails.application.routes.draw do
# β Don't do this - API will require web authentication
authenticate :user do
mount_avo
end
mount_avo_api # This breaks API token authentication
end
```
:::
## Basic Usage
### Simple Mount
Add this to your `config/routes.rb` file:
```ruby
# config/routes.rb
Rails.application.routes.draw do
mount_avo_api
end
```
This will mount the API at the default path: `#{Avo.configuration.root_path}/api`
If your Avo is configured with `root_path = "/admin"`, the API will be available at `/admin/api`.
### Custom Mount Path
You can specify a custom mount path:
```ruby
Rails.application.routes.draw do
mount_avo_api at: "/avo_api"
end
```
This makes the API available at `/avo_api` instead of the default path.
## Configuration Options
### Mount Path Option
```ruby
mount_avo_api at: "/custom/api/path"
```
**Parameters:**
- `at:` - String specifying where to mount the API (default: `"#{Avo.configuration.root_path}/api"`)
### Additional Mount Options
You can pass any options that Rails' `mount` method accepts:
```ruby
mount_avo_api at: "/api", via: [:get, :post], constraints: { subdomain: "api" }
```
**Common options:**
- `via:` - Restrict HTTP methods
- `constraints:` - Add routing constraints
- `defaults:` - Set default parameters
### Custom Routes Block
You can provide a block to define custom routes within the API engine:
```ruby
Rails.application.routes.draw do
mount_avo_api do
# Custom routes within the API engine
get 'health', to: 'health#check'
get 'version', to: 'version#show'
# Custom namespaces
namespace :custom do
resources :reports, only: [:index, :show]
end
end
end
```
## Generated API Endpoints
When you mount the API, it automatically generates RESTful endpoints for all your Avo resources:
### Standard Endpoints Pattern
For each resource, the following endpoints are created:
```
GET /admin/api/resources/v1/{resource_name} # List resources
POST /admin/api/resources/v1/{resource_name} # Create resource
GET /admin/api/resources/v1/{resource_name}/:id # Show resource
PATCH /admin/api/resources/v1/{resource_name}/:id # Update resource
PUT /admin/api/resources/v1/{resource_name}/:id # Update resource
DELETE /admin/api/resources/v1/{resource_name}/:id # Delete resource
```
### Example for User Resource
If you have an `Avo::Resources::User` resource:
```
GET /admin/api/resources/v1/users # List users
POST /admin/api/resources/v1/users # Create user
GET /admin/api/resources/v1/users/1 # Show user
PATCH /admin/api/resources/v1/users/1 # Update user
PUT /admin/api/resources/v1/users/1 # Update user
DELETE /admin/api/resources/v1/users/1 # Delete user
```
## Complete Examples
### Basic Setup
```ruby
# config/routes.rb
Rails.application.routes.draw do
devise_for :users
# Mount Avo API
mount_avo_api
# Mount Avo
authenticate :user do
mount_avo do
get "tool_with_form", to: "tools#tool_with_form", as: :tool_with_form
end
end
# Redirect to Avo root path
root to: redirect(Avo.configuration.root_path)
end
```
### API with Custom Constraints
```ruby
# config/routes.rb
Rails.application.routes.draw do
# API only accessible from api subdomain
mount_avo_api at: "/api", constraints: { subdomain: "api" }
# API with IP restrictions (for internal use)
mount_avo_api at: "/internal/api", constraints: lambda { |request|
%w[127.0.0.1 10.0.0.0/8 192.168.0.0/16].any? { |ip|
IPAddr.new(ip).include?(request.remote_ip)
}
}
end
```
### Development vs Production Setup
```ruby
# config/routes.rb
Rails.application.routes.draw do
mount_avo
if Rails.env.development?
# Development: Mount API with debugging routes
mount_avo_api at: "/api" do
get 'debug/info', to: proc { |env|
info = {
version: Avo::Api::VERSION,
environment: Rails.env,
timestamp: Time.current.iso8601
}
[200, { 'Content-Type' => 'application/json' }, [info.to_json]]
}
end
else
# Production: Simple mount
mount_avo_api at: "/api"
end
end
```
---
# Avo API Controller Generators
This document explains how to use the Rails generators to create individual controllers for your Avo API resources.
## Overview
The `avo-api` gem provides two generators to help you create individual controllers for each resource. This allows for resource-specific customization while maintaining all the base functionality.
## Available Generators
### 1. Bulk Controller Generator
Creates controllers for **all existing Avo resources** at once.
```bash
rails generate avo_api:controllers
```
**What it does:**
- Automatically discovers all existing Avo resources in your application
- Creates individual controllers for each resource
- Places them in `app/controllers/avo/api/resources/v1/`
- Each controller inherits from `BaseResourcesController`
**Example output:**
```
β Created UsersController
β Created PostsController
β Created CommentsController
β Created ProfilesController
β Created TagsController
Generated 5 controllers successfully!
All controllers inherit from BaseResourcesController and can be customized as needed.
```
### 2. Single Controller Generator
Creates a controller for a **specific resource**.
```bash
rails generate avo_api:controller ResourceName
```
**Parameters:**
- `ResourceName` - The name of the resource (e.g., User, Post, BlogPost, ProductCategory)
**Examples:**
```bash
rails generate avo_api:controller User
rails generate avo_api:controller Post
rails generate avo_api:controller BlogPost
rails generate avo_api:controller ProductCategory
```
**Example output:**
```
create app/controllers/avo/api/resources/v1/users_controller.rb
Created UsersController at app/controllers/avo/api/resources/v1/users_controller.rb
You can now customize this controller by adding methods or overriding the base functionality.
```
## Generated Controller Structure
Each generated controller follows this structure:
```ruby
module Avo
module Api
module Resources
module V1
class UsersController < BaseResourcesController
# Add any custom logic for User resources here
#
# Example: Override methods to customize behavior
# def index
# super
# # Add custom logic after calling super
# end
#
# def show
# super
# # Add custom logic for show action
# end
end
end
end
end
end
```
## Resource to Controller Mapping
The generators follow Rails naming conventions:
| Avo Resource | Generated Controller |
|---------------|---------------------|
| `Avo::Resources::User` | `Avo::Api::Resources::V1::UsersController` |
| `Avo::Resources::Post` | `Avo::Api::Resources::V1::PostsController` |
| `Avo::Resources::Comment` | `Avo::Api::Resources::V1::CommentsController` |
| `Avo::Resources::BlogPost` | `Avo::Api::Resources::V1::BlogPostsController` |
| `Avo::Resources::ProductCategory` | `Avo::Api::Resources::V1::ProductCategoriesController` |
## File Locations
Generated controllers are placed in:
```
app/controllers/avo/api/resources/v1/
βββ users_controller.rb
βββ posts_controller.rb
βββ comments_controller.rb
βββ profiles_controller.rb
βββ tags_controller.rb
```
## Customizing Generated Controllers
Since each controller inherits from `BaseResourcesController`, you get all the standard CRUD functionality automatically. You can customize behavior by overriding methods:
### Example: Custom Index Logic
```ruby
class UsersController < BaseResourcesController
def index
super
# Add custom logic after the base index action
# The @resources variable contains the paginated records
# The @pagy variable contains pagination info
end
end
```
### Example: Custom Show Logic
```ruby
class UsersController < BaseResourcesController
def show
super
# Add custom logic after the base show action
# The @resource variable contains the Avo resource instance
# The @record variable contains the actual model record
end
end
```
### Example: Custom Create Logic
```ruby
class UsersController < BaseResourcesController
private
def create_success_action
# Custom success response for user creation
render json: {
record: serialize_record(@resource, :show),
message: "Welcome! Your account has been created successfully."
}, status: :created
end
def create_fail_action
# Custom error response for user creation
render json: {
errors: @record.errors,
message: "Account creation failed. Please check the errors below."
}, status: :unprocessable_entity
end
end
```
### Example: Adding Before Actions
```ruby
class UsersController < BaseResourcesController
before_action :require_admin, only: [:destroy]
before_action :log_user_access, only: [:show, :index]
private
def require_admin
# Custom authorization logic
head :forbidden unless current_user&.admin?
end
def log_user_access
# Custom logging logic
Rails.logger.info "User #{current_user&.id} accessed users API"
end
end
```
## Available Methods to Override
The `BaseResourcesController` provides these methods that you can override:
### CRUD Actions
- `index` - List resources
- `show` - Show single resource
- `create` - Create new resource
- `update` - Update existing resource
- `destroy` - Delete resource
### Success/Failure Callbacks
- `create_success_action` - Called after successful creation
- `create_fail_action` - Called after failed creation
- `update_success_action` - Called after successful update
- `update_fail_action` - Called after failed update
- `destroy_success_action` - Called after successful deletion
- `destroy_fail_action` - Called after failed deletion
### Serialization Methods
- `serialize_records(resources, view)` - Serialize multiple records
- `serialize_record(resource, view)` - Serialize single record
- `serialize_field_value(field)` - Serialize individual field
## Workflow
1. **Create your Avo resources** as usual in `app/avo/resources/`
2. **Update your routes** to use individual controllers (this is already done)
3. **Generate controllers** using one of the generators:
- `rails generate avo_api:controllers` (for all resources)
- `rails generate avo_api:controller ResourceName` (for specific resource)
4. **Customize as needed** by overriding methods in the generated controllers
## Notes
- Generated controllers automatically inherit all functionality from `BaseResourcesController`
- You don't need to implement basic CRUD operations unless you want to customize them
- The routing system automatically maps to the correct controller based on the resource name
- All existing Avo features (authorization, field visibility, etc.) continue to work
- Controllers are generated with helpful comments showing common customization patterns
---
# CSRF Protection in Avo API
## Overview
Cross-Site Request Forgery (CSRF) protection is a security measure that prevents malicious websites from making unauthorized requests on behalf of authenticated users. The Avo API implements CSRF protection using Rails' built-in mechanisms.
## Implementation
The Avo API implements CSRF protection through a customizable class method hook in the `Avo::Api::Resources::V1::ResourcesController`:
```ruby{10-12}
# app/controllers/avo/api/resources/v1/resources_controller.rb
module Avo
module Api
module Resources
module V1
class ResourcesController < Avo::BaseController
delegate :setup_csrf_protection, to: :class
before_action :setup_csrf_protection, prepend: true
def self.setup_csrf_protection
protect_from_forgery with: :null_session
end
end
end
end
end
end
```
This approach makes the CSRF protection easily configurable and overridable.
## Customizing CSRF Protection
You can override the `setup_csrf_protection` method in your controllers that inherit from `Avo::Api::Resources::V1::ResourcesController` to customize CSRF handling:
### Example 1: Change CSRF protection method
```ruby{7-9}
# app/controllers/avo/api/resources/v1/users_controller.rb
module Avo
module Api
module Resources
module V1
class UsersController < BaseResourcesController
def self.setup_csrf_protection
protect_from_forgery with: :exception
end
end
end
end
end
end
```
### Example 2: Disable CSRF protection entirely
```ruby{7-9}
# app/controllers/avo/api/resources/v1/users_controller.rb
module Avo
module Api
module Resources
module V1
class UsersController < BaseResourcesController
def self.setup_csrf_protection
# No CSRF protection - leave empty
end
end
end
end
end
end
```
## What is `:null_session`?
The `:null_session` strategy is specifically designed for API endpoints and works as follows:
1. **For requests with valid CSRF tokens**: Normal session handling continues
2. **For requests without valid CSRF tokens**: A new, empty session is created for the duration of the request
3. **No exceptions are raised**: Unlike other strategies, this doesn't raise `ActionController::InvalidAuthenticityToken`
## Why `:null_session` for APIs?
This strategy is ideal for REST APIs because:
- **Stateless Nature**: APIs are typically stateless and don't rely on browser sessions
- **Token-based Authentication**: APIs usually use tokens (JWT, API keys) rather than session-based authentication
- **Cross-Origin Requests**: APIs are designed to be consumed by various clients (mobile apps, SPAs, other services)
- **No CSRF Token Distribution**: API clients don't typically have access to CSRF tokens like HTML forms do
## Best Practices for API Consumers
When consuming the Avo API:
1. **Use Token-based Authentication**: Implement proper API token authentication
2. **HTTPS Only**: Always use HTTPS to prevent token interception
3. **Token Rotation**: Implement token rotation for long-lived applications
## Testing CSRF Protection
To test that CSRF protection is working:
```bash
# This should work (with null_session, no exception is raised)
curl -X POST http://localhost:3000/admin/api/resources/v1/users \
-H "Content-Type: application/json" \
-d '{"user": {"first_name": "Test User"}}'
```
## Related Security Considerations
- Implement proper authentication and authorization
- Use CORS headers appropriately for browser-based clients
- Validate all input data
- Use HTTPS in production
- Implement rate limiting for API endpoints
## References
- [Rails Security Guide - CSRF](https://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf)
- [ActionController CSRF Protection](https://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html)
---
# Authentication
Avo API provides a flexible authentication system that can be customized for your specific needs. By default, the API requires authentication for all requests, but you can override this behavior in your controllers.
## How it works
The API uses a delegated authentication pattern where:
1. **Default Behavior**: All requests are rejected with a 401 Unauthorized error by default
2. **Override Pattern**: Individual controllers can override the `setup_authentication` method to implement custom authentication logic
3. **Error Handling**: Authentication failures are handled gracefully with JSON error responses
4. **Flexibility**: You can disable authentication entirely, implement token-based auth, session-based auth, or any custom solution
## Default Authentication
By default, Avo API controllers inherit from `Avo::Api::Resources::V1::ResourcesController`, which implements strict authentication:
```ruby{12-14}
# app/controllers/avo/api/resources/v1/resources_controller.rb
module Avo
module Api
module Resources
module V1
class ResourcesController < Avo::BaseController
rescue_from Avo::Api::AuthenticationError do |exception|
render json: { error: 'Unauthorized' }, status: :unauthorized
end
before_action :setup_authentication, prepend: true
def setup_authentication
raise Avo::Api::AuthenticationError
end
end
end
end
end
end
```
This means all API requests will return a 401 Unauthorized response unless you override the authentication behavior.
## Overriding Authentication
You can customize authentication by overriding the `setup_authentication` class method in your individual controllers:
### Example 1: Disable Authentication
```ruby{7-9,13}
# app/controllers/avo/api/resources/v1/users_controller.rb
module Avo
module Api
module Resources
module V1
class UsersController < BaseResourcesController
def setup_authentication
# Leave empty to disable authentication
end
# OR
skip_before_action :setup_authentication
end
end
end
end
end
```
### Example 2: API Key Authentication
Most suitable for server-to-server communication:
```ruby{7-12}
# app/controllers/avo/api/resources/v1/users_controller.rb
module Avo
module Api
module Resources
module V1
class UsersController < BaseResourcesController
def setup_authentication
api_key = request.headers['Authorization']&.sub(/^ApiKey /, '')
unless ApiKey.active.exists?(key: api_key)
raise Avo::Api::AuthenticationError
end
end
end
end
end
end
end
```
### Example 3: HTTP Basic Authentication
For more sophisticated token-based auth:
```ruby{7-16}
# app/controllers/avo/api/resources/v1/users_controller.rb
module Avo
module Api
module Resources
module V1
class UsersController < BaseResourcesController
def setup_authentication
raise Avo::Api::AuthenticationError unless authenticate_with_http_basic do |email, password|
user = User.find_by(email: email)
if user&.valid_password?(password)
sign_in(user, store: false)
else
false
end
end
end
end
end
end
end
end
```
## Error Responses
When authentication fails, the API returns a standardized JSON error response:
```json
{
"error": "Unauthorized"
}
```
The response includes:
- **Status Code**: 401 Unauthorized
- **Content-Type**: application/json
- **Body**: JSON object with error message
## Best Practices
1. **Override at the Controller Level**: Each resource controller can have its own authentication strategy
2. **Use Strong Tokens**: If implementing token authentication, use cryptographically secure random tokens
3. **Rate Limiting**: Consider implementing rate limiting for API endpoints
4. **HTTPS Only**: Always use HTTPS in production for token-based authentication
5. **Token Rotation**: Implement token expiration and rotation for better security
## Security Considerations
- The default behavior (rejecting all requests) is secure by default
- Authentication errors are handled without exposing sensitive information
- Each controller can implement the authentication strategy that best fits its needs
## Testing Authentication
You can test your authentication implementations by making requests with and without proper credentials:
```bash{1-2,4-6}
# Should return 401
curl -X GET "http://localhost:3000/admin/api/resources/v1/users"
# Should return 200 (if properly authenticated)
curl -X GET "http://localhost:3000/admin/api/resources/v1/users"
-H "Authorization: Basic YXZvQGF2b2hxLmlvOldIWV9BUkVfWU9VX1NPX0NVUklPVVM/"
```
---
# Filters
Most content management systems need a way to filter the data.
Avo provides two types of filters you can use when building your app.
1. Basic filters
2. Dynamic filters
## Differences
### 1. Basic filters
- configured as one filter per file
- there are four types of filters (Text, Boolean, Select, Multiple select)
- they are more configurable
- you can scope out the information better
- you can use outside APIs or configurations
- you must add and configure each filter for a resource
### 2. Dynamic filters
- easier to set up. They only require one option on the field
- the user can choose the condition on which they filter the records
- a lot more conditions than basic filters
- the user can add multiple conditions per attribute
- they are more composable
---
# Filters
Filters allow you to better scope the index queries for records you are looking for.
Each filter is configured in a class with a few dedicated [methods and options](#filter-options). To use a filter on a resource you must [register it](#register-filters) and it will be displayed on the view.
## Filter options
## Register filters
In order to use a filter you must register it on a `Resource` using the `filter` method inside the `filters` method.
```ruby{9}
class Avo::Resources::Post < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id
end
def filters
filter Avo::Filters::Published
end
end
```
## Filter types
Avo has several types of filters available [Boolean filter](#Boolean%20Filter), [Select filter](#Select%20Filter), [Multiple select filter](#Multiple%20select%20filter), [Text filter](#Text%20Filter) and since version [Date time filter](#Date%20time%20Filter).
### Filter values
Because the filters get serialized back and forth, the final `value`/`values` in the `apply` method will be stringified or have the stringified keys if they are hashes. You can declare them as regular hashes in the `options` method, but they will get stringified.
## Dynamic filter options
You might want to compose more advanced filters, like when you have two filters, one for the country and another for cities, and you'd like to have the cities one populated with cities from the selected country.
Let's take the `Avo::Resources::Course` as an example.
```ruby{3-5,7-14}
# app/models/course.rb
class Course < ApplicationRecord
def self.countries
["USA", "Japan", "Spain", "Thailand"]
end
def self.cities
{
USA: ["New York", "Los Angeles", "San Francisco", "Boston", "Philadelphia"],
Japan: ["Tokyo", "Osaka", "Kyoto", "Hiroshima", "Yokohama", "Nagoya", "Kobe"],
Spain: ["Madrid", "Valencia", "Barcelona"],
Thailand: ["Chiang Mai", "Bangkok", "Phuket"]
}
end
end
```
We will create two filtersβone for choosing countries and another for cities.
```ruby{4-5}
# app/avo/resources/course.rb
class Avo::Resources::Course < Avo::BaseResource
def filters
filter Avo::Filters::CourseCountryFilter
filter Avo::Filters::CourseCityFilter
end
end
```
The country filter is pretty straightforward. Set the query so the `country` field to be one of the selected countries and the `options` are the available countries as `Hash`.
```ruby{6,10}
# app/avo/filters/course_country.rb
class Avo::Filters::CourseCountry < Avo::Filters::BooleanFilter
self.name = "Course country filter"
def apply(request, query, values)
query.where(country: values.select { |country, selected| selected }.keys)
end
def options
Course.countries.map { |country| [country, country] }.to_h
end
end
```
The cities filter has a few more methods to manage the data better, but the gist is the same. The `query` makes sure the records have the city value in one of the cities that have been selected.
The `options` method gets the selected countries from the countries filter (`Avo::Filters::CourseCountryFilter`) and formats them to a `Hash`.
```ruby{6,10}
# app/avo/filters/course_city.rb
class Avo::Filters::CourseCity < Avo::Filters::BooleanFilter
self.name = "Course city filter"
def apply(request, query, values)
query.where(city: values.select { |city, selected| selected }.keys)
end
def options
cities_for_countries countries
end
private
# Get a hash of cities for certain countries
# Example payload:
# countries = ["USA", "Japan"]
def cities_for_countries(countries_array = [])
countries_array
.map do |country|
# Get the cities for this country
Course.cities.stringify_keys[country]
end
.flatten
# Prepare to transform to a Hash
.map { |city| [city, city] }
# Turn to a Hash
.to_h
end
# Get the value of the selected countries
# Example payload:
# applied_filters = {
# "Avo::Filters::CourseCountryFilter" => {
# "USA" => true,
# "Japan" => true,
# "Spain" => false,
# "Thailand" => false,
# }
# }
def countries
if applied_filters["Avo::Filters::CourseCountryFilter"].present?
# Fetch the value of the countries filter
applied_filters["Avo::Filters::CourseCountryFilter"]
# Keep only the ones selected
.select { |country, selected| selected }
# Pluck the name of the coutnry
.keys
else
# Return empty array
[]
end
end
end
```
The `countries` method above will check if the `Avo::Filters::CourseCountryFilter` has anything selected. If so, get the names of the chosen ones. This way, you show only the cities from the selected countries and not all of them.
## React to filters
Going further with the example above, a filter can react to other filters. For example, let's say that when a user selects `USA` from the list of countries, you want to display a list of cities from the USA (that's already happening in `options`), and you'd like to select the first one on the list. You can do that with the `react` method.
```ruby{21-36}
# app/avo/filters/course_city.rb
class Avo::Filters::CourseCity < Avo::Filters::BooleanFilter
self.name = "Course city filter"
def apply(request, query, values)
query.where(city: values.select { |city, selected| selected }.keys)
end
def options
cities_for_countries countries
end
# applied_filters = {
# "Avo::Filters::CourseCountryFilter" => {
# "USA" => true,
# "Japan" => true,
# "Spain" => false,
# "Thailand" => false,
# }
# }
def react
# Check if the user selected a country
if applied_filters["Avo::Filters::CourseCountryFilter"].present? && applied_filters["Avo::Filters::CourseCityFilter"].blank?
# Get the selected countries, get their cities, and select the first one.
selected_countries = applied_filters["Avo::Filters::CourseCountryFilter"].select do |name, selected|
selected
end
# Get the first city
cities = cities_for_countries(selected_countries.keys)
first_city = cities.first.first
# Return the first city as selected
[[first_city, true]].to_h
end
end
private
# Get a hash of cities for certain countries
# Example payload:
# countries = ["USA", "Japan"]
def cities_for_countries(countries_array = [])
countries_array
.map do |country|
# Get the cities for this country
Course.cities.stringify_keys[country]
end
.flatten
# Prepare to transform to a Hash
.map { |city| [city, city] }
# Turn to a Hash
.to_h
end
# Get the value of the selected countries
# Example `applied_filters` payload:
# applied_filters = {
# "Avo::Filters::CourseCountryFilter" => {
# "USA" => true,
# "Japan" => true,
# "Spain" => false,
# "Thailand" => false,
# }
# }
def countries
if applied_filters["Avo::Filters::CourseCountryFilter"].present?
# Fetch the value of the countries filter
applied_filters["Avo::Filters::CourseCountryFilter"]
# Keep only the ones selected
.select { |country, selected| selected }
# Pluck the name of the coutnry
.keys
else
# Return empty array
[]
end
end
end
```
After all, filters are applied, the `react` method is called, so you have access to the `applied_filters` object.
Using the applied filter payload, you can return the value of the current filter.
```ruby
def react
# Check if the user selected a country
if applied_filters["Avo::Filters::CourseCountryFilter"].present? && applied_filters["Avo::Filters::CourseCityFilter"].blank?
# Get the selected countries, get their cities, and select the first one.
selected_countries = applied_filters["Avo::Filters::CourseCountryFilter"]
.select do |name, selected|
selected
end
# Get the first city
cities = cities_for_countries(selected_countries.keys)
first_city = cities.first.first
# Return the first city selected as a Hash
[[first_city, true]].to_h
end
end
```
Besides checking if the countries filter is populated (`applied_filters["Avo::Filters::CourseCountryFilter"].present?`), we also want to allow the user to customize the cities filter further, so we need to check if the user has added a value to that filter (`applied_filters["Avo::Filters::CourseCountryFilter"].blank?`).
If these conditions are true, the country filter has a value, and the user hasn't selected any values from the cities filter, we can react to it and set a value as the default one.
Of course, you can modify the logic and return all kinds of values based on your needs.
## Empty message text
There might be times when you will want to show a message to the user when you're not returning any options. You may customize that message using the `empty_message` option.
```ruby{4}
# app/avo/filters/course_city.rb
class Avo::Filters::CourseCity < Avo::Filters::BooleanFilter
self.name = "Course city filter"
self.empty_message = "Please select a country to view options."
def apply(request, query, values)
query.where(city: values.select { |city, selected| selected }.keys)
end
def options
if countries.present?
[]
else
["Los Angeles", "New York"]
end
end
private
def countries
# logic to fetch the countries
end
end
```
## Keep filters panel open
There are scenarios where you wouldn't want to close the filters panel when you change the values. For that, you can use the `keep_filters_panel_open` resource option.
More on this on the `keep_filters_panel_open` resource option.
## Filter arguments
Filters can have different behaviors according to their host resource. In order to achieve that, arguments must be passed like on the example below:
```ruby{12-14}
class Avo::Resources::Fish < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id
field :name, as: :text
field :user, as: :belongs_to
field :type, as: :text, hide_on: :forms
end
def filters
filter Avo::Filters::NameFilter, arguments: {
case_insensitive: true
}
end
end
```
Now, the arguments can be accessed inside `Avo::Filters::NameFilter` ***`apply` method***, ***`options` method*** and on the ***`visible` block***!
```ruby{4-6,8-14}
class Avo::Filters::Name < Avo::Filters::TextFilter
self.name = "Name filter"
self.button_label = "Filter by name"
self.visible = -> do
arguments[:case_insensitive]
end
def apply(request, query, value)
if arguments[:case_insensitive]
query.where("LOWER(name) LIKE ?", "%#{value.downcase}%")
else
query.where("name LIKE ?", "%#{value}%")
end
end
end
```
## Manually create encoded URLs
You may want to redirect users to filtered states of the view from other places in your app. In order to create those filtered states you may use these helpers functions or Rails helpers.
### Rails helpers
### Standalone helpers
---
# Dynamic filters
The Dynamic filters make it so easy to add multiple, composable, and dynamic filters to the view.
The first thing you need to do is add the `filterable: true` attribute to the fields you need to filter through. We use `ransack` behind the scenes so it's essential to configure the `ransackable_attributes` list to ensure that every filterable field is incorporated within it.
:::info Filter Combination Logic
When multiple filters are applied:
- Filters on the same attribute are combined using OR conditions
- Filters on different attributes are combined using AND conditions
For example, if you have two filters on the `name` field (one for "John" and one for "Jane"), the query will find records where the name is either "John" OR "Jane". However, if you have one filter on `name` for "John" and another on `status` for "active", the query will find records where the name is "John" AND the status is "active".
:::
```ruby{4-6} [Fields]
class Avo::Resources::Project < Avo::BaseResource
def fields
field :name, as: :text
field :status, as: :status, filterable: true
field :stage, as: :badge, filterable: true
field :country, as: :country, filterable: true
end
end
```
Authorize ransackable_attributes
```ruby{3,11}
class Project < ApplicationRecord
def self.ransackable_attributes(auth_object = nil)
["status", "stage", "country"] # the array items should be strings not symbols
end
end
# Or authorize ALL attributes at once
class Project < ApplicationRecord
def self.ransackable_attributes(auth_object = nil)
authorizable_ransackable_attributes
end
end
```
:::warning
Ensure the array items are strings, not symbols.
:::
This will make Avo add this new "Filters" button to the view of your resource.
When the user clicks the button, a new filters bar will appear below enabling them to add filters based on the attributes you marked as filterable.
The user can add multiple filters for the same attribute if they desire so.
## Filter types
The filter type determines the kind of input provided by the filter.
For instance, a [text](#text) type filter will render a text input field, while a [select](#select) type filter will render a dropdown menu with predefined options fetched from the field.
#### Conditions
Each filter type also offers a different set of conditions. Conditions specify how the input value should be applied to filter the data. For example, [text](#text) filters have conditions such as `Contains` or `Starts with`, while number filters include `=` (equals) or `>` (greater than).
#### Query
Avo uses the input value and the specified condition to build a Ransack query. The filter conditions and input values are translated into Ransack predicates, which are then used to fetch the filtered data.
For instance, in the text filter example above, the `Contains` condition and the input value `John` are translated into a Ransack query resulting into the SQL `LIKE` operator to find all records where the name contains `John`.
## Options
You can have a few customization options available that you can add in your `avo.rb` initializer file.
```ruby
Avo.configure do |config|
# Other Avo configurations
end
if defined?(Avo::DynamicFilters)
Avo::DynamicFilters.configure do |config|
config.button_label = "Advanced filters"
config.always_expanded = true
end
end
```
## Field to filter matching
On versions **lower** than the filters are not configurable so each field will have a dedicated filter type. Check how to do a more advanced configuration on the [custom dynamic filters](#custom-dynamic-filters) section.
Field-to-filter matching in versions **lower** than :
```ruby
def field_to_filter(type)
case type.to_sym
when :boolean
:boolean
when :date, :date_time, :time
:date
when :id, :number, :progress_bar
:number
when :select, :badge, :country, :status
:select
when :text, :textarea, :code, :markdown, :password, :trix
:text
else
:text
end
end
```
## Caveats
At some point we'll integrate the Basic filters into the dynamic filters bar. Until then, if you have both basic and dynamic filters on your resource you'll have two `Filters` buttons on your view.
To mitigate that you can toggle the `always_expanded` option to true.
## Custom Dynamic Filters
Dynamic filters are great but strict, as each field creates a specific filter type, each with its own icon and query. The query remains static, targeting only that particular field. Since version , dynamic filters have become customizable and, even better, can be declared without being bound to a field.
There are two ways to define custom dynamic filters: the field's `filterable` option and the `dynamic_filter` method.
### Defining custom dynamic filters
To start customizing a dynamic filter from the `filterable` option, change its value to a hash:
```ruby
field :first_name,
as: :text,
filterable: true # [!code --]
filterable: { } # [!code ++]
```
From this hash, you can configure several options specified below.
Alternatively, you can define a custom dynamic filter using the `dynamic_filter` method, which should be called inside the `filters` method:
```ruby
def filters
# ...
dynamic_filter :first_name
# ...
end
```
Each option specified below can be used as a key in the hash definition or as a keyword argument in the method definition.
:::info Filters order
The filter order is computed. Dynamic filters defined by the `dynamic_filter` method will respect the definition order and will be rendered first in the filter list. Filters declared using the field's `filterable` option will be sorted by label.
:::
:::warning Custom Dynamic Filter IDs
When using a custom dynamic filter, the generated filter ID may not directly correspond to a database column. In such cases, you should use the [`query_attributes`](#query_attributes) option to specify which database columns the filter should apply to.
For example, consider a `City` model with a `population` column in the database:
```ruby
# The filter ID is custom_population
# However, the filter should apply the query to the population attribute.
dynamic_filter :custom_population, query_attributes: :population
```
:::
## Guides & Tutorials
---
# π Overview
## π Overview
:::warning
π§ This section is a work in progress.
:::
---
# Generators
Avo Forms provides generators to help you quickly create forms and pages.
## Form Generator
```bash
rails generate avo:form your_form_name
```
This will create a new form file at `app/avo/forms/your_form_name.rb` with the following structure:
```ruby
# app/avo/forms/your_form_name.rb
class Avo::Forms::YourFormName < Avo::Forms::Core::Form
self.title = "Your Form Name"
self.description = "Manage your your form name"
def fields
field :example, as: :text, default: "Hello World"
end
def handle
flash[:success] = { body: "Form submitted successfully", timeout: :forever }
flash[:notice] = params[:example]
default_response
end
end
```
## Page Generator
The page generator creates a new page class.
```bash
rails generate avo:page your_page_name
```
This will create a new page file at `app/avo/pages/your_page_name.rb` with the following structure:
```ruby
class Avo::Pages::YourPageName < Avo::Forms::Core::Page
self.title = "Your Page Name"
self.description = "A page for your page name"
def forms
# form Avo::Forms::AnyFormClass
end
def sub_pages
# sub_page Avo::Pages::AnySubPageClass
end
end
```
:::tip
To create a sub-page, you need to create a page first. The sub-page need to be namespaced under the parent page.
Read more about the Page Hierarchy.
Example:
```ruby
# app/avo/pages/parent_page.rb
class Avo::Pages::ParentPage < Avo::Forms::Core::Page
self.title = "Parent Page"
self.description = "A page for parent page"
def sub_pages
sub_page Avo::Pages::ParentPage::SubPage
end
end
```
```ruby
# app/avo/pages/parent_page/sub_page.rb
class Avo::Pages::ParentPage::SubPage < Avo::Forms::Core::Page
self.title = "Sub Page"
self.description = "A page for sub page"
end
```
:::
## Best Practices
1. Use descriptive names for your forms and pages
2. Keep form fields focused and relevant to their purpose
3. Organize related forms and pages together
4. Use sub-pages to create a logical navigation structure
5. Add appropriate descriptions to help users understand the purpose of each form and page
---
# Pages
Avo provides a powerful page system that enables you to build structured interfaces with nested content organization. Pages in Avo work similarly to resources but are designed for displaying and managing forms and sub-pages rather than database records.
## Hierarchy
Pages are organized in a hierarchical structure that follows a specific pattern:
### Page Structure
1. **Main Pages**
Example: `Avo::Pages::Settings`
- Always 1 level deep in the namespace
- Act as containers for related sub-pages
- Have a navigation entry on the left sidebar menu (if `self.menu_entry` is `true`)
- When accessed directly, redirect to the default sub-page if one is configured
2. **Sub-Pages**
Example: `Avo::Pages::Settings::General`, `Avo::Pages::Settings::Notifications`
- Always 2 or more levels deep in the namespace
- Contain the actual forms and content
- Are accessible through the parent page's navigation
This hierarchical organization allows you to create structured interfaces where users can navigate through different sections and manage various settings or configurations.
### Navigation Behavior
When a user visits a main page that has sub-pages:
1. If a default sub-page is configured, the user is automatically redirected to it
2. If no default sub-page is configured, the main page is displayed with its own forms (if any)
3. Sub-pages are displayed in a sidebar navigation for easy access
## Generating Pages
The generator usage is documented in the Generators page.
## Page Configuration Options
Pages have several class attributes that you can configure to customize their behavior and appearance.
## Page Methods
## Complete Example
Here's a complete example showing a settings page with multiple sub-pages:
```ruby
# Main settings page
# app/avo/pages/settings.rb
class Avo::Pages::Settings < Avo::Forms::Core::Page
self.title = "Settings"
self.description = "Manage your application settings"
def sub_pages
sub_page Avo::Pages::Settings::General, default: true
sub_page Avo::Pages::Settings::Notifications
sub_page Avo::Pages::Settings::Integrations
sub_page Avo::Pages::Settings::Security
end
end
```
```ruby
# General settings sub-page
# app/avo/pages/settings/general.rb
class Avo::Pages::Settings::General < Avo::Forms::Core::Page
self.title = "General Settings"
self.description = "Basic application configuration"
def forms
form Avo::Forms::Settings::AppSettings
form Avo::Forms::Settings::CompanyInfo
end
end
```
```ruby
# Notifications sub-page
# app/avo/pages/settings/notifications.rb
class Avo::Pages::Settings::Notifications < Avo::Forms::Core::Page
self.title = "Notifications"
self.description = "Configure notification preferences"
def forms
form Avo::Forms::Settings::EmailNotifications
form Avo::Forms::Settings::SlackIntegration
end
end
```
## Adding Pages to the Menu
To make your pages accessible through Avo's main navigation, add them to your Avo configuration:
```ruby{9-10,12,14}
# config/initializers/avo.rb
Avo.configure do |config|
config.main_menu = -> {
section "Resources", icon: "avo/resources" do
all_resources
end
section "Configuration", icon: "cog" do
page Avo::Pages::Settings, icon: "adjustments"
page Avo::Pages::SystemHealth, icon: "heart"
# Or
all_pages
end
}
end
```
## Best Practices
**Keep the hierarchy shallow**: While you can nest pages deeply, it's best to keep the structure simple with main pages and one level of sub-pages.
**Set default sub-pages**: If your main page primarily serves as a container, always set a default sub-page to improve user experience.
**Use descriptive titles and descriptions**: Help users understand what each page contains and what actions they can perform.
**Group related functionality**: Use the page hierarchy to logically group related forms and settings.
---
# Forms
Avo provide a powerful way to build custom forms for your interface. Unlike resources that are tied to database models, forms are standalone components that can handle any kind of data processing, settings management, or custom workflows.
## Overview
Forms in Avo are designed to:
- Handle custom data processing and workflows
- Manage application settings and configurations
- Provide standalone forms not tied to specific models
- Integrate seamlessly with pages for organized interfaces
- Support all Avo field types and layout components
- Be rendered anywhere in the interface
Forms are typically displayed on Pages and can be used for various purposes like user preferences, system settings, data imports, or any custom functionality your application requires.
Forms can also be rendered as a standalone component anywhere in the interface. For example, you can render the general settings form in a tool by using the following code:
```erb
<%= render Avo::Forms::Settings::General.new.component %>
```
## Generating Forms
The generator usage is documented in the Generators page.
## Form Structure
Every form inherits from `Avo::Forms::Core::Form` and requires two main methods:
1. **`def fields`** - Define the form structure and fields
2. **`def handle`** - Process form submission and define response
```ruby
# app/avo/forms/app_settings.rb
class Avo::Forms::AppSettings < Avo::Forms::Core::Form
self.title = "Application Settings"
self.description = "Manage your application configuration"
def fields
field :app_name, as: :text
field :maintenance_mode, as: :boolean
end
def handle
# Process form data
flash[:notice] = "Settings updated successfully"
default_response
end
end
```
## Form Configuration Options
Forms have several class attributes that customize their behavior and appearance.
## Form Methods
## Field Types and Layout
Forms support all Avo field types and layout components:
### Basic Fields
```ruby
def fields
field :name, as: :text
field :email, as: :text, required: true
field :age, as: :number
field :active, as: :boolean
field :bio, as: :textarea
field :role, as: :select, options: { admin: "Admin", user: "User" }
end
```
### Panels and Organization
```ruby
def fields
main_panel "Personal Information" do
field :first_name, as: :text
field :last_name, as: :text
field :email, as: :text
end
panel "Preferences", description: "Customize your experience" do
field :theme, as: :select, options: { light: "Light", dark: "Dark" }
field :notifications, as: :boolean
end
end
```
### Clusters for Inline Layout
```ruby
def fields
main_panel do
cluster do
field :first_name, as: :text
field :last_name, as: :text
end
end
end
```
### Working with Records
You can bind form fields to existing records:
```ruby
def fields
field :first_name, record: Avo::Current.user
field :last_name, record: Avo::Current.user
field :email, record: Avo::Current.user
end
```
## Form Submission Handling
### Processing Form Data
```ruby
def handle
# Access form parameters
app_name = params[:app_name]
maintenance_mode = params[:maintenance_mode]
# Update application settings
Rails.application.config.app_name = app_name
cookies[:maintenance_mode] = maintenance_mode
# Set success message
flash[:notice] = "Settings updated successfully"
# Return standard response
default_response
end
```
### Flash Messages
```ruby
def handle
# Informative message
flash[:notice] = "Operation completed successfully"
# Error message
flash[:error] = "Something went wrong"
# Success with timeout
flash[:success] = { body: "Saved successfully", timeout: 3000 }
# Warning message without dismissing
flash[:warning] = { body: "Something went wrong", timeout: :forever }
default_response
end
```
### Working with Models
```ruby
def handle
# Update current user
current_user.update(params.permit(:first_name, :last_name, :email))
# Create new records
Post.create(title: params[:title], body: params[:body])
# Complex data processing
if params[:import_data]
ImportService.new(params[:file]).process
end
flash[:notice] = "Data processed successfully"
default_response
end
```
## Complete Examples
### User Profile Settings Form
```ruby
# app/avo/forms/profile_settings.rb
class Avo::Forms::ProfileSettings < Avo::Forms::Core::Form
self.title = "Profile Settings"
self.description = "Update your personal information"
def fields
main_panel do
cluster do
with_options stacked: true, record: Avo::Current.user do
field :first_name, as: :text, required: true
field :last_name, as: :text, required: true
end
end
field :email, as: :text, required: true, record: Avo::Current.user
field :phone, as: :text, record: Avo::Current.user
end
panel "Preferences" do
field :theme, as: :select,
options: { light: "Light", dark: "Dark", auto: "Auto" },
default: "auto"
field :email_notifications, as: :boolean, default: true
field :timezone, as: :select, options: ActiveSupport::TimeZone.all.map { |tz| [tz.name, tz.name] }
end
end
def handle
# Update user profile
current_user.update(params.permit(:first_name, :last_name, :email, :phone))
# Update preferences (assuming a preferences model)
current_user.preferences.update(
theme: params[:theme],
email_notifications: params[:email_notifications],
timezone: params[:timezone]
)
flash[:notice] = "Profile updated successfully"
default_response
end
end
```
### Application Settings Form
```ruby
# app/avo/forms/app_settings.rb
class Avo::Forms::AppSettings < Avo::Forms::Core::Form
self.title = "Application Settings"
self.description = "Configure global application settings"
def fields
main_panel do
field :app_name, as: :text,
default: -> { Rails.application.class.module_parent_name },
required: true
field :app_url, as: :text,
default: -> { request.base_url },
placeholder: "https://yourapp.com"
field :maintenance_mode, as: :boolean, default: false
end
panel "Email Configuration" do
field :support_email, as: :text,
default: "support@yourapp.com",
required: true
field :from_email, as: :text,
default: "noreply@yourapp.com",
required: true
end
panel "Feature Flags" do
field :enable_registrations, as: :boolean, default: true
field :enable_api_access, as: :boolean, default: false
field :max_file_upload_size, as: :number,
default: 10,
help_text: "Maximum file size in MB"
end
end
def handle
# Store in application configuration or settings model
settings = {
app_name: params[:app_name],
app_url: params[:app_url],
maintenance_mode: params[:maintenance_mode],
support_email: params[:support_email],
from_email: params[:from_email],
enable_registrations: params[:enable_registrations],
enable_api_access: params[:enable_api_access],
max_file_upload_size: params[:max_file_upload_size]
}
# Update application settings (your implementation)
ApplicationSettings.update_all(settings)
# Or store in Rails credentials
# Rails.application.credentials.update(settings)
flash[:success] = {
body: "Application settings updated successfully",
timeout: 5000
}
default_response
end
end
```
### Data Import Form
```ruby
# app/avo/forms/data_import.rb
class Avo::Forms::DataImport < Avo::Forms::Core::Form
self.title = "Import Data"
self.description = "Upload and import data from CSV files"
def fields
main_panel do
field :import_type, as: :select,
options: {
users: "Users",
products: "Products",
orders: "Orders"
},
required: true
field :csv_file, as: :file,
required: true,
help_text: "Select a CSV file to import"
field :skip_header_row, as: :boolean,
default: true,
help_text: "Skip the first row if it contains headers"
end
panel "Import Options" do
field :update_existing, as: :boolean,
default: false,
help_text: "Update existing records if found"
field :send_notification, as: :boolean,
default: true,
help_text: "Send email notification when import completes"
end
end
def handle
import_type = params[:import_type]
csv_file = params[:csv_file]
options = {
skip_header_row: params[:skip_header_row],
update_existing: params[:update_existing]
}
# Process the import
begin
importer = DataImporter.new(import_type, csv_file, options)
result = importer.process
if params[:send_notification]
ImportNotificationMailer.import_completed(current_user, result).deliver_later
end
flash[:success] = {
body: "Import completed: #{result[:imported]} records imported, #{result[:skipped]} skipped",
timeout: 10000
}
rescue => e
flash[:error] = "Import failed: #{e.message}"
end
default_response
end
end
```
## Best Practices
**Keep forms focused**: Each form should handle a specific set of related functionality rather than trying to do everything.
**Use descriptive titles and descriptions**: Help users understand what the form does and what data is expected.
**Organize with panels**: Group related fields together using panels for better user experience.
**Validate input**: Always validate and sanitize form input in your handle method.
**Provide feedback**: Use flash messages to inform users about the results of their actions.
**Handle errors gracefully**: Wrap potentially failing operations in begin/rescue blocks.
**Use default values**: Provide sensible defaults for form fields when possible.
**Consider async processing**: For long-running operations, consider using background jobs and provide appropriate feedback to users.
---
# Custom pages (custom tools)
You may use custom tools to create custom sections or views to add to your app.
## Generate tools
`bin/rails generate avo:tool dashboard` will generate the necessary files to show the new custom tool.
```bash{2-6}
βΆ bin/rails generate avo:tool dashboard
create app/views/avo/sidebar/items/_dashboard.html.erb
insert app/controllers/avo/tools_controller.rb
create app/views/avo/tools/dashboard.html.erb
route namespace :avo do
get "dashboard", to: "tools#dashboard"
end
```
### Controller
If this is your first custom tool, a new `ToolsController` will be generated for you. Within this controller, Avo created a new method.
```ruby
class Avo::ToolsController < Avo::ApplicationController
def dashboard
end
end
```
You can keep this action in this controller or move it to another controller and organize it differently.
### Route
```ruby{2-4}
Rails.application.routes.draw do
namespace :avo do
get "dashboard", to: "tools#dashboard"
end
authenticate :user, ->(user) { user.admin? } do
mount_avo
end
end
```
The route generated is wrapped inside a namespace with the `Avo.configuration.root_path` name. Therefore, you may move it inside your authentication block next to the Avo mounting call.
### Sidebar item
The `_dashboard.html.erb` partial will be added to the `app/views/avo/sidebar/items` directory. All the files in this directory will be loaded by Avo and displayed in the sidebar. They are displayed alphabetically, so you may change their names to reorder the items.
### Customize the sidebar
If you want to customize the sidebar partial further, you can eject and update it to your liking. We're planning on creating a better sidebar customization experience later this year.
## Add assets
You might want to import assets (javascript and stylesheets files) when creating custom tools or fields. You can do that so easily from v1.3. Please follow this guide to bring your assets with your asset pipeline.
## Using helpers from your app
You'll probably want to use some of your helpers in your custom tools. To have them available inside your custom controllers inherited from Avo's `ApplicationController`, you need to include them using the `helper` method.
```ruby{3-5,10}
# app/helpers/home_helper.rb
module HomeHelper
def custom_helper
'hey from custom helper'
end
end
# app/controllers/avo/tools_controller.rb
class Avo::ToolsController < Avo::ApplicationController
helper HomeHelper
def dashboard
@page_title = "Dashboard"
end
end
```
```erb{13}
# app/views/avo/tools/dashboard.html.erb
<%= render Avo::PanelComponent.new title: 'Dashboard', display_breadcrumbs: true do |c| %>
<% c.with_tools do %>
This is the panels tools section.
<% end %>
<% c.with_body do %>
What a nice new tool π
<%= custom_helper %>
<% end %>
<% end %>
```
### Using path helpers
Because you're in a Rails engine, you will have to prepend the engine object to the path.
#### For Avo paths
Instead of writing `resources_posts_path(1)` you have to write `avo.resources_posts_path(1)`.
#### For the main app paths
When you want to reference paths from your main app, instead of writing `posts_path(1)`, you have to write `main_app.posts_path`.
---
# Custom fields
Avo ships with 20+ well polished and ready to be used, fields out of the box.
When you need a field that is not provided by default, Avo makes it easy to add it.
## Generate a new field
Every new field comes with three [view components](https://viewcomponent.org/), `Edit` (which is also used in the `New` view), and `Show` and `Index`. There's also a `Field` configuration file.
`bin/rails generate avo:field progress_bar` generates the files for you.
:::info
Please restart your rails server after adding a new custom field.
:::
```bash{2-9}
βΆ bin/rails generate avo:field progress_bar
create app/components/avo/fields/progress_bar_field
create app/components/avo/fields/progress_bar_field/edit_component.html.erb
create app/components/avo/fields/progress_bar_field/edit_component.rb
create app/components/avo/fields/progress_bar_field/index_component.html.erb
create app/components/avo/fields/progress_bar_field/index_component.rb
create app/components/avo/fields/progress_bar_field/show_component.html.erb
create app/components/avo/fields/progress_bar_field/show_component.rb
create app/avo/fields/progress_bar_field.rb
```
The `ProgressBarField` file is what registers the field in your admin.
```ruby
class Avo::Fields::ProgressBarField < Avo::Fields::BaseField
def initialize(name, **args, &block)
super(name, **args, &block)
end
end
```
Now you can use your field like so:
```ruby{7}
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id, link_to_record: true
field :progress, as: :progress_bar
end
end
```
The generated view components are basic text fields for now.
```erb{1,9,14}
# app/components/avo/fields/progress_bar_field/edit_component.html.erb
<%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal do %>
<%= @form.text_field @field.id,
class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)),
placeholder: @field.placeholder,
disabled: @field.readonly %>
<% end %>
# app/components/avo/fields/progress_bar_field/index_component.html.erb
<%= index_field_wrapper field: @field do %>
<%= @field.value %>
<% end %>
# app/components/avo/fields/progress_bar_field/show_component.html.erb
<%= show_field_wrapper field: @field, index: @index do %>
<%= @field.value %>
<% end %>
```
You can customize them and add as much or as little content as needed. More on customization [below](#customize-the-views).
## Field options
This file is where you may add field-specific options.
```ruby{3-6,11-14}
# app/avo/fields/progress_bar_field.rb
class Avo::Fields::ProgressBarField < Avo::Fields::BaseField
attr_reader :max
attr_reader :step
attr_reader :display_value
attr_reader :value_suffix
def initialize(name, **args, &block)
super(name, **args, &block)
@max = 100
@step = 1
@display_value = false
@value_suffix = nil
end
end
```
The field-specific options can come from the field declaration as well.
```ruby{11-14,24}
# app/avo/fields/progress_bar_field.rb
class Avo::Fields::ProgressBarField < Avo::Fields::BaseField
attr_reader :max
attr_reader :step
attr_reader :display_value
attr_reader :value_suffix
def initialize(name, **args, &block)
super(name, **args, &block)
@max = args[:max] || 100
@step = args[:step] || 1
@display_value = args[:display_value] || false
@value_suffix = args[:value_suffix] || nil
end
end
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id, link_to_record: true
field :progress, as: :progress_bar, step: 10, display_value: true, value_suffix: "%"
end
end
```
## Field Visibility
If you need to hide the field in some view, you can use the visibility helpers.
```ruby{16}
# app/avo/fields/progress_bar_field.rb
class Avo::Fields::ProgressBarField < Avo::Fields::BaseField
attr_reader :max
attr_reader :step
attr_reader :display_value
attr_reader :value_suffix
def initialize(name, **args, &block)
super(name, **args, &block)
@max = args[:max] || 100
@step = args[:step] || 1
@display_value = args[:display_value] || false
@value_suffix = args[:value_suffix] || nil
hide_on :forms
end
end
```
## Customize the views
No let's do something about those views. Let's add a progress bar to the `Index` and `Show` views.
```erb{1,15}
# app/components/avo/fields/progress_bar_field/show_component.html.erb
<%= show_field_wrapper field: @field, index: @index do %>
<% if @field.display_value %>
<%= @field.value %><%= @field.value_suffix if @field.value_suffix.present? %>
<% end %>
<% end %>
# app/components/avo/fields/progress_bar_field/index_component.html.erb
<%= index_field_wrapper field: @field do %>
<% if @field.display_value %>
<%= @field.value %><%= @field.value_suffix if @field.value_suffix.present? %>
<% end %>
<% end %>
```
For the `Edit` view, we're going to do something different. We'll implement a `range` input.
```erb{1}
# app/components/avo/fields/progress_bar_field/edit_component.html.erb
<%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal do %>
<% if @field.display_value %>
<%= @field.value %><%= @field.value_suffix if @field.value_suffix.present? %>
<% end %>
<%= @form.range_field @field.id,
class: 'w-full',
placeholder: @field.placeholder,
disabled: @field.readonly,
min: 0,
# add the field-specific options
max: @field.max,
step: @field.step,
%>
<% end %>
```
## Field assets
Because there isn't just one standardized way of handling assets in Rails, we decided we won't provide **asset loading** support for custom fields for now. That doesn't mean that you can't use custom assets (javascript or CSS files), but you will have to load them in your own pipeline in dedicated Avo files.
In the example above, we added javascript on the page just to demonstrate the functionality. In reality, you might add that to a stimulus controller inside your own Avo dedicated pipeline (webpacker or sprockets).
Some styles were added in the asset pipeline directly.
```css
progress {
@apply h-2 bg-white border border-gray-400 rounded shadow-inner;
}
progress[value]::-webkit-progress-bar {
@apply bg-white border border-gray-500 rounded shadow-inner;
}
progress[value]::-webkit-progress-value{
@apply bg-green-600 rounded;
}
progress[value]::-moz-progress-bar {
@apply bg-green-600 rounded appearance-none;
}
```
## Use pre-built Stimulus controllers
Avo ships with a few Stimulus controllers that help you build more dynamic fields.
### Hidden input controller
This controller allows you to hide your content and add a trigger to show it. You'll find it in the Trix field.
You should add the `:always_show` `attr_reader` and `@always_show` instance variables to your field.
```ruby{3,8}
# app/avo/fields/color_picker_field.rb
class Avo::Fields::ColorPickerField < Avo::Fields::BaseField
attr_reader :always_show
def initialize(id, **args, &block)
super(id, **args, &block)
@always_show = args[:always_show] || false
@allow_non_colors = args[:allow_non_colors]
end
end
```
Next, in your fields `Show` component, you need to do a few things.
1. Wrap the field inside a controller tag
1. Add the trigger that will show the content.
1. Wrap the value in a div with the `hidden` class applied if the condition `@field.always_show` is `false`.
1. Add the `content` target (`data-hidden-input-target="content"`) to that div.
```erb{4-7,8}
# app/components/avo/fields/color_picker_field/show_component.html.erb
<%= show_field_wrapper field: @field, index: @index do %>
class="hidden" <% end %> data-hidden-input-target="content">
<%= @field.value %>
<% end %>
```
### Non existing model field
To ensure proper rendering of a custom field that lacks getters and setters at the model level, you must implement these methods within the model.
```ruby
def custom_field
end
def custom_field=(value)
end
```
---
# Custom errors
Actions such as create, update, attach, etc... will not be completed if the record contains any errors. This ensures that only valid data is processed and saved, maintaining the integrity of your application. Custom validations can be added to your models to enforce specific rules and provide meaningful error messages to users.
## Adding Custom Errors
To add custom errors, you can define a validation method in your model. If the validation fails it adds an error to the record. These errors will prevent the action from completing and will be displayed as notifications to the user.
## In a Simple Record
Consider a simple `User` model where you want to enforce a custom validation rule, such as ensuring that the user's age is over a certain value.
```ruby
# app/models/user.rb
class User < ApplicationRecord
validate :age_must_be_over_18
private
def age_must_be_over_18
# Add a custom error to the record if age is less than 18.
if age < 18
errors.add(:age, "must be over 18.")
end
end
end
```
In this example, the `age_must_be_over_18` method checks if the user's age is less than 18. If so, it adds an error to the `age` attribute with a custom message. This error prevents any further Avo action on the record and notifies the user of the issue.
## In a Join Table
Consider a join table `TeamMembership` which links `Team` and `User` models. You might want to add a custom validation to ensure some business logic is enforced.
```ruby
# app/models/team_membership.rb
class TeamMembership < ApplicationRecord
belongs_to :team
belongs_to :user
validate :custom_validation
private
def custom_validation
if user.banned?
errors.add(:user, "is banned.")
end
end
end
```
In this example, the `custom_validation` method is called whenever a `TeamMembership` record is validated. If the conditions in this method are not met, an error is added to the `user` attribute with a custom message. This error prevents any further Avo action on the record and notifies the user of the issue.
---
# Resource tools
Similar to adding custom fields to a resource, you can add custom tools. A custom tool is a partial added to your resource's `Show` and `Edit` views.
## Generate a resource tool
Run `bin/rails generate avo:resource_tool post_info`. That will create two files. The configuration file `app/avo/resource_tools/post_info.rb` and the partial file `app/views/avo/resource_tools/_post_info.html.erb`.
The configuration file holds the tool's name and the partial path if you want to override it.
```ruby
class Avo::ResourceTools::PostInfo < Avo::BaseResourceTool
self.name = "Post info"
# self.partial = "avo/resource_tools/post_info"
end
```
The partial is ready for you to customize further.
```erb
<%= render Avo::PanelComponent.new title: "Post info" do |c| %>
<% c.with_tools do %>
<%= a_link('/avo', icon: 'heroicons/solid/academic-cap', style: :primary) do %>
Dummy link
<% end %>
<% end %>
<% c.with_body do %>
πͺ§ This partial is waiting to be updated
You can edit this file here app/views/avo/resource_tools/post_info.html.erb.
The resource tool configuration file should be here app/avo/resource_tools/post_info.rb.
<%
# In this partial, you have access to the following variables:
# tool
# @resource
# @resource.model
# form (on create & edit pages. please check for presence first)
# params
# Avo::Current.context
# current_user
%>
<% end %>
<% end %>
```
## Partial context
You might need access to a few things in the partial.
You have access to the `tool`, which is an instance of your tool `PostInfo`, and the `@resource`, which holds all the information about that particular resource (`view`, `model`, `params`, and others), the `params` of the request, the `Avo::Current.context` and the `current_user`.
That should give you all the necessary data to scope out the partial content.
## Tool visibility
The resource tool is default visible on the `Show` view of a resource. You can change that using the visibility options (`show_on`, `only_on`).
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
def fields
tool Avo::ResourceTools::PostInfo, show_on: :edit
end
end
```
### Using path helpers
Because you're in a Rails engine, you will have to prepend the engine object to the path.
#### For Avo paths
Instead of writing `resources_posts_path(1)` you have to write `avo.resources_posts_path(1)`.
#### For the main app paths
When you want to reference paths from your main app, instead of writing `posts_path(1)`, you have to write `main_app.posts_path`.
## Add custom fields on forms
**From Avo 2.12**
You might want to add a few more fields or pieces of functionality besides the CRUD-generated fields on your forms. Of course, you can already create new custom fields to do it in a more structured way, but you can also use a resource tool to achieve more custom behavior.
You have access to the `form` object that is available on the new/edit pages on which you can attach inputs of your choosing. You can even achieve nested form functionality.
You have to follow three steps to enable this functionality:
1. Add the inputs in a resource tool and enable the tool on the form pages
2. Tell Avo which `params` it should permit to write to the model
3. Make sure the model is equipped to receive the params
In the example below, we'll use the `Avo::Resources::Fish`, add a few input fields (they will be a bit unstyled because this is not the scope of the exercise), and do some actions with some of them.
We first need to generate the tool with `bin/rails g avo:resource_tool fish_information` and add the tool to the resource file.
```ruby{3}
class Avo::ResourcesFish < Avo::BaseResource
def fields
tool Avo::ResourceTools::FishInformation, show_on: :forms
end
end
```
In the `_fish_information.html.erb` partial, we'll add a few input fields. Some are directly on the `form`, and some are nested with `form.fields_for`.
The fields are:
- `fish_type` as a text input
- `properties` as a multiple text input which will produce an array in the back-end
- `information` as nested inputs which will produce a `Hash` in the back-end
```erb{13-36}
<%= render Avo::PanelComponent.new(title: @resource.model.name) do |c| %>
<% c.with_tools do %>
<%= a_link('/admin', icon: 'heroicons/solid/academic-cap', style: :primary) do %>
Primary
<% end %>
<% end %>
<% c.with_body do %>
```
Next, we need to tell Avo and Rails which params are welcomed in the `create`/`update` request. We do that using the `extra_params` option on the `Avo::Resources::Fish`. Avo's internal implementation is to assign the attributes you specify here to the underlying model (`model.assign_attributes params.permit(extra_params)`).
```ruby{2}
class Avo::Resources::Fish < Avo::BaseResource
self.extra_params = [:fish_type, :something_else, properties: [], information: [:name, :history]]
def fields
tool Avo::ResourceTools::FishInformation, show_on: :forms
end
end
```
The third step is optional. You must ensure your model responds to the params you're sending. Our example should have the `fish_type`, `properties`, and `information` attributes or setter methods on the model class. We chose to add setters to demonstrate the params are called to the model.
```ruby
class Fish < ApplicationRecord
self.inheritance_column = nil # required in order to use the type DB attribute
def fish_type=(value)
self.type = value
end
def properties=(value)
# properties should be an array
puts ["properties in the Fish model->", value].inspect
end
def information=(value)
# properties should be a hash
puts ["information in the Fish model->", value].inspect
end
end
```
If you run this code, you'll notice that the `information.information_age` param will not reach the `information=` method because we haven't allowed it in the `extra_params` option.
## Where to add logic
It's a good practice not to keep login in view files (partials).
You can hide that logic inside the tool using instance variables and methods, and access it in the partial using the `tool` variable.
[Here's an example](https://github.com/avo-hq/main.avodemo.com/commit/c8ecb9b53a770103a993df4c2b3acec0a1faf737) on how you could do that.
```ruby{8,10}
class Avo::ResourceTools::PostInfo < Avo::BaseResourceTool
self.name = "Post info"
# self.partial = "avo/resource_tools/post_info"
attr_reader :foo
def initialize(**kwargs)
super **kwargs # It's important to call super with the same keyword arguments
# You'll have access to the following objects:
# resource - when attached to a resource
# parent - which is the object it's attached to (resource if attached to a resource)
# view
@foo = :bar # Add your variables
end
def custom_method_call
:called
end
end
```
```erb{7,12}
<%= render Avo::PanelComponent.new title: "Post info" do |c| %>
<% c.with_body do %>
This variable was declared in the initializer:
<%= tool.foo %>
This is a method called on the tool:
<%= tool.custom_method_call %>
<% end %>
<% end %>
```
---
# Stimulus JS & HTML attributes
:::warning
This feature is in the **beta** phase. The API might change while seeing how the community uses it to build their apps.
This is not the **dependable fields** feature but a placeholder so we can observe and see what we need to ship to make it helpful to you.
:::
_What we'll be able to do at the end of reading these docs_
:::info
**Please note** that in order to have the JS code from your controllers loaded in Avo you'll need to add your asset pipeline using these instructions. It's really easier than it sounds. It's like you'd add a new JS file to your regular Rails app.
:::
One of the most requested features is the ability to make the forms more dynamic. We want to bring the first iteration of this feature through Stimulus JS integration.
This light layer will allow you to hook into the views and inject your functionality with Stimulus JS.
You'll be able to add your Stimulus controllers to the resource views (`Index`, `Show`, `Edit`, and `New`), attach `classes`, `style`, and `data` attributes to the fields and inputs in different views.
## Assign Stimulus controllers to resource views
To enable a stimulus controller to resource view, you can use the `stimulus_controllers` option on the resource file.
```ruby
class Avo::Resources::Course < Avo::BaseResource
self.stimulus_controllers = "course-resource"
end
```
You can add more and separate them by a space character.
```ruby
class Avo::Resources::Course < Avo::BaseResource
self.stimulus_controllers = "course-resource select-field association-fields"
end
```
Avo will add a `resource-[VIEW]` (`resource-edit`, `resource-show`, or `resource-index`) controller for each view.
### Field wrappers as targets
By default, Avo will add stimulus target data attributes to all field wrappers. The notation scheme uses the name and field type `[FIELD_NAME][FIELD_TYPE]WrapperTarget`.
```ruby
# Wrappers get the `data-[CONTROLLER]-target="nameTextWrapper"` attribute and can be targeted using nameTextWrapperTarget
field :name, as: :text
# Wrappers get the `data-[CONTROLLER]-target="createdAtDateTimeWrapper"` attribute and can be targeted using createdAtDateTimeWrapperTarget
field :created_at, as: :date_time
# Wrappers get the `data-[CONTROLLER]-target="hasSkillsTagsWrapper"` attribute and can be targeted using hasSkillsTagsWrapperTarget
field :has_skills, as: :tags
```
For example for the following stimulus controllers `self.stimulus_controllers = "course-resource select-field association-fields"` Avo will generate the following markup for the `has_skills` field above on the `edit` view.
```html{4-7}
```
You can add those targets to your controllers and use them in your JS code.
### Field inputs as targets
Similar to the wrapper element, inputs in the `Edit` and `New` views get the `[FIELD_NAME][FIELD_TYPE]InputTarget`. On more complex fields like the searchable, polymorphic `belongs_to` field, where there is more than one input, the target attributes are attached to all `input`, `select`, and `button` elements.
```ruby
# Inputs get the `data-[CONTROLLER]-target="nameTextInput"` attribute and can be targeted using nameTextInputTarget
field :name, as: :text
# Inputs get the `data-[CONTROLLER]-target="createdAtDateTimeInput"` attribute and can be targeted using createdAtDateTimeInputTarget
field :created_at, as: :date_time
# Inputs get the `data-[CONTROLLER]-target="hasSkillsTagsInput"` attribute and can be targeted using hasSkillsTagsInputTarget
field :has_skills, as: :tags
```
### All controllers receive the `view` value
All stimulus controllers receive the `view` attribute in the DOM.
```html{4-5}
```
Now you can use that inside your Stimulus JS controller like so:
```js{5,9}
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static values = {
view: String,
}
async connect() {
console.log('view ->', this.viewValue)
}
}
```
The possible values are `index`, `show`, `edit`, or `new`
## Assign Stimulus controllers to actions
Similarly as to resource, you can assign stimulus controller to an action. To do that you can use the `stimulus_controllers` option on the action file.
```ruby
class Avo::Actions::ShowCurrentTime < Avo::BaseAction
self.stimulus_controllers = "city-in-country"
end
```
You can add more and separate them by a space character.
```ruby
class Avo::Actions::ShowCurrentTime < Avo::BaseAction
self.stimulus_controllers = "course-resource select-field association-fields"
end
```
The same way as for the resources, Avo will add stimulus target data attributes to [all field wrappers](#field-wrappers-as-targets) and [all input fields](#field-inputs-as-targets).
Unlike with the resource, Avo will not add a specific default controller for each type of the view (`index`, `show`, `edit`).
Same way, the controllers will not receive the `view` attribute in the DOM, [as in case of resources](#all-controllers-receive-the-view-value).
## Attach HTML attributes
This section has moved.
## Composing the attributes together
You can use the attributes together to make your fields more dynamic.
```ruby{3-9}
field :has_skills, as: :boolean, html: {
edit: {
input: {
data: {
# On click run the toggleSkills method on the toggle-fields controller
action: "input->toggle-fields#toggleSkills",
}
}
}
}
field :skills, as: :tags, html: {
edit: {
wrapper: {
# hide this field by default
classes: "hidden"
}
}
}
```
```js
// toggle_fields_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["skillsTagsWrapper"]; // use the target Avo prepared for you
toggleSkills() {
this.skillsTagsWrapperTarget.classList.toggle("hidden");
}
}
```
## Pre-made stimulus methods
Avo ships with a few JS methods you may use on your resources.
### `resource-edit#toggle`
On your `Edit` views, you can use the `resource-edit#toggle` method to toggle the field visibility from another field.
```ruby{5-7}
field :has_country, as: :boolean, html: {
edit: {
input: {
data: {
action: "input->resource-edit#toggle", # use the pre-made stimulus method on input
resource_edit_toggle_target_param: "countrySelectWrapper", # target to be toggled
# resource_edit_toggle_targets_param: ["countrySelectWrapper"] # add more than one target
}
}
}
}
field :country, as: :select, options: Course.countries.map { |country| [country, country] }.to_h
```
### `resource-edit#disable`
Disable works similarly to toggle, with the difference that it disables the field instead of hiding it.
```ruby{5-7,16}
field :has_skills, as: :boolean, html: {
edit: {
input: {
data: {
action: "input->resource-edit#disable", # use the pre-made stimulus method on input
resource_edit_disable_target_param: "countrySelectInput", # target to be disabled
# resource_edit_disable_targets_param: ["countrySelectWrapper"] # add more than one target to disable
}
}
}
}
field :country, as: :select, options: Course.countries.map { |country| [country, country] }.to_h
```
You may also target the `wrapper` element for that field if the target field has more than one input like the searchable polymorphic `belongs_to` field.
```ruby{6}
field :has_skills, as: :boolean, html: {
edit: {
input: {
data: {
action: "input->resource-edit#disable", # use the pre-made stimulus method on input
resource_edit_disable_target_param: "countrySelectWrapper", # target the wrapper so all inputs are disabled
# resource_edit_disable_targets_param: ["countrySelectWrapper"] # add more than one target to disable
}
}
}
}
field :country, as: :select, options: Course.countries.map { |country| [country, country] }.to_h
```
### `resource-edit#debugOnInput`
For debugging purposes only, the `resource_edit` Stimulus JS controller provides the `debugOnInput` method that outputs the event and value for an action to the console. Use this just to make sure you targeted your fields correctly. It doesn't have any real use.
## Custom Stimulus controllers
:::info Check the source code
If you visit our demo website on the [course edit page](https://main.avodemo.com/avo/resources/courses/1/edit) you can see this in action.
- Demo of the feature in action
https://main.avodemo.com/avo/resources/courses/1/edit
- JS controller that does that change
https://github.com/avo-hq/main.avodemo.com/blob/main/app/javascript/controllers/course_controller.js
- Rails controller that returns the results
https://github.com/avo-hq/main.avodemo.com/blob/main/app/controllers/avo/courses_controller.rb#L3
- Stimulus action that triggers the update
https://github.com/avo-hq/main.avodemo.com/blob/main/app/avo/resources/course.rb#L68
:::
The bigger purpose of this feature is to create your own Stimulus JS controllers to bring the functionality you need to the CRUD interface.
Below is an example of how you could implement a city & country select feature where the city select will have its options changed when the user selects a country:
1. Add an action to the country select to trigger a change.
1. The stimulus method `onCountryChange` will be triggered when the user changes the country.
1. That will trigger a fetch from the server where Rails will return an array of cities for the provided country.
1. The city field will have a `loading` state while we fetch the results.
1. The cities will be added to the `city` select field
1. If the initial value is present in the returned results, it will be selected.
1. All of this will happen only on the `New` and `Edit` views because of the condition we added to the `connect` method.
::: code-group
```ruby [app/avo/resources/course.rb]
# app/avo/resources/course.rb
class Avo::Resources::Course < Avo::BaseResource
self.stimulus_controllers = "course-resource"
def fields
field :id, as: :id
field :name, as: :text
field :country, as: :select, options: Course.countries.map { |country| [country, country] }.to_h, html: {
edit: {
input: {
data: {
course_resource_target: "countryFieldInput", # Make the input a target
action: "input->course-resource#onCountryChange" # Add an action on change
}
}
}
}
field :city, as: :select, options: Course.cities.values.flatten.map { |city| [city, city] }.to_h, html: {
edit: {
input: {
data: {
course_resource_target: "cityFieldInput" # Make the input a target
}
}
}
}
end
end
```
```ruby{4-6} [config/routes.rb]
Rails.application.routes.draw do
if defined? ::Avo
Avo::Engine.routes.draw do
scope :resources do
get "courses/cities", to: "courses#cities"
end
end
end
end
```
```ruby{3} [app/controllers/avo/courses_controller.rb]
class Avo::CoursesController < Avo::ResourcesController
def cities
render json: get_cities(params[:country]) # return an array of cities based on the country we received
end
private
def get_cities(country)
return [] unless Course.countries.include?(country)
Course.cities[country.to_sym]
end
end
```
```ruby [app/models/course.rb]
class Course < ApplicationRecord
def self.countries
["USA", "Japan", "Spain", "Thailand"]
end
def self.cities
{
USA: ["New York", "Los Angeles", "San Francisco", "Boston", "Philadelphia"],
Japan: ["Tokyo", "Osaka", "Kyoto", "Hiroshima", "Yokohama", "Nagoya", "Kobe"],
Spain: ["Madrid", "Valencia", "Barcelona"],
Thailand: ["Chiang Mai", "Bangkok", "Phuket"]
}
end
end
```
```js [course_resource_controller.js]
import { Controller } from "@hotwired/stimulus";
const LOADER_CLASSES = "absolute bg-gray-100 opacity-10 w-full h-full";
export default class extends Controller {
static targets = ["countryFieldInput", "cityFieldInput", "citySelectWrapper"];
static values = {
view: String,
};
// Te fields initial value
static initialValue;
get placeholder() {
return this.cityFieldInputTarget.ariaPlaceholder;
}
set loading(isLoading) {
if (isLoading) {
// create a loader overlay
const loadingDiv = document.createElement("div");
loadingDiv.className = LOADER_CLASSES;
loadingDiv.dataset.target = "city-loader";
// add the loader overlay
this.citySelectWrapperTarget.prepend(loadingDiv);
this.citySelectWrapperTarget.classList.add("opacity-50");
} else {
// remove the loader overlay
this.citySelectWrapperTarget
.querySelector('[data-target="city-loader"]')
.remove();
this.citySelectWrapperTarget.classList.remove("opacity-50");
}
}
async connect() {
// Add the controller functionality only on forms
if (["edit", "new"].includes(this.viewValue)) {
this.captureTheInitialValue();
// Trigger the change on load
await this.onCountryChange();
}
}
// Read the country select.
// If there's any value selected show the cities and prefill them.
async onCountryChange() {
if (this.hasCountryFieldInputTarget && this.countryFieldInputTarget) {
// Get the country
const country = this.countryFieldInputTarget.value;
// Dynamically fetch the cities for this country
const cities = await this.fetchCitiesForCountry(country);
// Clear the select of options
Object.keys(this.cityFieldInputTarget.options).forEach(() => {
this.cityFieldInputTarget.options.remove(0);
});
// Add blank option
this.cityFieldInputTarget.add(new Option(this.placeholder));
// Add the new cities
cities.forEach((city) => {
this.cityFieldInputTarget.add(new Option(city, city));
});
// Check if the initial value is present in the cities array and select it.
// If not, select the first item
const currentOptions = Array.from(this.cityFieldInputTarget.options).map(
(item) => item.value
);
if (currentOptions.includes(this.initialValue)) {
this.cityFieldInputTarget.value = this.initialValue;
} else {
// Select the first item
this.cityFieldInputTarget.value =
this.cityFieldInputTarget.options[0].value;
}
}
}
// Private
captureTheInitialValue() {
this.initialValue = this.cityFieldInputTarget.value;
}
async fetchCitiesForCountry(country) {
if (!country) {
return [];
}
this.loading = true;
const response = await fetch(
`${window.Avo.configuration.root_path}/resources/courses/cities?country=${country}`
);
const data = await response.json();
this.loading = false;
return data;
}
}
```
:::
This is how the fields behave with this Stimulus JS controller.
## Use Stimulus JS in a tool
There are a few steps you need to take in order to register the Stimulus JS controller in the current app context.
First, you need to have a JS entrypoint (ex: `avo.custom.js`) and have that loaded in the `_head` partial. For instructions on that please follow these steps to add it to your app (`importmaps` or `esbuild`).
### Set up a controller
```js
// app/javascript/controllers/sample_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
connect() {
console.log("Hey from sample controller π");
}
}
```
### Register that controller with the current Stimulus app
```js
// app/javascript/avo.custom.js
import SampleController from "controllers/sample_controller";
// Hook into the stimulus instance provided by Avo
const application = window.Stimulus;
application.register("course-resource", SampleController);
// eslint-disable-next-line no-console
console.log("Hi from Avo custom JS π");
```
### Use the controller in the Avo tool
```erb
```
Done π Now you have a controller connecting to a custom Resource tool or Avo tool (or Avo views).
## StimulusJS in actions
Currently, Avo doesn't support the use of StimulusJS in the same manner in action modals. Please follow [this](https://github.com/avo-hq/avo/issues/2811) issue to get updates on when it will be available.
This is not very prio on our near roadmap, but we will take a contribution in the form of a PR or a sponsorship in order to prioritize it on our end.
---
# Custom asset pipeline
Avo plays well with most Rails asset pipelines.
| Asset pipeline | Avo compatibility |
|---------------|------------|
| [importmap](https://github.com/rails/importmap-rails) | β Fully supported |
| [Propshaft](https://github.com/rails/propshaft) | β Fully supported |
| [Sprockets](https://github.com/rails/sprockets) | β Fully supported |
| [Webpacker](https://github.com/rails/webpacker) | π» Only with Sprockets or Propshaft |
There are two things we need to mention when communicating about assets.
1. Avo's assets
2. You custom assets
## Avo's assets
We chose to impact your app, and your deploy processes as little as possible. That's why we bundle up Avo's assets when we publish on [rubygems](https://rubygems.org/gems/avo), so you don't have to do anything else when you deploy your app. Avo doesn't require a NodeJS, or any kind of any other special environment in your deploy process.
Under the hood Avo uses TailwindCSS 3.0 with the JIT engine and bundles the assets using [`jsbundling`](https://github.com/rails/jsbundling-rails) with `esbuild`.
## Exclude servings Avo assets from a CDN?
If you utilize a Content Delivery Network (CDN) for serving assets and you want to exclude Avo paths from the default asset host you may use the following code snippet.
```ruby
config.action_controller.asset_host = Proc.new do |source|
# Exclude assets under the "/avo" path from CDN
next nil if source.start_with?("/avo")
# Set the general asset host (CDN) using an environment variable
ENV.fetch("ASSET_HOST")
end
```
This configuration ensures that assets are served through the specified CDN, except for those under the `/avo` path. Adjust the paths and environment variable as needed for your application.
## Your custom assets
Avo makes it easy to use your own styles and javascript through your already set up asset pipeline. It just hooks on to it to inject the new assets to be used in Avo.
## Use TailwindCSS utility classes
Please follow the dedicated TailwindCSS integration guide.
## Add custom JS code and Stimulus controllers
There are more ways of dealing with JS assets, and Avo handles that well.
## Use Importmap to add your assets
Importmap has become the default way of dealing with assets in Rails 7. For you to start using custom JS assets with Avo and importmap you should run this install command `bin/rails generate avo:js:install`. That will:
- create your `avo.custom.js` file as your JS entrypoint;
- add it to the `app/views/avo/partials/_head.html.erb` partial so Avo knows to load it;
- pin it in your `importmap.rb` file so `importmap-rails` knows to pick it up.
## Use `js-bundling` with `esbuild`
`js-bundling` gives you a bit more flexibility and power when it comes to assets. We use that under the hood and we'll use it to expose your custom JS assets.
When you install `js-bundling` with `esbuild` you get this npm script `"build": esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets`. That script will take all your JS entrypoint files under `app/javascript` and bundle them under `assets/builds`.
```bash
bin/rails generate avo:js:install --bundler esbuild
```
That command will:
- eject the `_head.html.erb` file;
- add the `avo.custom.js` asset to it;
- create the `avo.custom.js` file under `app/javascript` which will be your entrypoint.
That will be picked up by the `build` script and create it's own `assets/builds/avo.custom.js` file that will, in turn, be picked up by sprockets or propshaft and loaded into your app.
## Use `js-bundling` with `rollup` or `webpack`
Avo supports the other bundlers too but we just don't have a generator command to configure them for you. If you use the other bundlers and have configured them to use custom assets, then please [open up a PR](https://github.com/avo-hq/avo) and help the community get started faster.
## Manually add your CSS and JS assets
In order to manually add your assets you have to eject the `_pre_head.html.erb` partial (`bin/rails generate avo:eject --partial :pre_head`), create the asset files (examples below), and add the asset files from your pipeline to the `_pre_head` partial. Then, your asset pipeline will pick up those assets and use add them to your app.
:::warning
You should add your custom styles to `_pre_head.html.erb`, versus `_head.html.erb` to avoid overriding Avo's default styles. This
The order in which Avo loads the partials and asset files is this one:
1. `_pre_head.html.erb`
2. Avo's CSS and JS assets
3. `_head.html.erb`
:::
### Sprockets and Propshaft
Create `avo.custom.js` to the `app/javascripts` directory and `avo.custom.css` to `app/assets/stylesheets` with the desired scripts and styles.
Then add them to Avo using the `_pre_head.html.erb` partial (`rails generate avo:eject --partial :pre_head`).
```erb
# app/views/avo/partials/_pre_head.html.erb
<%= javascript_include_tag 'avo.custom', defer: true %>
<%= stylesheet_link_tag 'avo.custom', media: 'all' %>
```
:::warning
Please ensure that when using `javascript_include_tag` you add the `defer: true` option so the browser will use the same loading strategy as Avo's and the javascript files are loaded in the right order.
:::
### Webpacker
:::warning
We removed support for webpacker. In order to use Avo with your assets you must install Sprockets or Propshaft in order to serve assets like SVG, CSS, or JS files.
:::
:::info
Instructions below are for Webpacker version 6. Version 5 has different paths (`app/javascript/packs`).
:::
Create `avo.custom.js` and `avo.custom.css` inside `app/packs/entrypoints` with the desired scripts and styles.
Then add them to Avo using the `_pre_head.html.erb` partial (`rails generate avo:eject --partial :pre_head`).
```erb
# app/views/avo/partials/_pre_head.html.erb
<%= javascript_pack_tag 'avo.custom', defer: true %>
<%= stylesheet_pack_tag 'avo.custom', media: 'all' %>
```
---
# TailwindCSS integration
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 feature, not a bug. It's a performance optimization.
But there's an easy way to overcome that. You can add your own TailwindCSS process to watch for your the utility classes you use.
In versions prior to Avo 3, we maintained separate pre-compiled assets and provided a way to inject your Tailwind CSS assets into Avo's application. This often led to stylesheet conflicts. Now, we've improved integration by compiling a single stylesheet during the build process. If you want to add Tailwind configurations to Avo, your application will compile Avo's assets alongside your own in one build.
```bash
bin/rails generate avo:tailwindcss:install
```
That command will:
- install `tailwindcss-rails` gem if you haven't installed it yet;
- generate Avo's tailwind config.js `config/avo/tailwind.config.js`
- generate tailwind `base`, `components` and `utilities` under `app/assets/stylesheets/avo/tailwind` directory (workaround to import avo's base css after tailwind's base)
- create a custom `app/assets/stylesheets/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 build script to your `package.json`. **Ensure a `package.json` file is present;`yarn init` will generate one if your project doesn't have one**.
- add the following code to your `Rakefile`:
```ruby
# When running `rake assets:precompile` this is the order of events:
# 1 - Task `avo:yarn_install`
# 2 - Task `avo:sym_link`
# 3 - Cmd `yarn avo:tailwindcss`
# 4 - Task `assets:precompile`
Rake::Task["assets:precompile"].enhance(["avo:sym_link"])
Rake::Task["avo:sym_link"].enhance(["avo:yarn_install"])
Rake::Task["avo:sym_link"].enhance do
`yarn avo:tailwindcss`
end
```
Now, instead of running `bin/rails server`, you can run that Procfile with `bin/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.
:::
Inside `app/assets/stylesheets/avo` you'll have a new `tailwind.css` file that's waiting for you to customize. The default `config/avo/tailwind.config.js` file should have the proper paths set up for purging and should be ready to go. Notice that it utilizes an preset that we manage, that preset is essential to build all avo's styles.
```css
@import 'tailwindcss/base';
/* Have all of Avo's custom and plugins styles available. */
@import '../../../../tmp/avo/avo.base.css';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
/*
@layer components {
.btn-primary {
@apply py-2 px-4 bg-blue-200;
}
}
*/
```
:::warning Avo Task Dependencies
You must ensure that the `avo:sym_link` and `avo:yarn_install` tasks are executed before building the Avo assets.
These tasks are responsible for creating various symbolic links within the `tmp/avo` directory and installing necessary Node modules within Avo's path. These modules are essential for utilizing the Avo Tailwind preset. And the symbolic links are essentials for purging all Avo's tailwind classes.
:::
---
# Media Library
If you run an asset-intensive, having a place to view all those asses would be great. It's becoming easier with Avo and it's Media Library feature.
The Media Library has two goals in mind.
1. Browse and manage all your assets
2. Use it to inject assets in all three of Avo's rich text editors (trix, rhino, and markdown).
:::warning
The Media Library feature is still in alpha and future releases might contain breaking changes so keep an eye out for the upgrade guide.
This is just the initial version and we'll be adding more features as we progress and get more feedback on usage.
:::
## How to enable it
The Media Library feature is disabled by default (until we release the stable version). To enable it, you need to do the following:
```ruby{4}
# config/initializers/avo.rb
if defined?(Avo::MediaLibrary)
Avo::MediaLibrary.configure do |config|
config.enabled = true
end
end
```
This is the killswitch of the whole feature.
When disabled, the Media Library will not be available to anyone. It will hide the menu item, block the all the routes, and hide media the library icons from the editors.
## Hide menu item
You can hide the menu item from the sidebar by setting the `visible` option to `false`.
```ruby
# config/initializers/avo.rb
if defined?(Avo::MediaLibrary)
Avo::MediaLibrary.configure do |config|
config.visible = false
end
end
```
You may also use a block to conditionally show the menu item. You'll have access to the `Avo::Current` object and you can use it to show the menu item based on the current user.
```ruby
# config/initializers/avo.rb
if defined?(Avo::MediaLibrary)
Avo::MediaLibrary.configure do |config|
config.visible = -> { Avo::Current.user.is_developer? }
end
end
```
This will hide the menu item from the sidebar if the current user is not a developer.
## Add it to the menu editor
The Media Library is a menu item in the sidebar. You can add it to the menu editor by using the `media_library` helper.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.main_menu = lambda {
link_to 'Media Library', avo.media_library_index_path
}
end
```
## Use it with the rich text editors
The Media Library will seamlessly integrate with all the rich text editors.
```ruby
field :body, as: :trix
field :body, as: :rhino
field :body, as: :markdown
```
The editors will each have a button to open the Media Library modal.
Once open, after the user selects the asset, it will be injected into the editor.
---
# Cache
Avo uses the application's cache system to enhance performance. The cache system is especially beneficial when dealing with resource index tables and license requests.
## Cache store selection
The cache system dynamically selects the appropriate cache store based on the application's environment:
### Production
In production, if the existing cache store is one of the following: `ActiveSupport::Cache::MemoryStore` or `ActiveSupport::Cache::NullStore` it will use the default `:file_store` with a cache path of `tmp/cache`. Otherwise, the existing cache store `Rails.cache` will be used.
### Test
In testing, it directly uses the `Rails.cache` store.
### Development and other environments
In all other environments the `:memory_store` is used.
### Custom selection
You can force Avo to use a particular store.
```ruby
# config/initializers/avo.rb
config.cache_store = -> {
ActiveSupport::Cache.lookup_store(:solid_cache_store)
}
# or
config.cache_store = ActiveSupport::Cache.lookup_store(:solid_cache_store)
```
`cache_store` configuration option is expecting a cache store object, the lambda syntax can be useful if different stores are desired on different environments.
:::warning `MemoryStore` in production
Our recomendation is to not use MemoryStore in production because it will not be shared between multiple processes (when using Puma).
:::
## Caching caveats
Avo caches each record on the view for improved performance. However side-effects may occur from this strategy. We'll try to outline some of them below and keep this page up to date as we find them or as they get reported to us.
These are things that may happen to regular Rails apps, not just in the Avo context.
### Rows may not be automatically updated when certain associations change
There are two things you could do to prevent this:
#### Option 1: Use `touch: true` on association
Example with Parent Model and Association
```ruby
class Post < ApplicationRecord
has_many :comments, dependent: :destroy
end
```
Example with Child Model and Association with `touch: true`
```ruby
class Comment < ApplicationRecord
belongs_to :post, touch: true
end
```
#### Option 2: override `cache_hash` method on resource to take associations in consideration
Avo, internally, uses the `cache_hash` method to compute the hash that will be remembered by the caching driver when displaying the rows.
You can take control and override it on that particular resource to take the association into account.
```ruby
class Avo::Resources::User < Avo::BaseResource
def fields
# your fields
end
def cache_hash(parent_record)
# record.post will now be taken under consideration
result = [record, file_hash, record.post]
if parent_record.present?
result << parent_record
end
result
end
end
```
### `root_path` change won't break the cache keys
When the rows are cached, the links from the controls, `belongs_to` and `record_link` fields, and maybe others will be cached along.
The best solution here is to clear the cache with this ruby command `Rails.cache.clear`. If that's not an option then you can try to add the `root_path` to the `cache_hash` method in your particular resource.
## Solid Cache
Avo seamlessly integrates with [Solid Cache](https://github.com/rails/solid_cache). To setup Solid Cache follow these essential steps
Add this line to your application's Gemfile:
```ruby
gem "solid_cache"
```
And then execute:
```bash
$ bundle
```
Or install it yourself as:
```bash
$ gem install solid_cache
```
Add the migration to your app:
```bash
$ bin/rails solid_cache:install:migrations
```
Then run it:
```bash
$ bin/rails db:migrate
```
To set Solid Cache as your Rails cache, you should add this to your environment config:
```ruby
# config/environments/production.rb
config.cache_store = :solid_cache_store
```
Check [Solid Cache repository](https://github.com/rails/solid_cache) for additional valuable information.
---
# Views performance
## Log ViewComponent loading times and allocations
Sometimes, you may want to track the loading times and memory allocations of ViewComponents, similar to how you do with partials. Follow these two steps to enable this functionality.
#### 1. Enable ViewComponent Instrumentation
First, you need to enable instrumentation for ViewComponents. Add the following configuration to your `application.rb` or `development.rb` file:
```ruby
# application.rb or development.rb
config.view_component.instrumentation_enabled = true
```
#### 2. Add Logging
Next, set up logging to capture the performance data. Create or update the `config/initializers/view_component.rb` file with the following code:
```ruby
# config/initializers/view_component.rb
module ViewComponent
class LogSubscriber < ActiveSupport::LogSubscriber
define_method :'!render' do |event|
info do
message = +" Rendered #{event.payload[:name]}"
message << " (Duration: #{event.duration.round(1)}ms"
message << " | Allocations: #{event.allocations})"
end
end
end
end
ViewComponent::LogSubscriber.attach_to :view_component
```
:::warning
Enabling this logging can negatively impact your applicationβs performance. We recommend using it in the development environment or disabling it in production once you have completed debugging.
:::
---
# `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::Current.context
# current_user
%>
<% end %>
<% end %>
```
:::
You may add new fields using the `avo_show_field`, or `avo_edit_field` methods and use the arguments you are used to from resources.
```ruby
# In your resource file
field :name, as: :text
```
```erb
<%= avo_edit_field :name, as: :text %>
```
## The `form` option
If this is an or a view, you should pass it the `form` object that an Avo resource tool provides for you.
```erb
<%= avo_edit_field :name, as: :text, form: form %>
```
## The `value` option
When you are building a show field and you want to give it a value to show, use the `value` options
```erb
<%= avo_show_field(:photo, as: :external_image, value: record.cdn_image) %>
```
## Other field options
The fields take all the field options you are used to like, `help`, `required`, `readonly`, `placeholder`, and more.
```erb
<%= avo_edit_field :name, as: :text, form: form, help: "The user's name", readonly: -> { !current_user.is_admin? }, placeholder: "John Doe", nullable: true %>
```
## Component options
The field taks a new `component_options` argument that will be passed to the view component for that field. Please check out the field wrapper documentation for more details on that.
## `avo_field` helper
You may use the `avo_field` helper to conditionally switch from `avo_show_field` and `avo_edit_field`.
```erb
<%= avo_field :name, as: :text, view: :show %>
<%= avo_field :name, as: :text, view: :edit %>
<%= avo_field :name, as: :text, view: ExampleHelper.view_conditional %>
```
---
# Field wrappers
Each field display in your Avo resource has a field wrapper that helps display it in a cohesive way across the whole app.
This not only helps with a unitary design, but also with styling in a future theming feature.
:::info
You'll probably never have to use these components and helpers by themselves, but we'd like to document how they work as a future reference for everyone.
:::
# Index field wrapper
Each field displayed on the view is wrapped in this component that regulates the way content is displayed and makes it easy to control some options.
You may use the component `Avo::Index::FieldWrapperComponent` or the helper `index_field_wrapper`.
# 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
---
# Internals
This section documents on how we think about the internals of Avo and hwo much you could/should hook into them to extend it.
### Public Methods and Internal Usage
Not all public methods within the Avo codebase are meant for direct user consumption. Some methods are publicly accessible but primarily intended for internal use by various components of the Avo framework itself. This distinction arises due to the complex nature of building a framework or an ecosystem of gems, where numerous moving parts require public interfaces for framework developers rather than for end users.
---
# Testing
:::info
We know the testing guides aren't very detailed, and some testing helpers are needed. So please send your feedback [here](https://github.com/avo-hq/avo/discussions/1168).
:::
Testing is an essential aspect of your app. Most Avo DSLs are Ruby classes, so regular testing methods should apply.
## Testing helpers
We prepared a few testing helpers for you to use in your apps. They will help with opening/closing datepickers, choosing the date, saving the records, add/remove tags, and also select a lot of elements throughout the UI.
You can find them all [here](https://github.com/avo-hq/avo/blob/main/lib/avo/test_helpers.rb),
## Testing Actions
Given this `Avo::Actions::ReleaseFish`, this is the `spec` that tests it.
```ruby
class Avo::Actions::ReleaseFish < Avo::BaseAction
self.name = "Release fish"
self.message = "Are you sure you want to release this fish?"
def fields
field :message, as: :textarea, help: "Tell the fish something before releasing."
end
def handle(query:, fields:, **_)
query.each(&:release)
succeed "#{query.count} fish released with message '#{fields[:message]}'."
end
end
```
```ruby
require 'rails_helper'
RSpec.feature Avo::Actions::ReleaseFish, type: :feature do
let(:fish) { create :fish }
let(:current_user) { create :user }
let(:resource) { Avo::Resources::User.new.hydrate model: fish }
it "tests the dummy action" do
args = {
fields: {
message: "Bye fishy!"
},
current_user: current_user,
resource: resource,
query: [fish]
}
action = described_class.new(resource: resource, user: current_user, view: :edit)
expect(action).to receive(:succeed).with "1 fish released with message 'Bye fishy!'."
expect(fish).to receive(:release)
action.handle **args
end
end
```
---
# `Avo::Current`
`Avo::Current` is based on the `Current` pattern Rails exposes using [`ActiveSupport/CurrentAttributes`](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html).
On each request Avo will set some values on it.
**Related:**
- Multitenancy
---
# Execution context
Avo enables developers to hook into different points of the application lifecycle using blocks.
That functionality can't always be performed in void but requires some pieces of state to set up some context.
Computed fields are one example.
```ruby
field :full_name, as: :text do
"#{record.first_name} #{record.last_name}"
end
```
In that block we need to pass the `record` so you can compile that value. We send more information than just the `record`, we pass on the `resource`, `view`, `view_context`, `request`, `current_user` and more depending on the block that's being run.
## How does the `ExecutionContext` work?
The `ExecutionContext` is an object that holds some pieces of state on which we execute a lambda function.
```ruby
module Avo
class ExecutionContext
attr_accessor :target, :context, :params, :view_context, :current_user, :request
def initialize(**args)
# If target don't respond to call, handle will return target
# In that case we don't need to initialize the others attr_accessors
return unless (@target = args[:target]).respond_to? :call
args.except(:target).each do |key,value|
singleton_class.class_eval { attr_accessor "#{key}" }
instance_variable_set("@#{key}", value)
end
# Set defaults on not initialized accessors
@context ||= Avo::Current.context
@params ||= Avo::Current.params
@view_context ||= Avo::Current.view_context
@current_user ||= Avo::Current.current_user
@request ||= Avo::Current.request
end
delegate :authorize, to: Avo::Services::AuthorizationService
# Return target if target is not callable, otherwise, execute target on this instance context
def handle
target.respond_to?(:call) ? instance_exec(&target) : target
end
end
end
# Use it like so.
SOME_BLOCK = -> {
"#{record.first_name} #{record.last_name}"
}
Avo::ExecutionContext.new(target: &SOME_BLOCK, record: User.first).handle
```
This means you could throw any type of object at it and it it responds to a `call` method wil will be called with all those objects.
---
# Execution context
[`Avo::Services::EncryptionService`](https://github.com/avo-hq/avo/blob/main/lib/avo/services/encryption_service.rb) it's used internally by Avo when is needed to encrypt sensible params.
One example is the select all feature, where we pass the query, encrypted, through params.
## How does the [`Avo::Services::EncryptionService`](https://github.com/avo-hq/avo/blob/main/lib/avo/services/encryption_service.rb) work?
The `EncryptionService` is an service that can be called anywhere on the app.
### Public methods
### Mandatory arguments:
### Optional arguments
This service uses [`ActiveSupport::MessageEncryptor`](https://api.rubyonrails.org/v5.2.3/classes/ActiveSupport/MessageEncryptor.html) as encryptor so [`Avo::Services::EncryptionService`](https://github.com/avo-hq/avo/blob/main/lib/avo/services/encryption_service.rb) accepts any argument specified on [`ActiveSupport::MessageEncryptor` documentation](https://api.rubyonrails.org/v5.2.3/classes/ActiveSupport/MessageEncryptor.html)
## Usage example
### Basic text:
```ruby
secret_encryption = Avo::Services::EncryptionService.encrypt(message: "Secret string", purpose: :demo)
# "x+rnETtClF2cb80PtYzlULnVB0vllf+FvwoqBpPbHWa8q6vlml5eRWrwFMcYrjI6--h2MiT1P5ctTUjwfQ--k2WsIRknFVE53QwXADDDJw=="
Avo::Services::EncryptionService.decrypt(message: secret_encryption, purpose: :demo)
# "Secret string"
```
### Objects with custom serializer:
```ruby
secret_encryption = Avo::Services::EncryptionService.encrypt(message:Course::Link.first, purpose: :demo, serializer: Marshal)
# "1UTtkhu9BDywzz8yl8/7cBZnOoM1wnILDJbT7gP+zz8M/t1Dve4QTFQP5nfHZdYK9KvFDwkizm8DTHyNZdixDtCO/M7yNMlzL8Mry1RQ3AF0qhhTzFeqb5UqyQv/Cuq+NWvQ+GXv3gFckXaNqsFSX5yDccEpRDpyNkYT4MFxOa+8hVR4roebkNKB89lb73anBDTHsTAd37y2LFiv2YaiFguPQ/...
Avo::Services::EncryptionService.decrypt(message: secret_encryption, purpose: :demo, serializer: Marshal)
# #
```
## Secret key base
:::warning
[`Avo::Services::EncryptionService`](https://github.com/avo-hq/avo/blob/main/lib/avo/services/encryption_service.rb) fetches a secret key base to be used on the encrypt / decrypt process. Make sure that you have it defined in any of the following:
`ENV["SECRET_KEY_BASE"] || Rails.application.credentials.secret_key_base || Rails.application.secrets.secret_key_base`
:::
---
# Select All
The "Select All" feature is designed to enable users to select all queried records and perform actions on the entire selection. This feature is particularly useful when dealing with large datasets, allowing users to trigger actions on all queried records, not just the ones visible on the current page.
## How does it work?
When a user toggles the "Select all" checkbox, Avo will first check to see if there are more records than just those displayed on that page, and if there are, it will ask if the user if they want to select all the records or not.
This is being done through serializing the query to be unserialized back in the action.
## Serializing the query
The query might include various filters, sorting parameters, and other custom elements. Reconstructing this query at the time of the action request can be complex. Therefore, the system serializes the entire query object into a secure format before sending it with the action request.
- **Security**: To ensure that sensitive data is protected, the serialized query is encrypted before it is transmitted.
- **Efficiency**: This approach allows the system to accurately and efficiently reconstruct the original query when the action is executed, ensuring that all relevant records are included.
:::warning
If an error occurs during the serialization process, the "Select All" feature is automatically disabled. This safeguard ensures that the page will not crash because of a coding error.
We listed a few reasons on why it might crash below.
:::
## Serialization known issues
In this section, we outline common serialization problems and provide guidance on how to resolve them effectively.
##### `normalize`
If your model includes any `normalize` proc, such as:
```ruby
normalizes :status, with: ->(status) { status }
```
Serialization may fail when a filter is applied to the normalized attribute (e.g., `status` in this example). This can result in the error `TypeError: no _dump_data is defined for class Proc`, which causes the "Select All" feature to be automatically disabled.
For applications created before Rails `7.1`, configuring the `marshalling_format_version` to `7.1` or higher will resolve the issue:
```ruby
# config/application.rb
config.active_record.marshalling_format_version = 7.1
```
More details on [`normalizes` documentation](https://api.rubyonrails.org/classes/ActiveRecord/Normalization/ClassMethods.html#method-i-normalizes).
---
# Icons
Avo provides a collection of SVG icons organized into two directories: [`avo`](https://github.com/avo-hq/avo/tree/main/app/assets/svgs/avo) and [`heroicons`](https://github.com/avo-hq/avo/tree/main/app/assets/svgs/heroicons) ([check heroicons](https://heroicons.com/)). These icons are easily accessible using the [`svg` method](https://github.com/avo-hq/avo/blob/main/app/helpers/avo/application_helper.rb#L63).
To render an icon in your application, use the svg method. This method allows you to specify the icon's path and class.
Examples:
```ruby
# in a View Component
helpers.svg("avo/editor-strike")
# in a Rails helper
svg("heroicons/outline/magnifying-glass-circle", class: "block h-6 text-gray-600")
```
```erb
<%= svg 'avo/bell.svg', class: "h-4" %>
```
There are some places where Avo have custom DSL accepting the `icon` option. There you only need to specify the `icon`'s path (`avo/...` or `heroicons/...`). Behind the scenes Avo applies the [`svg` method](https://github.com/avo-hq/avo/blob/main/app/helpers/avo/application_helper.rb#L63).
## Avo icons
Avo uses a [set of custom icons](https://github.com/avo-hq/avo/tree/main/app/assets/svgs/avo) which you can use yourself with this notation: `avo/ICON_NAME`.
#### Example:
```erb
<%= svg "avo/bell.svg", class: "h-4" %>
```
## Using heroicons
Avo uses the delightful [`heroicons` library](https://heroicons.com/) which is kept up to date by the team.
Heroicons come in 4 variants `outline`, `solid`, `mini`, and `micro`.
You can use these icons with this notation: `heroicons/VARIANT/ICON_NAME`.
We usually use the `outline` variant.
#### Examples:
```erb
<%= svg "heroicons/outline/academic-cap.svg" %>
<%= svg "heroicons/mini/arrow-path-rounded-square.svg" %>
```
---
# Reserved model names and routes
When defining models in an Avo-powered application, certain names should be avoided as they are used by Avoβs internal controllers. Using these names may lead to conflicts, routing issues, or unexpected behavior.
## Model names to avoid
Avo uses the following names for its internal controllers:
- `action`
- `application`
- `association`
- `attachment`
- `base_application`
- `base`
- `chart`
- `debug`
- `home`
- `private`
- `resource`
- `search`
Using these names for models may override built-in functionality, cause routing mismatches, or introduce other conflicts.
## Why these names are reserved
Avo relies on these names for its controller and routing system. For example:
- `resource` is essential for managing Avo resources.
- `chart` is used for analytics and visualizations.
- `search` handles search functionality.
Since Avo dynamically maps models and controllers, using these names may interfere with how Avo processes requests and displays resources.
## Alternative approaches
If your application requires one of these names, consider the following alternatives:
- **Use a prefix or suffix**
- `user_resource` instead of `resource`
- `advanced_search` instead of `search`
- **Choose a synonym**
- `graph` instead of `chart`
### Using Avo with existing models
If your application already has models with these names, you can generate an Avo resource with a different name while keeping the same model class.
For example for `Resource` run the following command:
```sh
bin/rails generate avo:resource user_resource --model-class resource
```
This will generate:
- `Avo::Resources::UserResource`
- `Avo::UserResourcesController`
However, it will still use the existing `Resource` model, ensuring no conflicts arise.
## Route Conflicts with `resources :resources`
If your application has a route definition like:
```ruby
resources :resources
```
This will create path helpers such as `resources_path`, which **conflicts with [Avoβs internal routing helpers](https://github.com/avo-hq/avo/blob/main/app/helpers/avo/url_helpers.rb#L3)**. Avo uses `resources_path` internally, and having this route in your application **will override Avoβs default helpers**, potentially breaking parts of the admin panel.
### How to Fix It
To prevent conflicts, rename the route helpers to something more specific:
```ruby
resources :resources, as: 'articles'
```
This allows you to maintain the desired URL structure (`/resources`) without interfering with Avoβs internals.
---
# `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{6-8,11-13,18}
# app/controllers/concerns/multitenancy.rb
module Multitenancy
extend ActiveSupport::Concern
included do
before_action :multitenancy_detector
# or
prepend_before_action :multitenancy_detector
end
def multitenancy_detector
# your logic here
end
end
# configuration/initializers/avo.rb
Rails.configuration.to_prepare do
Avo::ApplicationController.include Multitenancy
end
```
With this technique, the `multitenancy_detector` method and its `before_action` will be included safely in `Avo::ApplicationController`.
:::info
If you'd like to add a `before_action` before all of Avo's before actions, use `prepend_before_action` instead. That will run that code first and enable you to set an account or do something early on.
:::
## Override `ApplicationController` methods
Sometimes you don't want to add methods but want to override the current ones.
For example, you might want to take control of the `Avo::ApplicationController.fill_record` method and add your own behavior.
TO do that you should change a few things in the approach we mentioned above. First we want to `prepend` the concern instead of `include` it and next, if we want to run a class method, we used `prepended` instead of `included`.
```ruby{5-8,10-12,14-17,23}
# app/controllers/concerns/application_controller_overrides.rb
module ApplicationControllerOverrides
extend ActiveSupport::Concern
# we use the `prepended` block instead of `included`
prepended do
before_action :some_hook
end
def some_hook
# your logic here
end
def fill_record
# do some logic here
super
end
end
# configuration/initializers/avo.rb
Rails.configuration.to_prepare do
# we will prepend instead of include
Avo::ApplicationController.prepend ApplicationControllerOverrides
end
```
**Related:**
- Multitenancy
---
# Asset manager
In your plugins or custom content you might want to add a new stylesheet or javascript file to be loaded inside Avo.
You can manually add them to the `_head.html.erb` or `_pre_head.html.erb` files or you can use the `AssetManager`.
Next, the asset manager will add them to the `` element of Avo's layout file.
## Add a stylesheet file
Use `Avo.asset_manager.add_stylesheet PATH`
Example:
```ruby
Avo.asset_manager.add_stylesheet "/public/magic_file.css"
Avo.asset_manager.add_stylesheet Avo::Engine.root.join("app", "assets", "stylesheets", "magic_file.css")
```
## Add a javascript file
Use `Avo.asset_manager.add_javascript PATH`
Example:
```ruby
Avo.asset_manager.add_javascript "/public/magic_file.js"
Avo.asset_manager.add_javascript Avo::Engine.root.join("app", "javascripts", "magic_file.js")
```
---
# Plugins
:::warning
This feature is in beta and we might change the API as we develop it.
These docs are in beta too, so please [ask for more information](https://github.com/avo-hq/avo/discussions) when you need it.
:::
## Overview
Plugins are a way to extend the functionality of Avo.
### Light layer
We are in the early days of the plugin system and we're still figuring out the best way to do it. This is why we have a light layer that you can use to extend the functionality of Avo.
This means we provide two hooks that you can use to extend the functionality of the Rails app, and a few Avo APIs to add scrips and stylesheets.
## Register the plugin
The way we do it is through an initializer. We mostly use the `engine.rb` file to register the plugin.
```ruby{8-15}
# lib/avo/feed_view/engine.rb
module Avo
module FeedView
class Engine < ::Rails::Engine
isolate_namespace Avo::FeedView
initializer "avo-feed-view.init" do
# Avo will run this hook on boot time
ActiveSupport.on_load(:avo_boot) do
# Register the plugin
Avo.plugin_manager.register :feed_view
# Register the mounting point
Avo.plugin_manager.mount_engine Avo::FeedView::Engine, at: "/feed_view"
end
end
end
end
end
```
This will add the plugin to a list of plugins which Avo will run the hooks on.
## Hook into Avo
```ruby
module Avo
module FeedView
class Engine < ::Rails::Engine
isolate_namespace Avo::FeedView
initializer "avo-feed-view.init" do
ActiveSupport.on_load(:avo_boot) do
Avo.plugin_manager.register :feed_view
# Add some concerns
Avo::Resources::Base.include Avo::FeedView::Concerns::FeedViewConcern
# Remove some concerns
Avo::Resources::Base.included_modules.delete(Avo::Concerns::SOME_CONCERN)
# Add asset files to be loaded by Avo
# These assets will be added to Avo's `application.html.erb` layout file
Avo.asset_manager.add_javascript "/avo-advanced-assets/avo_advanced"
Avo.asset_manager.add_stylesheet "/avo-kanban-assets/avo_kanban"
end
ActiveSupport.on_load(:avo_init) do
# Run some code on each request
Avo::FeedView::Current.something = VALUE
end
end
end
end
end
```
## Hooks
## Avo `AssetManager`
We use the `AssetManager` to add our own asset files (JavaScript and CSS) to be loaded by Avo. They will be added in the `` section of Avo's layout file.
It has two methods:
## Using a middleware to surface asset files
One tricky thing to do with Rails Engines is to expose some asset files to the parent Rails app.
The way we do it is by using a middleware that will serve the files from the Engine's `app/assets/builds` directory.
So `app/assets/builds/feed_view.js` from the `feed_view` engine will be served by the parent Rails app at `/feed-view-assets/feed_view.js` with the following middleware added to your `engine.rb` file.
```ruby
module Avo
module FeedView
class Engine < ::Rails::Engine
isolate_namespace Avo::FeedView
initializer "avo-feed-view.init" do
ActiveSupport.on_load(:avo_boot) do
Avo.plugin_manager.register :feed_view
end
end
config.app_middleware.use(
Rack::Static,
urls: ["/feed-view-assets"], # π This is the path where the files will be served
root: root.join("app", "assets", "builds") # π This is the path where the files are located
)
end
end
end
```
:::info
Avo doesn't compile the assets in any way, but just adds them to the layout file. This means that the assets should be compiled and ready for the browser to use them.
We use [`jsbundling-rails`](https://github.com/rails/jsbundling-rails) with `esbuild` to compile the assets before packaging them in the `gem` file.
Please check out [the scripts](https://github.com/avo-hq/avo/blob/main/package.json) we use.
:::
## Create your own plugin
We don't yet have a generator for that but what we do is to create a new Rails Engine and add the plugin to it.
1. Run `rails plugin new feed-view`
1. Add the plugin to the `engine.rb` file
1. Register the plugin to the `lib/avo/feed_view/engine.rb` file
1. Optionally add assets
1. Add the plugin to your app's `Gemfile` using the `path` option to test it out
---
# Guides
These are various guides on how to build some things with Avo or how to integrate with different pieces of tech.
Some guides have been written by us, and some by our community members.
# Videos
We regularly publish videos on our [YouTube channel](https://www.youtube.com/@avo_hq).
SupeRails featured Avo in a few of [their videos](https://superails.com/playlists/avo).
- [How to filter associations using dynamic filters](https://www.loom.com/share/d8bd49086d014d77a3013796c8480339)
---
# Act as taggable on integration
A popular way to implement the tags pattern is to use the [`acts-as-taggable-on`](https://github.com/mbleigh/acts-as-taggable-on) gem.
Avo already supports it in the `tags` field, but you might also want to browse the tags as resources.
[This template](https://railsbytes.com/templates/VRZskb) will add the necessarry resource and controller files to your app.
Run `rails app:template LOCATION='https://railsbytes.com/script/VRZskb'`
If you're using the menu editor don't forget to add the resources to your menus.
```ruby
resource :taggings
resource :tags
```
---
# Acts As Tenant Integration
Recipe [contributed](https://github.com/avo-hq/docs.avohq.io/pull/218) by [SahSantoshh](https://github.com/sahsantoshh).
:::warning
The guide expressed here shows how you we can add subdomain-level multitenancy (sah.example.org, adrian.example.org, etc).
This makes for more than one URL per application which in turn requires a special license.
To get more information please reach out to us.
:::
There are different ways to achieve multi-tenancy in an application.
We already have a doc which describes about Multitenancy with Avo.
Here we will deep dive in integrating [Acts As Tenant](https://github.com/ErwinM/acts_as_tenant) which supports row-level multitenancy with Avo.
In this implementation we will be setting tenant to subdomain.
:::info
Check out the [acts_as_tenant](https://github.com/ErwinM/acts_as_tenant) documentation for reference.
:::
## Installation
To use it, add it to your Gemfile:
```ruby
gem 'acts_as_tenant'
```
## Tenant
Let's create model for tenant. We are using `Account` as our tenant.
**Account Migration and Model class**
:::code-group
```ruby [db/migrate/random_number_create_accounts.rb]{3}
# Migration
class CreateAccounts < ActiveRecord::Migration[7.1]
def change
create_table :accounts do |t|
t.string :name
t.string :subdomain
t.timestamps
end
add_index :accounts, :subdomain, unique: true
add_index :accounts, :created_at
end
end
```
```ruby [app/models/account.rb]{3}
# Account model handles Tenant management
class Account < ApplicationRecord
MAX_SUBDOMAIN_LENGTH = 20
validates :name, :subdomain, presence: true
validates_uniqueness_of :name, :subdomain, case_sensitive: false
validates_length_of :subdomain, :name, maximum: MAX_SUBDOMAIN_LENGTH
end
```
:::
## Scope models
___
Now let's add users to `Account`. Here I am assuming to have an existing user model which is used for `Authentication`.
Similarly we can scope other models.
:::code-group
```ruby [db/migrate/random_number_add_account_to_users.rb]{3}
class AddAccountToUsers < ActiveRecord::Migration
def up
add_column :users, :account_id, :integer # if we have existing user set null to true then update the data using seed
add_index :users, :account_id
end
end
```
```ruby [app/models/account.rb]{3}
# Authentication
class User < ActiveRecord::Base
acts_as_tenant(:account)
end
```
:::
## Setting the current tenant
There are three ways to set the current tenant but we be using the subdomain to lookup the current tenant.
Since Avo has it's own `Application Controller` so there is no point in setting the tenant in Rails default `Application Controller` but we will set it there as well just to be safe site and also we might have some other pages other than Admin Dashboard supported by Avo.
:::code-group
```ruby [app/controllers/concerns/multitenancy.rb]{3}
# Multitenancy, to set the current account/tenant.
module Multitenancy
extend ActiveSupport::Concern
included do
prepend_before_action :set_current_account
end
def set_current_account
hosts = request.host.split('.')
# just to make sure we are using subdomain path
subdomain = (hosts[0] if hosts.length > 2)
# We only allow users to login from their account specific subdomain not outside of it.
sign_out(current_user) if subdomain.blank?
current_account = Account.find_by(subdomain:)
sign_out(current_user) if current_account.blank?
# set tenant for Avo and ActAsTenant
ActsAsTenant.current_tenant = current_account
Avo::Current.tenant = current_account
Avo::Current.tenant_id = current_account.id
end
end
```
```ruby [config/initializers/avo.rb]{3}
Avo.configure do |config|
# configuration values
end
Rails.configuration.to_prepare do
Avo::ApplicationController.include Multitenancy
end
```
:::
Now, whenever we navigate to https://sahsantoshh.example.com/ the tenant & the tenant_id will be set to **sahsantoshh**.
## Move existing data to model
We might have to many users and other records which needs to be associated with `Account`.
For example, we will only move users record to the account
:::code-group
```ruby [db/seeds.rb]{3}
# Create default/first account where we want to associate exiting data
account = Account.find_or_create_by!(name: 'Nepal', subdomain: 'sahsantoshh')
User.unscoped.in_batches do |relation|
relation.update_all(account_id: account.id)
sleep(0.01) # throttle
end
```
:::
Now run the seed command to update existing records
---
# Add nested fields to CRUD forms
Please follow this guide to learn how to implement nested fields on Avo forms.
---
# Use Avo in an `api_only` Rails app
**After Avo version 2.9 π**
The `api_mode` might not be supported. The reason for that is that Rails does not generate some paths for the [`resource` route helper](https://guides.rubyonrails.org/routing.html#resource-routing-the-rails-default). Most important being the `new` and `edit` paths. That's because APIs don't have the `new` path (they have the `create` path).
But you're probably safer using Rails with `api_only` disabled (`config.api_only = false`).
**Pre Avo version 2.9 π**
You might have an api-only Rails app where you'd like to use Avo. In my early explorations I found that it needs the `::ActionDispatch::Flash` middleware for it to properly work.
So, add it in your `application.rb` file.
```ruby{18}
require_relative "boot"
require "rails/all"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module RailApi
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.0
# Only loads a smaller set of middleware suitable for API only apps.
# Middleware like session, flash, cookies can be added back manually.
# Skip views, helpers and assets when generating a new resource.
config.api_only = true
config.middleware.use ::ActionDispatch::Flash
end
end
```
---
# Attachment Policy Extension for Pundit
When using Pundit, it's common to define permissions for each attachment action (e.g., upload, delete, download) individually. This can lead to repetitive code like:
```ruby
def upload_logo?
update?
end
def delete_logo?
update?
end
def download_logo?
update?
end
```
To streamline this process, you can extend your `ApplicationPolicy` with a helper method that dynamically handles attachment permissions.
## Step 1: Add the `method_missing` Helper to `ApplicationPolicy`
This method intercepts calls to undefined policy methods that follow a specific pattern and delegates them to a predefined permission mapping:
```ruby
def method_missing(method_name, *args)
if method_name.to_s =~ /^(upload|delete|download)_(.+)\?$/
action = Regexp.last_match(1).to_sym
attachment = Regexp.last_match(2).to_sym
return attachment_concerns[attachment][action] if attachment_concerns.key?(attachment) &&
attachment_concerns[attachment].key?(action)
end
super
end
```
## Step 2: Define `attachment_concerns` in Your Policy
In each model-specific policy, define the permitted actions for each attachment:
```ruby
def attachment_concerns
{
logo: {
upload: update?,
delete: update?,
download: update?
}
}
end
```
With this setup, calls to `upload_logo?`, `delete_logo?`, or `download_logo?` will be automatically resolved based on the configuration in `attachment_concerns`, reducing boilerplate and improving maintainability.
---
# Add Avo behind Basic Authentication
Because in Rails we commonly do that using a static function on the controller we need to [safely extend the controller](https://avohq.io/blog/safely-extend-a-ruby-on-rails-controller) to contain that function.
In actuality we will end up with something that behaves like this:
```ruby{2}
class Avo::ApplicationController < ::ActionController::Base
http_basic_authenticate_with name: "adrian", password: "password"
# More methods here
end
```
## Safely add it to Avo
We described the process in depth in [this article](https://avohq.io/blog/safely-extend-a-ruby-on-rails-controller) so let's get down to business.
1. Add the `BasicAuth` concern
1. The concern will prepend the basic auth method
1. `include` that concern to Avo's `ApplicationController`
:::warning
Ensure you restart the server after you extend the controller in this way.
:::
```ruby{8,20}
# app/controllers/concerns/basic_auth.rb
module BasicAuth
extend ActiveSupport::Concern
# Authentication strategy came from this article:
# https://dev.to/kevinluo201/setup-a-basic-authentication-in-rails-with-http-authentication-388e
included do
http_basic_authenticate_with name: "adrian", password: "password"
end
end
# config/initializers/avo.rb
Avo.configure do |config|
# Avo configuration
end
# Add this to include it in Avo's ApplicationController
Rails.configuration.to_prepare do
# Add basic authentication to Avo
Avo::ApplicationController.include BasicAuth
end
```
---
# Bulk destroy action using customizable controls
In this guide, we'll explore how to implement a customizable bulk destroy action in Avo. This allows to delete multiple records at once while providing users with a clear, informative interface that shows exactly what will be deleted. The implementation includes a confirmation message with a scrollable list of records to be deleted and clear warning messages about the permanent nature of this action.
The bulk destroy action is particularly useful when you need to:
- Delete multiple records simultaneously
- Show users exactly which records will be affected
- Provide clear warnings about the irreversible nature of the action
- Handle the deletion process with proper error handling
## Bulk destroy action
```ruby
# app/avo/actions/bulk_destroy.rb
class Avo::Actions::BulkDestroy < Avo::BaseActionAdd commentMore actions
self.name = "Bulk Destroy"
self.message = -> {
tag.div do
safe_join([
"Are you sure you want to delete these #{query.count} records?",
tag.div(class: "text-sm text-gray-500 mt-2 mb-2 font-bold") do
"These records will be permanently deleted:"
end,
tag.ul(class: "ml-4 overflow-y-scroll max-h-64") do
safe_join(query.map do |record|
tag.li(class: "text-sm text-gray-500") do
"- #{::Avo.resource_manager.get_resource_by_model_class(record.class).new(record:).record_title}"
end
end)
end,
tag.div(class: "text-sm text-red-500 mt-2 font-bold") do
"This action cannot be undone."
end
])
end
}
def handle(query:, **)
query.each(&:destroy!)
succeed "Deleted #{query.count} records"
rescue => e
fail "Failed to delete #{query.count} records: #{e.message}"
end
end
```
## Register it on all resources with except list
Once you've defined your bulk destroy action, you might want to make it available across multiple resources while excluding specific ones. This approach allows you to implement the action globally while maintaining control over where it can be used. The following configuration adds the bulk destroy functionality to your base resource class with a customized appearance and selective implementation.
Related docs:
- [Extending Avo::BaseResource](https://docs.avohq.io/3.0/resources.html#extending-avo-baseresource)
- [Customizable controls](https://docs.avohq.io/3.0/customizable-controls.html)
```ruby
# app/avo/base_resource.rb
class Avo::BaseResource < Avo::Resources::Base
self.index_controls = -> {
# Don't show bulk destroy for these resources
return default_controls if resource.class.in?([
Avo::Resources::User,
Avo::Resources::Post,
Avo::Resources::Product,
Avo::Resources::Person,
Avo::Resources::Spouse,
Avo::Resources::Movie,
Avo::Resources::Fish,
])
bulk_title = tag.span(class: "text-xs") do
safe_join([
"Delete all selected #{resource.plural_name.downcase}",
tag.br,
"Select at least one #{resource.singular_name.downcase} to run this action"
])
end.html_safe
action Avo::Actions::BulkDestroy,
icon: "heroicons/solid/trash",
color: "red",
label: "",
style: :outline,
title: bulk_title
default_controls
}
end
```
## Feedback
We value your experience with this bulk destroy implementation! Whether you've successfully implemented it, made improvements, or encountered challenges, your feedback helps the community. Here's how you can contribute:
- **Success stories**: Share how you've implemented this in your project and what benefits it brought to your workflow
- **Improvements**: Tell us if you've enhanced this implementation with additional features or better error handling
- **Questions**: Ask about specific use cases or implementation details you're unsure about
- **Troubleshooting**: If you're experiencing issues, describe your setup and the problems you're encountering
- **Customizations**: Share how you've adapted this to better suit your specific needs
- **Alternative approaches**: If you've implemented bulk destroy differently, we'd love to hear about your solution
You can share your feedback through [Feedback: Bulk destroy action using customizable controls](https://github.com/avo-hq/avo/discussions/3930).
---
# Conditionally render styled rows
We've had [a request](https://discord.com/channels/740892036978442260/1197693313520771113) come in from a customer to style their soft-deleted records differently than the regular ones.
Their first idea was to add a new option to Avo to enable that. They even tried to monkey-patch our code to achieve that.
It's a "fair" strategy; we're not judging.
Our impression was to add a new option, too, but in the end, we found a better solution. Something that doesn't involve monkey-patching or us adding new code to the framework.
New code that we should maintain in the future and bring on more and more requests.
## Solution
The solution came to me a little while after the request came over, and it's so simple!
**Use the `has` CSS selector.**
#### 1. Attach a CSS class to the `id` field of the records you want to mark
```ruby
def fields
field :id, as: :id, html: -> {
index do
wrapper do
classes do
# We'll mark every record that has an even `id`
if record.id % 2 == 0
"soft-deleted"
end
end
end
end
}
end
```
#### 2. Target the row that has that child element and style it as you need it
```css
tr[data-component-name="avo/index/table_row_component"]:has(.soft-deleted){
background: #fef2f2;
}
/* you may even target a specific resource by it's name */
tr[data-component-name="avo/index/table_row_component"][data-resource-name="course_links"]:has(.soft-deleted){
background: #fef2f2;
}
```
Of course, I chose a trivial rule like the records with an even `id` column, but you can tweak that rule as needed.
I think there's a lesson or two to be learned from this, which I wrote about in [this article](https://avohq.io/blog/state-the-problem-not-the-solution).
---
# How to Use Custom IDs with Avo
Avo seamlessly integrates custom IDs, including popular solutions like FriendlyID, prefixed IDs, or Hashids. Below, you'll find examples illustrating each approach for effortless customization within your application.
## Example with FriendlyID
FriendlyID is a gem that allows you to generate pretty URLs and unique IDs. To integrate FriendlyID with Avo, follow these steps:
**Install [friendly_id](https://github.com/norman/friendly_id) gem by adding this line to your application's Gemfile:**
```ruby
gem "friendly_id", "~> 5.5.0"
```
And then execute:
```bash
bundle install
```
**Generate and run the migration to add a slug column to your model:**
```bash
rails generate friendly_id
rails db:migrate
```
**Add `friendly_id` to your model:**
```ruby{3,6}
# app/models/post.rb
class Post < ApplicationRecord
extend FriendlyId
# This post model have a name column
friendly_id :name, use: :finders
end
```
With this setup, you can use `Post.find("bar")` to find records by their custom IDs.
:::info
For a version of [friendly_id](https://github.com/norman/friendly_id) smaller then 5.0 you can use custom query scopes
:::
View [friendly_id](https://github.com/norman/friendly_id) setup in action: [View Demo](https://main.avodemo.com/avo/resources/users)
Check out the code: [Code on GitHub](https://github.com/avo-hq/main.avodemo.com/blob/main/app/models/user.rb)
## Example with Prefixed IDs
Prefixed IDs involve adding a custom prefix to your IDs.
**Install [prefixed_ids](https://github.com/excid3/prefixed_ids) gem by adding this line to your application's Gemfile:**
```ruby
gem "prefixed_ids"
```
And then execute:
```bash
bundle install
```
**Basic Usage**
Add `has_prefix_id :my_prefix` to your models to autogenerate prefixed IDs:
```ruby{3}
# app/models/post.rb
class Post < ApplicationRecord
has_prefix_id :post
end
```
View [prefixed_ids](https://github.com/excid3/prefixed_ids) setup in action: [View Demo](https://main.avodemo.com/avo/resources/teams)
Check out the code: [Code on GitHub](https://github.com/avo-hq/main.avodemo.com/blob/main/app/models/team.rb)
## Example with Hashids
Hashid Rials is a gem that generates short, unique, and cryptographically secure IDs.
**Install [hashid-rails](https://github.com/jcypret/hashid-rails) gem by adding this line to your application's Gemfile:**
```ruby
gem "hashid-rails", "~> 1.0"
```
And then execute:
```bash
bundle install
```
**Include Hashid Rails in the ActiveRecord model you'd like to enable hashids:**
```ruby{3}
# app/models/post.rb
class Post < ApplicationRecord
include Hashid::Rails
end
```
View [hashid-rails](https://github.com/jcypret/hashid-rails) setup in action: [View Demo](https://main.avodemo.com/avo/resources/spouses)
Check out the code: [Code on GitHub](https://github.com/avo-hq/main.avodemo.com/blob/main/app/models/spouse.rb)
---
# Custom link field
When you want to add a custom link as a field on your resource that points to a related resource (and you don't want to use one of the available association fields) you can use the `Text` field like so.
```ruby
# with the format_using option
field :partner_home, as: :text, format_using: -> { link_to(value, value, target: "_blank") } do
avo.resources_partner_url record.partner.id
end
# with the as_html option
field :partner_home, as: :text, as_html: true do
if record.partner.present?
link_to record.partner.first_name, avo.resources_partner_url(record.partner.id)
end
end
```
---
# Display and Edit Join Table Fields in `has_many :through` Associations
A common scenario in Rails is using a `has_many :through` association to connect two models via a join model that contains extra fields. In Avo, you might want to display and edit attributes from the join table directly in your resource views (index, show, edit). This guide demonstrates how to achieve that.
## Example Models
```ruby
class Store < ApplicationRecord
has_one :location
has_many :patronships, class_name: :StorePatron
has_many :patrons, through: :patronships, class_name: :User, source: :user
end
class User < ApplicationRecord
has_many :patronships, class_name: :StorePatron
has_many :stores, through: :patronships
# Needed to make the field editable in Avo
attr_accessor :review
end
# Join Table
class StorePatron < ApplicationRecord
belongs_to :store
belongs_to :user
validates :review, presence: true
end
```
## Displaying Join Table Fields
You can display a join table attribute (like `review`) on the index or show view of the related resource by adding the field in your resource file and using `format_using` to fetch the correct value from the join table.
```ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
field :review,
format_using: -> {
# Fetch the review from the StorePatron join table
record.patronships.find_by(store_id: params[:via_record_id])&.review
}
end
end
```
This will show the `review` field from the join table when viewing users from the context of a store.
## Editing Join Table Fields
To allow editing, you need to:
1. Add a writer for the field to the model (e.g., `attr_accessor :review` or a custom setter).
2. Use the `update_using` option to update the join record.
```ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
if params[:resource_name] == 'stores' || params[:via_resource_class] == 'Avo::Resources::Store'
field :review,
update_using: -> {
# Update the review in the StorePatron join table
patronship = record.patronships.find_by(user_id: record.id.to_i)
patronship.update(review: value)
},
format_using: -> {
record.patronships.find_by(user_id: record.id.to_i)&.review
}
end
end
end
```
**Note:**
- The field will only render on the form if the model has a writer for it.
- You may need to adjust the logic for finding the join record depending on your association direction.
## Conditional Display Based on Parent Resource
You can use the `params` to control when the field is shown or editable. For example:
```ruby
# We use different params to detect the navigation context:
# - `resource_name` identifies when users access through the index table
# - `via_resource_class` identifies when users click to view or edit the resource
if params[:resource_name] == 'stores' || params[:via_resource_class] == 'Avo::Resources::Store'
# field
end
```
In this example, the `review` field is only visible/editable on User when the resource is accessed from the `Store` resource.
```ruby
# app/avo/resources/store.rb
class Avo::Resources::Store < Avo::BaseResource
def fields
field :patrons,
as: :has_many,
through: :patronships,
translation_key: "patrons",
attach_fields: -> {
# Add the review field to the attach form
field :review, as: :text
}
end
end
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
# Only show when accessed from the Store resource
if params[:resource_name] == 'stores' || params[:via_resource_class] == 'Avo::Resources::Store'
field :review,
format_using: -> {
# Fetch the review from the StorePatron join table
record.patronships.find_by(user_id: record.id.to_i)&.review
}
end
end
end
```
## Gotchas & Tips
- Computed fields (using a block) do not render on forms. Use `format_using` and provide a writer on the model.
- Avo checks for a writer method to decide if a field is editable.
- If the form fails to save, your join field may revert to its original value β consider validations and persistence carefully.
---
# Display scope record count
The `name` and `description` scope options can be callable values and receive the `resource`, `scope` and `query` objects.
The `query` object is the actual Active Record query (unscoped) that is made to fetch the records.
There is also possible to access the `scoped_query` method that will return the `query` after applying the `scope`.
You may use that to display a counter of how many records are there in that scope. Notice that it can impact page loading time when applying on large data tables.
### Example
```ruby{2-9}
class Avo::Scopes::Scheduled < Avo::Advanced::Scopes::BaseScope
self.name = -> {
sanitize(
"Scheduled " \
"" \
"#{scoped_query.count}" \
""
)
}
self.description = -> { "All the scheduled jobs." }
self.scope = -> { query.finished.invert_where }
self.visible = -> { true }
end
```
In this example we made the `name` option a callable block and are returning the name of the scope and a `span` with the count of the records.
We are also using the `sanitize` method to return it as HTML.
In order to make the counter stand out, we're using some Tailwind CSS classes that we have available in Avo. If you're trying different classes and they are not applying, you should consider adding the Tailwind CSS integration.
:::warning
This approach will have some performance implications as it will run the `count` query on every page load.
:::
---
# Export to CSV action
Even if we don't have a dedicated export to CSV feature, you may create an action that will take all the selected records and export a CSV file for you.
Below you have an example which you can take and customize to your liking. It even give you the ability to use custom user-selected attributes.
```ruby
# app/avo/actions/export_csv.rb
class Avo::Actions::ExportCsv < Avo::BaseAction
self.name = "Export CSV"
self.no_confirmation = false
self.standalone = true
def fields
# Add more fields here for custom user-selected columns
field :id, as: :boolean
field :created_at, as: :boolean
end
def handle(records:, fields:, resource:, **args)
# uncomment if you want to download all the records if none was selected
# records = resource.model_class.all if records.blank?
return error "No record selected" if records.blank?
# uncomment to get all the models' attributes.
# attributes = get_attributes_from_record records.first
# uncomment to get some attributes
# attributes = get_some_attributes
attributes = get_attributes_from_fields fields
# uncomment to get all the models' attributes if none were selected
# attributes = get_attributes_from_record records.first if attributes.blank?
file = CSV.generate(headers: true) do |csv|
csv << attributes
records.each do |record|
csv << attributes.map do |attr|
record.send(attr)
end
end
end
download file, "#{resource.plural_name}.csv"
end
def get_attributes_from_record(record)
record.class.columns_hash.keys
end
def get_attributes_from_fields(fields)
fields.select { |key, value| value }.keys
end
def get_some_attributes
["id", "created_at"]
end
end
```
---
# Pretty JSON objects to the code field
It's common to have JSON objects stored in your database. So you might want to display them nicely on your resource page.
```ruby
field :meta, as: :code, language: 'javascript'
```
But that will be hard to read on one line like that. So we need to format it.
Luckily we can use `JSON.pretty_generate` for that and a computed field.
```ruby{3}
field :meta, as: :code, language: 'javascript' do
if record.meta.present?
JSON.pretty_generate(record.meta.as_json)
end
end
```
That's better! You'll notice that the field is missing on the `Edit` view. That's normal for a computed field to be hidden on `Edit`.
To fix that, we should add another one just for editing.
```ruby{1}
field :meta, as: :code, language: 'javascript', only_on: :edit
field :meta, as: :code, language: 'javascript' do
if record.meta.present?
JSON.pretty_generate(record.meta.as_json)
end
end
```
Now you have a beautifully formatted JSON object in a code editor.
## When you have more JSON fields
We can use a DRY solution that will help us to make our code cleaner and readable.
### 1. Concern
We will create a new concern in `app/models/concerns/avo_json_fields.rb` to be used in our models.
```ruby
module AvoJsonFields
extend ActiveSupport::Concern
class_methods do
def avo_json_fields(*fields)
fields.each do |field|
define_method "#{field}_json" do
JSON.pretty_generate(send(field).as_json)
end
define_method "#{field}_json=" do |value|
begin
send("#{field}=", JSON.parse(value))
rescue JSON::ParserError => e
# handle or ignore it
end
end
end
end
end
end
```
The `AvoJsonFields` prepares two methods for each field we provide. The first is for displaying, and the second is for storing the JSON object.
We can use it only on the models we need or include it in the `ApplicationRecord` for all.
```ruby{4}
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
include AvoJsonFields
end
```
### 2. Usage in models
When we have the concern in place, we can use it. For the example above, it could look like this:
```ruby{2}
class Page < ApplicationRecord
avo_json_fields :meta
end
```
That will create two methods for the `meta` field: `meta_json` and `meta_json=(value)`.
### 3. Usage in Avo resources
Now, we can use the `meta_json` field in our Avo resources. With the `name` option, we set the original name back.
```ruby
field :meta_json, as: :code, name: :meta, only_on: %i[show new edit], language: "javascript"
```
---
# Generating a custom component for a field
Each field in Avo has a component for each view that is responsible for rendering the field in that view.
Some fields, like the `textarea` field, don't have a component for certain views by default. For example, the `textarea` field doesn't have a component for the Index view. This guide shows you how to create one by using an existing field's component as a starting point.
## Using the Text field as a base
Instead of starting from scratch, it's easier to use the `text` field's index component as a base since it handles text content display well.
### Step 1: Eject the Text field component
In this step we're using the eject feature to generate the component files for the `text` field.
Run the following command to eject the text field's index component:
```bash
rails g avo:eject --field-components text --view index
```
This will generate the component files in your application.
### Step 2: Rename the component directory
Rename the generated directory from `text_field` to `textarea_field` to match the field type you're creating the component for.
```bash
mv app/components/avo/fields/text_field/ app/components/avo/fields/textarea_field/
```
### Step 3: Update the class reference
In the generated component file, update the class reference from:
```ruby
Avo::Fields::TextField::IndexComponent
```
to:
```ruby
Avo::Fields::TextareaField::IndexComponent
```
### Step 4: Customize the ERB template
Replace the ERB content with something appropriate for textarea content. For example, to truncate long text:
```erb
<%= index_field_wrapper(**field_wrapper_args) do %>
<%= @field.value.truncate(60) %>
<% end %>
```
### Step 5: Enable index visibility
By default, `textarea` fields are hidden on the index view. You need to explicitly show them by adding the `show_on: :index` option to your textarea fields:
```ruby
field :body, as: :textarea, show_on: :index
```
## Global configuration for all textarea fields
If you want all `textarea` fields in your application to show on the index view by default, you can extend the base resource and override the `field` method:
```ruby
# app/avo/base_resource.rb
module Avo
class BaseResource < Avo::Resources::Base
def field(id, **args, &block)
if args[:as] == :textarea
args[:show_on] = :index
end
super(id, **args, &block)
end
end
end
```
For more information about extending the base resource, see the Extending Avo::BaseResource documentation.
## Related documentation
- Field components - Learn how to eject and override existing field components
- Ejecting views - Learn how to eject and override existing views
- Extending Avo::BaseResource
- Views - Understanding different view types in Avo
---
# Hide field labels
One common use case for the `file`, `files`, and `external_image` fields is to display the logo for a record. You might want to do that but in a more "un-fieldy" way, so it doesn't look like a field with a label on top.
You can hide that label using CSS in your custom asset pipeline, or in a `_footer` partial.
Avo is littered with great `data` selectors so you can pick and choose any element you'd like. If it doesn't have it, we'll add it.
Here's an example on how to remove the label on an `external_image` field for the `Team` resource (try it [here](https://main.avodemo.com/avo/resources/teams/4)).
```css
[data-resource-name="TeamResource"] [data-field-type="external_image"][data-field-id="logo"] [data-slot="label"]{
display: none;
}
```
---
# How to Use [Phlex](https://www.phlex.fun/) Components in Avo
Avo uses [ViewComponent](https://viewcomponent.org/) to render fields, resources, and other parts of the UI. However, that doesn't mean you can't use [Phlex](https://www.phlex.fun/) components in your Avo views.
The initialization process between the two is quite similar allowing a smooth transition between them. You just need to configure the component you want to use for a given field or resource on a specific view, and Avo will take care of the rest.
This guide walks you through how to use [Phlex](https://www.phlex.fun/) components inside your Avo views.
> _Note: This guide assumes you already have Phlex installed in your app._
## Step 1: Create a [Phlex](https://www.phlex.fun/) component
Let's start with a simple [Phlex](https://www.phlex.fun/) component for a field. This component uses the same Tailwind CSS classes as the default Avo field component, and includes an additional message about the field.
You can make your components as simple or as complex as you'd like, this is just an example.
:::warning
All Tailwind CSS classes used in this guide are already included in Avo's design system and available in its pre-purged assets. If you plan to customize the appearance beyond what's shown here, consider setting up the TailwindCSS integration.
:::
```ruby
# app/components/phlex_component.rb
class PhlexComponent < Phlex::HTML
def initialize(field:, **)
@field = field
end
def view_template
div class: "flex items-center px-6 py-4" do
span class: "font-semibold text-gray-500 text-sm w-64 uppercase" do
@field.name
end
span class: "text-gray-900" do
@field.value
end
span class: "text-gray-300 mx-3" do
"|"
end
span class: "mr-1" do
"βΉοΈ"
end
span class: "text-sm text-gray-500 italic" do
"This is a unique course link. Share it with enrolled users."
end
end
end
end
```
---
## Step 2: Use the component in your field declaration
With the `components` option, you can specify the component to be used for the `show` view of a field.
```ruby{10-12}
# app/avo/resources/course_link.rb
class Avo::Resources::CourseLink < Avo::BaseResource
self.title = :link
self.model_class = ::Course::Link
def fields
field :id, as: :id
field :link,
as: :text,
components: {
show_component: PhlexComponent
}
field :course, as: :belongs_to, searchable: true
end
end
```
:::tip
While this example uses a field, the same pattern applies to resources. You can use the `components` option to customize the component for the `index`, `show`, `edit`, and `new` views.
:::
## Conclusion
Even though Avo relies on [ViewComponent](https://viewcomponent.org/) under the hood, you're free to use [Phlex](https://www.phlex.fun/) components in your Avo views.
This guide covered a basic example, but [Phlex](https://www.phlex.fun/) is capable of much more. Check out the [official Phlex documentation](https://www.phlex.fun/introduction/) to learn how to build more advanced components.
If you have questions, suggestions, or feedback, join the conversation in the [Feedback: Using Phlex Components in Avo Views](https://github.com/avo-hq/avo/discussions/3860).
---
# Manage information-heavy resources
This has been sent in by our friends at [Wyndy.com](https://wyndy.com). I'm just going to paste David's message because it says it all.
David π
Hey y'all - we've got a very information heavy app where there are pretty distinct differences between the data we display on index, show, & form views as well as how it's ordered.
We created a concern for our resources to make organizing this a bit easier, would love y'all's thoughts/feedback as to whether this could be a valuable feature! Example gist: [https://gist.github.com/davidlormor/d1d7e32a3568f6a9b3540669e7f601dc](https://gist.github.com/davidlormor/d1d7e32a3568f6a9b3540669e7f601dc)
We went with a concern because I ran into inheritance issues trying to create a `BaseResource` class (issues with Avo's `model_class` expectations) and monkey-patching `Avo::BaseResource` seemed to cause issues with Rails' autoloading/zeitwork?
```ruby
class ExampleResource < Avo::BaseResource
include ResourceExtensions
field :id, as: :id
field :name, as: :text
index do
field :some_field, as: :text
field :some_index_field, as: :text, sortable: true
end
show do
field :some_show_field, as: :markdown
field :some_field, as: :text
end
create do
field :some_create_field, as: :number
end
edit do
field :some_create_field, as: :number, readonly: true
field :some_field
field :some_editable_field, as: :text
end
end
```
```ruby
require "active_support/concern"
module ResourceExtensions
extend ActiveSupport::Concern
class_methods do
def index(&block)
with_options only_on: :index, &block
end
def show(&block)
with_options only_on: :show, &block
end
def create(&block)
with_options only_on: :new, &block
end
def edit(&block)
with_options only_on: :edit, &block
end
end
end
```
---
# Multi-language URLs
Implementing multi-language URLs is a common use-case. Using a route scope block in Avo allows you to seamlessly adapt your application to support multiple languages, enhancing the user experience. This recipe will guide you through the steps to configure a locale scope, ensuring your application dynamically sets and respects the user's preferred language. Let's dive in!
## 1. Mount Avo within a `:locale` scope
Using a locale scope is an effective way to set the locale for your users.
```ruby{3-5}
# config/routes.rb
Rails.application.routes.draw do
scope ":locale" do
mount_avo
end
end
```
## 2. Apply the `locale` Scope
To properly handle localization within Avo, you'll need to ensure the `locale` parameter is respected throughout the request which we'll do by overriding the `set_avo_locale` method in your `Avo::ApplicationController` as follows:
:::info
If you don't have the `app/controllers/avo/application_controller.rb` file present in your app, you can eject it using this command:
```bash
rails generate avo:eject --controller application_controller
```
:::
```ruby{4-6}
# app/controllers/avo/application_controller.rb
module Avo
class ApplicationController < BaseApplicationController
def set_avo_locale(&action)
I18n.with_locale(params[:locale], &action)
end
end
end
```
This implementation uses `I18n.with_locale` to set the desired locale for the duration of the request, ensuring consistent localization behavior across Avo's interface and that it won't impact the other non-Avo parts of your app.
---
# Multilingual content
This is not an official feature yet, but until we add it with all the bells and whistles, you can use this guide to monkey-patch it into your app.
We pushed some code to take in the `set_locale` param and set the `I18n.locale` and `I18n.default_locale` so all subsequent requests will use that locale. **That will change the locale for your whole app. Even to the front office**.
If you don't want to change the locale for the whole app, you can use `force_locale`, which will change the locale for that request only. It will also append `force_locale` to all your links going forward.
```ruby
def set_default_locale
I18n.locale = params[:set_locale] || I18n.default_locale
I18n.default_locale = I18n.locale
end
# Temporary set the locale
def set_force_locale
if params[:force_locale].present?
initial_locale = I18n.locale.to_s.dup
I18n.locale = params[:force_locale]
yield
I18n.locale = initial_locale
else
yield
end
end
```
## Install the mobility gem
Follow the install instructions [here](https://github.com/shioyama/mobility#installation). A brief introduction below (but follow their guide for best results)
- add the gem to your `Gemfile` `gem 'mobility', '~> 1.2.5'`
- `bundle install`
- install mobility `rails generate mobility:install`
- update the backend (like in the guide) `backend :key_value, type: :string`
- add mobility to your model `extend Mobility`
- add translatable field `translates :name`
- π that's it. The content should be translatable now.
## Add the language switcher
**Before v 2.3.0**
First, you need to eject the `_profile_dropdown` partial using this command `bin/rails generate avo:eject :profile_dropdown`. In that partial, add the languages you need to support like so:
```erb
<% destroy_user_session_path = "destroy_#{Avo.configuration.current_user_resource_name}_session_path".to_sym %>
```
Feel free to customize the dropdown as much as you need it to and add as many locales as you need.
**After v2.3.0**
Use the `profile_menu` to add the language-switching links.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.profile_menu = -> {
link "Switch to Portuguese", path: "?set_locale=pt-BR"
link "Switch to English", path: "?set_locale=en"
}
end
```
**After v2.10**
The `set_locale` param will change the locale for the entire website (for you and your customers). If you need to change it just for the current visit, use `force_locale`. That will switch the locale for that request only, not for your customers. It will also add the `force_locale` param to each link as we advance, making it easy to update all your multilingual content.
**After v2.11**
A change was pushed to consider the `locale` from the initializer. That will change the locale for Avo requests.
```ruby{2}
Avo.configure do |config|
config.locale = :en # default is nil
end
```
## Workflow
You will now be able to edit the attributes you marked as translatable (eg: `name`) in the locale you are in (default is `en`). Next, you can go to the navbar on the top and switch to a new locale. The switch will then allow you to edit the record in that locale and so on.
## Support
This is the first iteration of multilingual content. It's obvious that this could be done in a better way, and we'll add that better way in the future, but until then, you can use this method to edit your multilingual content.
Thanks!
---
# Use route-level multitenancy
:::tip
We published a new multitenancy guide.
:::
Multitenancy is not a far-fetched concept, and you might need it when you reach a certain level with your app. Avo is ready to handle that.
This guide will show you **one way** of achieving that, but if can be changed if you have different needs.
## Prepare the Current model
We will use Rails' [`Current`](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html) model to hold the account.
```ruby{3}
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :account
end
```
## Add middleware to catch the account param
We're trying to fetch the account number from the `params` and see if we have an account with that ID in this middleware. If so, store it in the `Current.account` model, where we can use it throughout the app.
```ruby{18,21,23,25}
## Multitenant Account Middleware
#
# Included in the Rails engine if enabled.
#
# Used for setting the Account by the first ID in the URL like Basecamp 3.
# This means we don't have to include the Account ID in every URL helper.
# From JumpstartRails AccountMiddleware
class AccountMiddleware
def initialize(app)
@app = app
end
# http://example.com/12345/projects
def call(env)
request = ActionDispatch::Request.new env
# Fetch the account id from the path
_, account_id, request_path = request.path.split("/", 3)
# Check if the id is a number
if /\d+/.match?(account_id)
# See if that account is present in the database.
if (account = Account.find_by(id: account_id))
# If the account is present, set the Current.account to that
Current.account = account
else
# If not, redirect to the root path
return [302, {"Location" => "/"}, []]
end
request.script_name = "/#{account_id}"
request.path_info = "/#{request_path}"
end
@app.call(request.env)
end
end
```
## Update the custom tools routes
By default, when generating custom tools, we're adding them to the parent app's routes. Because we're declaring them there, the link helpers don't hold the account id in the params.
```ruby{2-4}
Rails.application.routes.draw do
scope :avo do
get "custom_page", to: "avo/tools#custom_page"
end
devise_for :users
# Your routes
authenticate :user, -> user { user.admin? } do
mount_avo
end
end
```
To fix that, we need to move them as if they were added to Avo's routes.
```ruby{13-18}
# config/routes.rb
Rails.application.routes.draw do
devise_for :users
# Your routes
authenticate :user, -> user { user.admin? } do
mount_avo
end
end
# Move Avo custom tools routes to Avo engine
if defined? ::Avo
Avo::Engine.routes.draw do
# make sure you don't add the `avo/` prefix to the controller below
get 'custom_page', to: "tools#custom_page"
end
end
```
```ruby
# app/controllers/avo/tools_controller.rb
class Avo::ToolsController < Avo::ApplicationController
def custom_page
@page_title = "Your custom page"
add_breadcrumb "Your custom page"
end
end
```
## Retrieve and use the account
Throughout your app you can use `Current.account` or if you add it to Avo's `context` object and use it from there.
```ruby{8}
# config/initializers/avo.rb
Avo.configure do |config|
config.set_context do
{
foo: 'bar',
user: current_user,
params: request.params,
account: Current.account
}
end
end
```
Check out [this PR](https://github.com/avo-hq/avodemo/pull/4) for how to update an app to support multitenancy.
---
# Nested records when creating
A lot of you asked for the ability to create nested `has_many` records on the view. Although it's fairly "easy" to implement using `accepts_nested_attributes_for` for simple cases, it's a different story to extract it, make it available, and cover most edge cases for everyone.
That's why Avo and no other similar gems dont't offer this feature as a first-party feature.
But, that doesn't mean that it's impossible to implement it yourself. It's actually similar to how you'd implement it for your own app.
We prepared this scenario where a `Fish` model `has_many` `Review`s. I know, it's not the `Slider` `has_many` `Item`s example, but you'll get the point.
## Full set of changes
The full code is available in Avo's [dummy app](https://github.com/avo-hq/avo/tree/main/spec/dummy) and the changes in [this PR](https://github.com/avo-hq/avo/pull/1472).
## Guide to add it to your app
You can add this functionality using these steps.
### 1. Add `accepts_nested_attributes_for` on your parent model
```ruby{4}
class Fish < ApplicationRecord
has_many :reviews, as: :reviewable
accepts_nested_attributes_for :reviews
end
```
:::warning
Ensure you have the `has_many` association on the parent model.
:::
### 2. Add a JS helper package that dynamically adds more review forms
`yarn add stimulus-rails-nested-form`
In your JS file register the controller.
```js{3,6}
// Probably app/javascript/avo.custom.js
import { Application } from '@hotwired/stimulus'
import NestedForm from 'stimulus-rails-nested-form'
const application = Application.start()
application.register('nested-form', NestedForm)
```
:::info
Use this guide to add custom JavaScript to your Avo app.
:::
### 3. Generate a new resource tool
`bin/rails generate avo:resource_tool nested_fish_reviews`
This will generate two files. The `NestedFishReviews` ruby file you'll register on the `Avo::Resources::Fish` file and we'll edit the template to contain our fields.
### 4. Register the tool on the resource
We'll display it only on the view.
```ruby{7}
class Avo::Resources::Fish < Avo::BaseResource
# other fields actions, filters and more
def fields
field :reviews, as: :has_many
tool Avo::ResourceTools::NestedFishReviews, only_on: :new
end
end
```
### 5. Create a partial for one new review
This partial will have the fields for one new review which we'll add more on the page.
```erb
<%= render Avo::PanelComponent.new do |c| %>
<% c.with_body do %>
<%= avo_edit_field :body, as: :trix, form: f, help: "What should the review say", required: true %>
<%= avo_edit_field :user, as: :belongs_to, form: f, help: "Who created the review", required: true %>
<% end %>
<% end %>
```
### 6. Update the resource tool partial
It's time to put it all together. In the resource tool partial we're wrapping the whole thing with the `nested-form` controller div, creating a new `form` helper to reference the nested fields with `form.fields_for` and wrapping the "new" template so we can use replicate it using the `nested-form` package.
In the footer we'll also add the button that will add new reviews on the page.
```erb
<%= content_tag :div,data: { controller: 'nested-form', nested_form_wrapper_selector_value: '.nested-form-wrapper' } do %>
<%= render Avo::PanelComponent.new(name: "Reviews", description: "Create some reviews for this fish") do |c| %>
<% c.with_bare_content do %>
<% if form.present? %>
<%= form.fields_for :reviews, Review.new, multiple: true, child_index: 'NEW_RECORD' do |todo_fields| %>
<%= render "avo/partials/fish_review", f: todo_fields %>
<% end %>
<%= form.fields_for :reviews, Review.new, multiple: true do |todo_fields| %>
<%= render "avo/partials/fish_review", f: todo_fields %>
<% end %>
<% end %>
<% end %>
<% c.with_footer_tools do %>
<%= a_link 'javascript:void(0);', icon: 'plus', color: :primary, style: :outline, data: {action: "click->nested-form#add"} do %>
Add another review
<% end %>
<% end %>
<% end %>
<% end %>
```
### 7. Permit the new nested params
There's one more step we need to do and that's to whitelist the new `reviews_attributes` params to be passed to the model.
```ruby{2}
class Avo::Resources::Fish < Avo::BaseResource
self.extra_params = [reviews_attributes: [:body, :user_id]]
# other fields actions, filters and more
def fields
field :reviews, as: :has_many
tool Avo::ResourceTools::NestedFishReviews, only_on: :new
end
end
```
## Conclusion
There you have it!
Apart from the resource tool and the `extra_params` attribute, we wrote regular Rails code that we would have to write to get this functionality in our app.
---
# Authentication using Rails' scaffold
In essence, the [authentication scaffold](https://github.com/rails/rails/pull/52328) that Rails 8 comes with is custom authentication so we need to do a few things to ensure it's working properly with Avo.
## 1. Set the current user
The scaffold uses the `Current.user` thread-safe global to hold the current authenticated user so we need to tell Avo how to fetch them.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
# other pieces of configuration
# tell Avo how to find the current authenticated user.
config.current_user_method do
Current.user
end
end
```
## 2. Set the sign out link
The scaffold uses the `SessionsController` to sign out the user so the link should be `sessions_path`. We need to add that to Avo as well.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
# other pieces of configuration
# tell Avo how to sign out the authenticated user.
config.sign_out_path_name = :session_path
end
```
## 3. Ensure only authenticated users are allowed on Avo
Now, here comes the part which might seem unfamiliar but it's actually pretty standard.
The scaffold adds the `Authentication` concern to your `ApplicationController` which is great. We will add it to Avo's `ApplicationController` and also add the `before_action`, but instead of just appending it wil will prepend it so we can ensure it will be fired as soon as possible in the request lifecycle.
Since `require_authentication` runs in the Avo context, it's necessary to delegate the `new_session_path` to the `main_app` to ensure proper routing.
```ruby{4,5,8}
# app/controllers/avo/application_controller.rb
module Avo
class ApplicationController < BaseApplicationController
include Authentication
delegate :new_session_path, to: :main_app
# we are prepending the action to ensure it will be fired very early on in the request lifecycle
prepend_before_action :require_authentication
end
end
```
:::info
If you don't have the `app/controllers/avo/application_controller.rb` file present in your app, you can eject it using this command:
```bash
rails generate avo:eject --controller application_controller
```
:::
---
# REST API integration
Recipe [contributed](https://github.com/avo-hq/avo/issues/656) by [santhanakarthikeyan](https://github.com/santhanakarthikeyan).
I've built a page using AVO + REST API without using the ActiveRecord model. I was able to build an index page + associated has_many index page. It would be great if we could offer this as a feature, I guess, Avo would be the only admin framework that can offer this feature in case we take it forward :+1:
I've made it work along with Pagination, Filter and even search are easily doable.
`app/avo/filters/grace_period.rb`
```ruby
class GracePeriod < Avo::Filters::BooleanFilter
self.name = 'Grace period'
def apply(_request, query, value)
query.where(value)
end
def options
{
grace_period: 'Within graceperiod'
}
end
end
```
`app/avo/resources/aging_order_resource.rb`
```ruby
class AgingOrderResource < Avo::BaseResource
self.title = :id
self.includes = []
field :id, as: :text
field :folio_number, as: :text
field :order_submitted_at, as: :date_time, timezone: 'Chennai', format: '%B %d, %Y %H:%M %Z'
field :amc_name, as: :text
field :scheme, as: :text
field :primary_investor_id, as: :text
field :order_type, as: :text
field :systematic, as: :boolean
field :order_reference, as: :text
field :amount, as: :text
field :units, as: :text
field :age, as: :text
filter GracePeriod
end
```
`app/controllers/avo/aging_orders_controller.rb`
```ruby
module Avo
class AgingOrdersController < Avo::ResourcesController
def pagy_get_items(collection, _pagy)
collection.all.items
end
def pagy_get_vars(collection, vars)
collection.where(page: page, size: per_page)
vars[:count] = collection.all.count
vars[:page] = params[:page]
vars
end
private
def per_page
params[:per_page] || Avo.configuration.per_page
end
def page
params[:page]
end
end
end
```
`app/models/aging_order.rb`
```ruby
class AgingOrder
include ActiveModel::Model
include ActiveModel::Conversion
include ActiveModel::Validations
extend ActiveModel::Naming
attr_accessor :id, :investment_date, :folio_number, :order_submitted_at,
:amc_name, :scheme, :primary_investor_id, :order_type, :systematic,
:order_reference, :amount, :units, :age
class << self
def column_names
%i[id investment_date folio_number order_submitted_at amc_name
scheme primary_investor_id order_type systematic
order_reference amount units age]
end
def base_class
AgingOrder
end
def root_key
'data'
end
def count_key
'total_elements'
end
def all(query)
response = HTTParty.get(ENV['AGING_URL'], query: query)
JSON.parse(response.body)
end
end
def persisted?
id.present?
end
end
```
`app/models/lazy_loader.rb`
```ruby
class LazyLoader
def initialize(klass)
@offset, @limit = nil
@params = {}
@items = []
@count = 0
@klass = klass
end
def where(query)
@params = @params.merge(query)
self
end
def items
all
@items
end
def count(_attr = nil)
all
@count
end
def offset(value)
@offset = value
self
end
def limit(value)
@limit = value
items[@offset, @limit]
end
def all
api_response
self
end
def to_sql
""
end
private
def api_response
@api_response ||= begin
json = @klass.all(@params)
json.fetch(@klass.root_key, []).map do |obj|
@items << @klass.new(obj)
end
@count = json.fetch(@klass.count_key, @items.size)
end
end
end
```
`app/policies/aging_order_policy.rb`
```ruby
class AgingOrderPolicy < ApplicationPolicy
class Scope < Scope
def resolve
LazyLoader.new(scope)
end
end
def index?
user.admin?
end
def show?
false
end
end
```
`config/initializers/array.rb`
```ruby
class Array
def limit(upto)
take(upto)
end
end
```
---
# Integration with rolify
_Recipe contributed by [Paul](https://github.com/FLX-0x00) after discussing it [here](https://github.com/avo-hq/avo/issues/1568)._
It is possible to implement the [`rolify`](https://github.com/RolifyCommunity/rolify) gem in conjunction with `pundit` in an Avo using basic functionality.
Following the next steps allows for easy management of roles within the admin panel, which can be used to control access to different parts of the application based on user roles. By assigning specific permissions to each user role, Avo users can ensure that their admin panels remain secure and accessible only to authorised users.
:::warning
You must manually require `rolify` in your `Gemfile`.
:::
```ruby
gem "rolify"
```
**If this is a new app you need to do some initial steps, create the role model and specify which models should be handled by rolify**
:::info
Check out the [rolify documentation](https://github.com/RolifyCommunity/rolify) for reference.
:::
We assume that your model for managing users is called `Account` (default when using `rodauth`) and your role model is called `Role` (default when using `rolify`).
```ruby
class Account < ApplicationRecord
rolify
# ...
end
```
A `Role` connects to an `Account` through `has_and_belongs_to_many` while an `Account` connects to `Role` through `has_many` (not directly used in the model because the `rolify` statement manage this). Although rolify has its own functions for adding and deleting roles, normal rails operations can also be used to manage the roles. To implement this in avo, the appropriate resources need to be created.
*Perhaps the creation of the account resource is not necessary, as it has already been done in previous steps or has been created automatically by the avo generator through a scaffold/model. So we assume this step is already done.*
```zsh
bin/rails generate avo:resource role
```
After this step the `roles` should now accessible via the avo interface. The final modification should be done in the corresponding `Account` resource file.
```ruby
class AccountResource < Avo::BaseResource
# ...
field :assigned_roles, as: :tags, hide_on: :forms do
record.roles.map {|role|role.name}
end
# Only show roles that have not already been assigned to the object, because Avo does not use the add_role method, so it is possible to assign a role twice
field :roles, as: :has_many, attach_scope: -> { query.where.not(id: parent.roles.pluck(:id)) }
# ...
end
```
Example of RoleResource file:
```ruby
class RoleResource < Avo::BaseResource
self.title = :name
self.includes = []
field :name, as: :text
field :accounts, as: :has_and_belongs_to_many
end
```
The roles of an account can now be easily assigned and removed using avo. The currently assigned roles are displayed in the index and show view using the virtual `assigned_roles' field.
---
# Run Avo on the root path
You might want to run avo on the root path on your app.
We've seen plenty of users use this strategy.
This is as simple as changing the `root_path` from the `avo.rb` initializer to `/`.
```ruby{5}
Avo.configure do |config|
# other pieces of configuration
# Change the path to `/` to make it the root path
config.root_path = '/'
end
```
I used these commands to create a new repo and change the path.
```bash
rails new avo-root-path
cd avo-root-path
bin/rails app:template LOCATION='https://avohq.io/app-template'
sed -i '' "s|config.root_path = '/avo'|config.root_path = '/'|" config/initializers/avo.rb
```
---
# How to safely override the resource views without maintaining core components
Sometimes it's the small things in a UI that make a big impact. One of those things is being able to show a helpful message at the top of an index view page. This is typically where users land to see lists of posts, products, orders, or anything else. You might want to point out something important, offer quick guidance, or simply highlight a recent change.
:::info
What makes this guide particularly valuable is that it demonstrates how to safely override and customize the resource index component without having to maintain the original index component on each version update. While we'll be focusing on the index component in this guide, this technique can be applied to any resource view component in Avo. This approach lets you add custom functionality while still benefiting from Avo's updates to the core components, ensuring your customizations remain compatible across upgrades.
:::
That's where this guide comes in. I'll walk you through how to inject a custom message at the top of the index view. We'll do this by creating a new component that extends the one Avo already uses to render index pages, setting it as the default for specific resources (or all of them), and customizing the view to display our message cleanly above the list.
Let's jump in.
## Create a new view component
Start by generating a new view component that inherits from Avo's index view:
```sh
rails generate component Avo::Views::ResourceCustomIndex --parent=Avo::Views::ResourceIndexComponent
```
This will generate three files:
```ruby
# app/components/avo/views/resource_custom_index_component.rb
# frozen_string_literal: true
class Avo::Views::ResourceCustomIndexComponent < Avo::Views::ResourceIndex
end
```
```html
Add Avo::Views::ResourceCustomIndexComponent template here
```
```rb
# test/components/avo/views/resource_custom_index_component_test.rb
# frozen_string_literal: true
require "test_helper"
class Avo::Views::ResourceCustomIndexeComponentTest < ViewComponent::TestCase
def test_component_renders_something_useful
# assert_equal(
# %(Hello, components!),
# render_inline(Avo::Views::ResourceCustomIndexeComponent.new(message: "Hello, components!")).css("span").to_html
# )
end
end
```
:::tip
You can delete the generated test file `test/components/avo/views/resource_custom_index_component_test.rb` since we won't cover testing in this guide.
:::
## Use the custom component in a resource
Let's apply the new component to a specific resource. I'll use the `Movie` resource as an example.
Update the resource file (`Avo::Resources::Movie`) to use the new component via the `self.components` configuration:
```ruby
# app/avo/resources/movie.rb
class Avo::Resources::Movie < Avo::Resources::ArrayResource
self.components = { # [!code ++]
"Avo::Views::ResourceIndexComponent": Avo::Views::ResourceCustomIndexComponent # [!code ++]
} # [!code ++]
# ...
end
```
Now when you visit the Movies resource page, it will render the custom component, currently just showing the placeholder text.
## Render the parent view and add your message
Next, let's modify the component so it wraps the original Avo index component and adds a message on top.
Avo will now call this custom component first, let's update the Ruby component file to store all keyword arguments, and use those to render the parent component.
```ruby
# app/components/avo/views/resource_custom_index_component.rb
# frozen_string_literal: true
class Avo::Views::ResourceCustomIndexComponent < Avo::Views::ResourceIndex
def initialize(**kwargs) # [!code ++]
@kwargs = kwargs # [!code ++]
end # [!code ++]
end
```
Update the ERB template to render a message above the original component:
:::warning
All Tailwind CSS classes used in this guide are already part of Avo's design system and included in its pre-purged assets. If you plan to customize the appearance of the message component beyond what's shown here, you may need to set up the TailwindCSS integration.
:::
```html
Add Avo::Views::ResourceCustomIndexComponent template here
MovieFest 2025 β’ Discover what\'s trending this season in cinema πΏ
```
Now when you visit the Movies resource page, it will render the custom component that shows the original component and your custom message on top. πππ
## Apply this component to all the resources
You can apply the new component to each resource individually by setting `self.components`, but there's a more efficient approach. Since all your resources inherit from `Avo::BaseResource`, we can centralize this configuration by extending that base class.
To do this, override the base resource class by creating or modifying `app/avo/base_resource.rb`:
```rb
# app/avo/base_resource.rb
module Avo
class BaseResource < Avo::Resources::Base
self.components = { # [!code ++]
"Avo::Views::ResourceIndexComponent": Avo::Views::ResourceCustomIndexComponent # [!code ++]
} # [!code ++]
end
end
```
Now you can remove this configuration from the Movie resource:
```ruby
# app/avo/resources/movie.rb
class Avo::Resources::Movie < Avo::Resources::ArrayResource
self.components = { # [!code --]
"Avo::Views::ResourceIndexComponent": Avo::Views::ResourceCustomIndexComponent # [!code --]
} # [!code --]
# ...
end
```
With this change in place, every resource will automatically use the custom index component, no extra configuration needed. However, that raises a practical question: what if some resources should have a message, and others shouldn't?
Let's make the component more flexible by introducing a lightweight DSL extension.
## Make the message configurable via a resource method
To turn our static message into something dynamic and optional we'll fetch the message from a method on each resource. If a resource defines the `index_message` method, the component will render it. If not, it wonβt show anything.
Letβs update the Ruby component to support this:
```ruby
# app/components/avo/views/resource_custom_index_component.rb
# frozen_string_literal: true
class Avo::Views::ResourceCustomIndexComponent < Avo::Views::ResourceIndex
def initialize(**kwargs)
@kwargs = kwargs
@index_message = kwargs[:resource].try(:index_message) # [!code ++]
end
end
```
Now tweak the view to conditionally render the message:
```html
<% if @index_message.present? %>
MovieFest 2025 β’ Discover what\'s trending this season in cinema πΏ
<%= @index_message %>
<% end %>
<%= render Avo::Views::ResourceIndexComponent.new(**@kwargs) %>
```
To use this, just add an `index_message` method to any resource:
```ruby
# app/avo/resources/movie.rb
class Avo::Resources::Movie < Avo::Resources::ArrayResource
def index_message # [!code ++]
'MovieFest 2025 β’ Discover what\'s trending this season in cinema πΏ'.html_safe # [!code ++]
end # [!code ++]
# ...
end
```
---
### Wrapping up
Adding contextual messages to index pages can go a long way in making your internal tool more helpful. With this approach, you've learned how to:
- Extend Avo's default index view component
- Add custom UI above the resource index table
- Apply the enhancement globally across all resources
- Keep it flexible using a simple per-resource DSL
This solution is modular, declarative, and easy to maintain. You can now provide dynamic guidance to your users where it makes the most sense.
The beauty of this approach is that it safely overrides and customizes the resource index component without requiring you to maintain the original index component on each version update. While we've focused on adding a message at the top, this pattern opens horizons for extending the index component in any direction, whether adding elements at the bottom, on the sides, or anywhere else your application needs. You get the flexibility of customization while continuing to benefit from Avo's ongoing improvements to the core components.
---
# Display counter indicator on tabs switcher
When a tab contains an association field you may want to show some counter indicator about how many records are on that particular tab. You can include that information inside tab's name.
```ruby{7,10,16-23}
class Avo::Resources::User < Avo::BaseResource
def fields
main_panel do
end
tabs do
tab name_with_counter("Teams", record&.teams&.size) do
field :teams, as: :has_and_belongs_to_many
end
tab name_with_counter("People", record&.people&.size) do
field :people, as: :has_many
end
end
end
def name_with_counter(name, counter)
view_context.sanitize(
"#{name} " \
"" \
"#{counter}" \
""
)
end
end
```
We are also using the `sanitize` method to return it as HTML.
In order to make the counter stand out, we're using some Tailwind CSS classes that we have available in Avo. If you're trying different classes and they are not applying, you should consider adding the Tailwind CSS integration.
:::warning
This approach will have some performance implications as it will run the `count` query on every page load.
:::
---
# Use markdown for help attributes
:::info User contribution
Recipe [contributed](https://github.com/avo-hq/avo/issues/1390#issuecomment-1302553590) by [dhnaranjo](https://github.com/dhnaranjo).
:::
Desmond needed a way to write markdown in the help field and built an HTML to Markdown compiler.
```ruby
module MarkdownHelpText
class Renderer < Redcarpet::Render::HTML
def header(text, level)
case level
when 1 then %(
#{text}
)
when 2 then %(
#{text})
else
%(#{text})
end
end
def paragraph(text)
%(
#{text}
)
end
def block_code(code, language)
<<~HTML
#{code.chomp}
HTML
end
def codespan(code)
%(#{code})
end
def list(contents, list_type)
list_style = case list_type
when "ul" then "list-disc"
when "ol" then "list-decimal"
else "list-none"
end
%(<#{list_type} class="ml-8 mb-2 #{list_style}">#{contents}#{list_type}>)
end
end
def markdown_help(content, renderer: Renderer)
markdown = Redcarpet::Markdown.new(
renderer.new,
filter_html: false,
escape_html: false,
autolink: true,
fenced_code_blocks: true
).render(content)
%(#{markdown})
end
end
```
```ruby
field :description_copy, as: :markdown,
help: markdown_help(<<~MARKDOWN
# Dog
## Cat
### bird
paragraph about hats **bold hat**
~~~
class Ham
def wow
puts "wow"
end
end
~~~
`code thinger`
- one
- two
- three
MARKDOWN
)
```
---
# Use own helpers in Resource files
## TL;DR
Run `rails app:template LOCATION='https://railsbytes.com/script/V2Gsb9'`
## Details
A common pattern is to have some helpers defined in your app to manipulate your data. You might need those helpers in your `Resource` files.
#### Example:
Let's say you have a `Post` resource and you'd like to show a stripped-down version of your `body` field. So in your `posts_helper.rb` file you have the `extract_excerpt` method that sanitizes the body and truncates it to 120 characters.
```ruby
# app/helpers/posts_helper.rb
module PostsHelper
def extract_excerpt(body)
ActionView::Base.full_sanitizer.sanitize(body).truncate 120
end
end
```
Now, you'd like to use that helper inside one of you computed fields.
```ruby
class Avo::Resources::Post < Avo::BaseResource
def fields
field :excerpt, as: :text do |model|
extract_excerpt model.body
end
end
end
```
Initially you'll get an error similar to `undefined method 'extract_excerpt' for #`. That's because the compute field executes that method in a scope that's different from your application controller, thus not having that method present.
## The solution
The fix is to include the helper module in the `BaseField` and we can do that using this snippet somewhere in the app (you can add it in `config/initializers/avo.rb`).
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
# Usual Avo config
end
module FieldExtensions
# Include a specific helper
include PostsHelper
end
Rails.configuration.to_prepare do
Avo::Fields::BaseField.include FieldExtensions
end
```
Or you can go wild and include all helpers programatically.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
# Usual Avo config
end
module FieldExtensions
# Include all helpers
helper_names = ActionController::Base.all_helpers_from_path Rails.root.join("app", "helpers")
helpers = ActionController::Base.modules_for_helpers helper_names
helpers.each do |helper|
send(:include, helper)
end
end
Rails.configuration.to_prepare do
Avo::Fields::BaseField.include FieldExtensions
end
```
Now you can reference all helpers in your `Resource` files.
---
## Generation Information
- **Generated at:** 2025-06-30T13:55:35.746Z
- **Total sections:** 156
### Source Files
- docs/4.0/index.md
- docs/4.0/technical-support.md
- docs/4.0/best-practices.md
- docs/4.0/index.md
- docs/4.0/rails-and-hotwire.md
- docs/4.0/installation.md
- docs/4.0/gem-server-authentication.md
- docs/4.0/license-troubleshooting.md
- docs/4.0/authentication.md
- docs/4.0/authorization.md
- docs/4.0/resources.md
- docs/4.0/array-resources.md
- docs/4.0/http-resources.md
- docs/4.0/fields.md
- docs/4.0/field-options.md
- docs/4.0/field-discovery.md
- docs/4.0/controllers.md
- docs/4.0/record-previews.md
- docs/4.0/scopes.md
- docs/4.0/records-reordering.md
- docs/4.0/discreet-information.md
- docs/4.0/customizable-controls.md
- docs/4.0/cover-and-profile-photos.md
- docs/3.0/fields/array.md
- docs/3.0/fields/badge.md
- docs/3.0/fields/boolean.md
- docs/3.0/fields/boolean_group.md
- docs/3.0/fields/code.md
- docs/3.0/fields/country.md
- docs/3.0/fields/date.md
- docs/3.0/fields/date_time.md
- docs/3.0/fields/easy_mde.md
- docs/3.0/fields/external_image.md
- docs/3.0/fields/file.md
- docs/3.0/fields/files.md
- docs/3.0/fields/gravatar.md
- docs/3.0/fields/heading.md
- docs/3.0/fields/hidden.md
- docs/3.0/fields/id.md
- docs/3.0/fields/key_value.md
- docs/3.0/fields/location.md
- docs/3.0/fields/markdown.md
- docs/3.0/fields/money.md
- docs/3.0/fields/number.md
- docs/3.0/fields/password.md
- docs/3.0/fields/preview.md
- docs/3.0/fields/progress_bar.md
- docs/3.0/fields/radio.md
- docs/3.0/fields/record_link.md
- docs/3.0/fields/rhino.md
- docs/3.0/fields/select.md
- docs/3.0/fields/status.md
- docs/3.0/fields/tags.md
- docs/3.0/fields/text.md
- docs/3.0/fields/textarea.md
- docs/3.0/fields/time.md
- docs/3.0/fields/tip_tap.md
- docs/3.0/fields/trix.md
- docs/4.0/associations.md
- docs/4.0/associations/belongs_to.md
- docs/4.0/associations/has_one.md
- docs/4.0/associations/has_many.md
- docs/4.0/associations/has_and_belongs_to_many.md
- docs/4.0/resource-panels.md
- docs/4.0/resource-clusters.md
- docs/4.0/resource-sidebar.md
- docs/4.0/tabs.md
- docs/4.0/views.md
- docs/4.0/table-view.md
- docs/4.0/grid-view.md
- docs/4.0/map-view.md
- docs/4.0/customization.md
- docs/4.0/eject-views.md
- docs/4.0/custom-view-types.md
- docs/4.0/menu-editor.md
- docs/4.0/search.md
- docs/4.0/i18n.md
- docs/4.0/branding.md
- docs/4.0/routing.md
- docs/4.0/multitenancy.md
- docs/4.0/actions/overview.md
- docs/4.0/actions/generate.md
- docs/4.0/actions/registration.md
- docs/4.0/actions/execution.md
- docs/4.0/actions/customization.md
- docs/4.0/actions/guides-and-tutorials.md
- docs/4.0/dashboards.md
- docs/4.0/cards.md
- docs/4.0/kanban-boards.md
- docs/4.0/collaboration.md
- docs/4.0/rest-api/mount.md
- docs/4.0/rest-api/generators.md
- docs/4.0/rest-api/csrf-protection.md
- docs/4.0/rest-api/authentication.md
- docs/4.0/filters.md
- docs/4.0/basic-filters.md
- docs/4.0/dynamic-filters.md
- docs/4.0/forms-and-pages/overview.md
- docs/4.0/forms-and-pages/generator.md
- docs/4.0/forms-and-pages/pages.md
- docs/4.0/forms-and-pages/forms.md
- docs/4.0/custom-tools.md
- docs/4.0/custom-fields.md
- docs/4.0/custom-errors.md
- docs/4.0/resource-tools.md
- docs/4.0/stimulus-integration.md
- docs/4.0/custom-asset-pipeline.md
- docs/4.0/tailwindcss-integration.md
- docs/4.0/media-library.md
- docs/4.0/cache.md
- docs/4.0/views-performance.md
- docs/4.0/native-components/avo-panel-component.md
- docs/4.0/native-field-components.md
- docs/4.0/field-wrappers.md
- docs/4.0/internals.md
- docs/4.0/testing.md
- docs/4.0/avo-current.md
- docs/4.0/execution-context.md
- docs/4.0/encryption-service.md
- docs/4.0/select-all.md
- docs/4.0/icons.md
- docs/4.0/internal-model-names.md
- docs/4.0/avo-application-controller.md
- docs/4.0/asset-manager.md
- docs/4.0/plugins.md
- docs/4.0/guides.md
- docs/4.0/guides/act-as-taggable-on-integration.md
- docs/4.0/guides/acts_as_tenant_integration.md
- docs/4.0/guides/add-nested-fields-to-forms.md
- docs/4.0/guides/api-only-app.md
- docs/4.0/guides/attachment-policy-extension-for-pundit.md
- docs/4.0/guides/basic-authentication.md
- docs/4.0/guides/bulk_destroy_action_using_customizable_controls.md
- docs/4.0/guides/conditionally-render-styled-rows.md
- docs/4.0/guides/custom-ids.md
- docs/4.0/guides/custom-link-field.md
- docs/4.0/guides/display-and-edit-join-table-fields.md
- docs/4.0/guides/display-scope-record-count.md
- docs/4.0/guides/export-to-csv.md
- docs/4.0/guides/format-ruby-object-to-json.md
- docs/4.0/guides/generating-components-for-fields.md
- docs/4.0/guides/hide-field-labels.md
- docs/4.0/guides/how-to-use-phlex-components.md
- docs/4.0/guides/manage-information-heavy-resources.md
- docs/4.0/guides/multi-language-urls.md
- docs/4.0/guides/multilingual-content.md
- docs/4.0/guides/multitenancy.md
- docs/4.0/guides/nested-records-when-creating.md
- docs/4.0/guides/rails-authentication-scaffold.md
- docs/4.0/guides/rest-api-integration.md
- docs/4.0/guides/rolify-integration.md
- docs/4.0/guides/run-avo-on-the-root-path.md
- docs/4.0/guides/safely-override-resource-components.md
- docs/4.0/guides/tabs-counter-indicator.md
- docs/4.0/guides/use-markdown-in-help-attributes.md
- docs/4.0/guides/use-own-helpers-in-resource-files.md