# Avo for Ruby on Rails Documentation - Version 4.0
Generated from Avo documentation v4.0 for LLM consumption.
Concise index: https://docs.avohq.io/4.0/llms.txt
Documentation map (every page with its headings): https://docs.avohq.io/4.0/docs-map.md
# Getting started
Avo turns your Rails app into something your whole team can operate. You describe your data in small Ruby configuration files, and Avo builds the interface around it: the screens to browse and edit records, the actions that run your business logic, the dashboards, the search. It's the layer your team logs into every day to run the product, whether you use it as an admin panel, an internal tool, or the app itself.
Everything in these docs fits into three rings:
1. **[The core](#the-core)**. Describe your data once and Avo generates the full interface around it.
2. **[Add-ons](#add-ons)**. Optional gems that extend the core with new capabilities as your needs grow.
3. **[Your code](#your-code)**. Escape hatches at every level, so configuration never boxes you in.
## The core
You don't build screens with Avo. You tell it what data you have, and it builds the screens.
A resource maps to one of your models and declares its fields: `first_name` as `text`, `birthday` as `date`, `cover_photo` as `file`, and so on. From that, Avo renders the views to list, show, create, and edit records, complete with associations, validations surfaced from your models, and file uploads through Active Storage.
```ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
field :id, as: :id
field :first_name, as: :text
field :birthday, as: :date
field :cover_photo, as: :file
field :projects, as: :has_many
end
end
```
Around resources sit the tools your team works with every day: actions to run business logic on one or many records, filters to slice lists, and search to get to any record fast.
## Add-ons
The core covers managing records. Add-ons extend it toward whatever job your app or your internal tools have to do. Each one is a separate gem you install when the need shows up, and it slots into the same configuration style as everything else.
**Find and organize records**
- Global Search: search every resource from one box.
- Dynamic Filters: let users stack their own filter combinations without you writing each one.
- Resource Scopes: reuse the model scopes you already trust as one-click segments.
- Searchable Associations: type-to-search when attaching related records, even in huge tables.
- Record Reordering: drag-and-drop ordering for lists where position matters.
- Menu Editor: shape the sidebar into a navigation that makes sense for your team.
**Capture and edit data**
- Forms: standalone forms and pages beyond your resources, for surveys, settings, and bespoke flows.
- Nested Records: create and edit associated records inside the parent form.
- Reactive Fields: fields that show, hide, or update based on what the user picks.
- Media Library: manage every uploaded asset in one place.
**Run day-to-day operations**
- Dashboards and cards: metrics and charts at a glance.
- Kanban: drag-and-drop boards for pipelines and workflows.
- Notifications: in-app notifications for the events that matter.
- Collaboration: comments and status updates on records, so context stays with the data.
- Audit Logging: who did what, and when.
**Control who can do what**
- Authorization: granular permissions using Pundit policies.
- Custom Controls: decide exactly which buttons and actions each user sees.
**Connect other systems**
- JSON API: expose your resources as a JSON API for integrations and mobile apps.
- HTTP Resource: manage data from an external API as if it were local.
## Your code
When configuration isn't enough, you drop down to plain Rails. Avo is built to be extended, not worked around:
- Custom fields slot into panels and views just like built-in ones.
- Resource tools embed your own partials inside a resource's pages.
- Custom tools give you entire pages built from partials or View Components.
- Eject views copies any of Avo's own templates into your app for full control.
- JavaScript & Stimulus is baked in for sprinkling interactivity anywhere.
- Plugins package your extensions for reuse across apps.
## Security
Avo runs entirely inside your Rails app. It ships as a gem, reads and writes through your own models, and your data stays on your servers; there's no external dashboard or third-party service between your team and your database.
On top of that, you control access at every level: authentication plugs into whatever you already use (Devise, the Rails 8 scaffold, or anything that gives you a current user), authorization enforces granular permissions through Pundit policies, Custom Controls decide which buttons each user even sees, and Audit Logging keeps a record of who did what.
## Build it with AI agents
The same thing that makes Avo fast for you makes it fast for AI agents: features are small, declarative Ruby files, so agents like Claude Code or Cursor generate resources, actions, and filters that work on the first try instead of hallucinating view code.
We lean into it. Point your agent at the [docs map](https://docs.avohq.io/4.0/docs-map.md) so it pulls accurate, current docs, and install the official [Avo skills](https://github.com/avo-hq/skills), pre-built workflows for building resources, writing tests, creating field types, and more. The Agentic engineering page walks through the full setup for every editor.
## Seamless upgrades
Avo ships as a [gem](https://rubygems.org/gems/avo), so none of its internals live in your app: no file conflicts, no generated code drifting out of date.
```bash
bin/rails avo:update
```
That resolves every Avo gem you have β core plus any add-ons β and updates them together, conservatively.
## Start here
1. Install Avo in your app
2. Hook up the current user
3. Create your first resource
4. Set up authorization
5. [Explore the live demo app](https://main.avodemo.com/)
## Where to go next
- Best practices for structuring a real-world Avo app.
- Guides with worked examples and recipes.
- FAQ for common setup questions.
- Avo 3 to Avo 4 upgrade if you're coming from an existing app.
- Technical support when you get stuck.
---
# 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
```
---
# Agentic engineering
Avo is designed to work well with AI coding agents. This page covers the tools and setup that let you build Avo features faster by pairing with agents like Claude Code, Cursor, Windsurf, and others.
## Code editors and LLM setup
AI agents generate better code when they have up-to-date Avo documentation in their context. Point your tool at https://docs.avohq.io/4.0/docs-map.md β a map of every Avo 4 docs page and its headings, with links β and the agent will fetch exactly the pages it needs to generate accurate resources, fields, actions, filters, and more.
### Pick your tool
## Skills
Skills are pre-built instruction sets that teach your agent how to perform specific Avo workflows. Instead of prompting from scratch each time, you install a skill and the agent follows a proven, repeatable process.
The skills ship **inside the `avo` gem**, so they describe the version your app has locked rather than whatever version a globally-installed copy happened to be written for.
Install the loader that finds them with **`rails g avo:skills`**:
That starts a short interactive installer β one question per screen, all answered up front, with a summary of exactly what it will do before it writes anything (Avo 4.1.3 and later). It:
- installs into this app, or into your home directory so one copy serves every Avo project
- sets up any of `.agents/skills`, `.claude/skills`, and `.cursor/skills`
- writes one real file and symlinks the others to it, so they cannot drift apart
- offers to remove skills left behind by an older, unversioned install
It writes one small markdown file. The skills themselves are never copied into your repo β the loader resolves them from the installed gem when a task actually needs them.
Skills are organized by **vertical** β a whole feature area, not a single task β so one skill covers creating, configuring, and troubleshooting that part of Avo.
### Keeping them current
They are always current. The skills come from the gem your app has locked, so there is nothing to copy and nothing that can fall behind.
Update your Avo gems with `bin/rails avo:update` and the skills come with them. Use that task rather than `bundle update avo`, which moves core alone and leaves each add-on's skills on the version they were already pinned to.
### Core
| Skill | What it covers |
| ------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `avo-resources` | generate and configure resources β title, includes, sorting, pagination, cover/avatar, array (non-DB) resources |
| `avo-fields` | add and configure fields in `def fields` β pick the `as:` type, options, formatting, layout |
| `avo-associations` | wire `belongs_to` / `has_many` / `has_one` / HABTM fields, polymorphism, STI, nested create-in-form |
| `avo-actions` | build actions that run Ruby on selected, single, or no records β bulk ops, forms, modals, responses |
| `avo-filters` | basic filters on the index β dynamic filters and scopes ship as their own add-on skills |
| `avo-index-views` | control how the index renders β table styling, grid cards, map markers, view types |
| `avo-custom-fields` | build a brand-new field type β generator plus its Edit/Show/Index view components |
| `avo-menu-icons` | pick semantically appropriate Tabler icons and set them on resources and dashboards |
### Config & ops
| Skill | What it covers |
| ------------------------- | ---------------------------------------------------------------------------------------------- |
| `avo-setup` | install Avo, mount it, authenticate the private gem server, and set the license key |
| `avo-update` | bump the Avo gems and apply every upgrade-guide step for the versions crossed, with a log |
| `avo-authentication` | tell Avo who the current user is, gate access, and wire roles / profile / sign-out |
| `avo-admin-config` | global initializer knobs β app name, per-page, container width, density, home path |
| `avo-performance` | caching and stale-row fixes to make the admin fast |
| `avo-testing` | unblock the license check in the test suite and use Avo's test helpers |
| `avo-multitenancy` | scope the admin per tenant β route- or session-based, with an account switcher |
### Customization
| Skill | What it covers |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `avo-branding-appearance` | make the admin look like the product β logo, favicon, color scheme, palettes, CSS re-skin, icons |
| `avo-navigation-search` | per-resource search, breadcrumbs, keyboard shortcuts, and the auto-generated sidebar |
| `avo-custom-ui` | build custom pages, embedded panels, dynamic/nested forms, eject views, JS/Stimulus, Tailwind |
| `avo-i18n` | translate and localize the admin β labels, locale switching, RTL |
| `avo-media-library` | turn on the central asset browser and the picker inside rich-text editors |
| `avo-controllers` | override per-resource CRUD controller hooks and safely extend Avo's `ApplicationController` |
| `avo-engine-internals` | engine plumbing for custom Ruby β `main_app`/`avo` helpers, `Avo::Current`, `ExecutionContext`, reserved names |
### Add-ons
Separately-licensed gems (paid add-on or Enterprise). These skills ship inside their own gem, not inside `avo`.
| Skill | What it covers |
| ------------------------- | -------------------------------------------------------------------------------------------------- |
| `avo-dashboards-cards` | dashboards (grids of cards) and the six card types β metrics, charts, tables, lists |
| `avo-notifications` | in-app notifications β bell dropdown, levels, action buttons, optional realtime |
| `avo-rest-api` | JSON REST API over every resource, with token auth and a per-token permission matrix |
| `avo-forms-and-pages` | model-agnostic forms (settings, imports, workflows) and sidebar page hierarchies |
| `avo-kanban` | DB-backed drag-and-drop boards across resources |
| `avo-audit-logging` | track who changed and viewed what β timeline, diffs, revert |
| `avo-collaboration` | comments, reactions, and an automatic change-log on a record |
| `avo-authorization` | restrict who sees and does what with Pundit policies β resources, actions, associations, files |
| `avo-http-resource` | back a resource with an external HTTP API instead of Active Record |
| `avo-menu` | build the sidebar with the menu editor DSL β sections, dividers, links, icons, per-user visibility |
| `avo-scopes` | one-click segment tabs above the index, with counts and a default view |
| `avo-dynamic-filters` | let end users build their own ad-hoc filters from a filters bar |
| `avo-advanced-search` | the Cmd + K global search palette, and type-to-search association pickers |
| `avo-record-reordering` | persistent up/down and drag-and-drop record ordering |
| `avo-custom-controls` | take over the show/edit/index/row button bars β relabel, remove, add links/actions/dropdowns |
### Cross-cutting
| Skill | What it covers |
| ------------------------- | ---------------------------------------------------------------------------------------------- |
| `avo-aware` | keep the admin in sync when you change a Rails model, even when the request never mentions Avo |
| `avo-troubleshoot` | diagnose a broken or misbehaving Avo app, organized by symptom |
Paid add-on skills arrive with their gems β install `avo-kanban` and its skill comes with it. The loader lists what your app actually has, and names the add-on for anything it does not, so an agent never describes a feature you cannot use.
### One loader for every project
Choose **This machine** when the installer asks where the loader should go.
The loader carries no version knowledge β it finds the app by walking up from the working directory and reads that app's `Gemfile.lock` β so a single copy serves every Avo project on the machine, each resolving its own version. Install once and you are done.
The per-app install is still worth it for a team: the loader is one small file you commit, so everyone gets it on `git pull` rather than each person installing it themselves.
## MCP server
For agents that support MCP, the [Context7](https://context7.com/) [MCP server](https://github.com/upstash/context7-mcp) serves up-to-date docs for many libraries, including Avo. The agent queries it for the exact docs it needs while building features, instead of relying on stale training data.
For example, to add it to Claude Code, run:
Each editor's [setup page](#pick-your-tool) covers how to add it to that tool. Then write `use context7` at the end of your prompt.
## Suggested workflow
1. Set up your editor with the Avo LLM context β [see above](#code-editors-and-llm-setup).
2. Install the Avo skills loader β run `rails g avo:skills` in your app.
3. Describe what you want to build. The agent will follow the skill workflow and reference the docs automatically.
4. Optionally connect the [Context7 MCP server](#mcp-server) so the agent can query Avo's docs directly.
---
# Installation
## Requirements
- Ruby on Rails >= 6.1
- Ruby >= 3.2
- `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'
```
:::tip Let an LLM do it for you
Working with an AI coding assistant (Claude, Cursor, Copilot, etc.)? Paste this prompt and let it handle the install:
```
Install and configure Avo in this Rails app by following
https://docs.avohq.io/4.0/installation.html β run the install steps,
wire up the initializer and routes, then generate my first resource.
```
:::
### 2. Manual, step by step.
1. Add the appropriate Avo gem to the `Gemfile`
```ruby
# Avo Community
gem "avo", ">= 4.0.0"
# If you're on a paid plan, add the feature gems included in your license.
source "https://packager.dev/avo-hq/" do
# all or some of these
gem "avo-authorization", ">= 4.0.0"
gem "avo-advanced_search", ">= 4.0.0"
gem "avo-advanced_file_uploads", ">= 4.0.0"
gem "avo-record_reordering", ">= 4.0.0"
gem "avo-menu_editor", ">= 4.0.0"
gem "avo-menu", ">= 4.0.0"
gem "avo-dashboards", ">= 4.0.0"
gem "avo-http_resource", ">= 4.0.0"
gem "avo-dynamic_filters", ">= 4.0.0"
gem "avo-nested", ">= 4.0.0"
gem "avo-collaboration", ">= 4.0.0"
gem "avo-forms", ">= 4.0.0"
gem "avo-kanban", ">= 4.0.0"
gem "avo-api", ">= 4.0.0"
gem "avo-reactive_fields", ">= 4.0.0"
gem "avo-notifications", ">= 4.0.0"
end
```
:::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.
If you have a Bullet Train app and you'd like to add Avo, please use [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.
---
# 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, so you mount everything with a single `mount_avo` call.
## 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 `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` also accepts a block, so you can append your own routes to the Avo engine inline (see [Add your own routes](#add-your-own-routes)), and a `mount_lookbook: true` argument to mount [Lookbook](https://lookbook.build) alongside Avo in development.
## 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.
:::
## 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}
# config/routes.rb
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
# config/initializers/avo.rb
Avo.configure do |config|
config.home_path = "/uk/admin/dashboard"
config.set_initial_breadcrumbs do
add_breadcrumb "Dashboard", "/uk/admin/dashboard"
end
end
```
## Serve Avo from a custom `map` in `config.ru`
If you serve your Rails app under a prefix through a custom `map` block in `config.ru`, set `prefix_path` to that mapping's prefix so Avo generates correct paths.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.prefix_path = "/internal"
end
```
## Namespaced resource routes
A namespaced resource like `Avo::Resources::Galaxy::Planet` gets a slash-joined route path derived from its class name β `galaxy/planets` in this case. Avo generates and mounts these routes for you, so there's nothing to configure by hand.
## 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
# config/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
```
---
# Gem server authentication
Avo comes in a few tiers. The Community tier is a free gem available on rubygems.org, and a few paid tiers ship as private gems hosted on our own private gems server ([`packager.dev`](https://packager.dev)).
To access the paid gems you must authenticate using the **Gem Server Token** found on your [dashboard](https://avohq.io/dashboard).
There are a few ways to do that. We focus on the most important and secure ones: [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 use the `xxx` notation 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 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 has its own way to add environment variables. See how to do it on [Heroku](#heroku), [Hatchbox](#hatchbox), [GitHub Actions](#github-actions), [Docker](#docker-and-docker-compose) or [Kamal](#kamal).
:::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.
Add the environment variable through the service's dedicated page or by running the `export` command before `bundle install`.
:::
### Heroku
Set the environment variable with the following command. This way `bundler` uses it when authenticating to `packager.dev`.
```bash
heroku config:set BUNDLE_PACKAGER__DEV=xxx
```
### Hatchbox
Set the environment variable in your app's "Environment" tab. This way `bundler` uses it when authenticating to `packager.dev`.
```yaml
BUNDLE_PACKAGER__DEV: xxx
```
### GitHub Actions
You might need to install Avo's paid gems in your GitHub Actions pipeline. There are two steps to enable that.
#### 1. Add `BUNDLE_PACKAGER__DEV` to your repository's secrets
In your repo, go to Settings β Secrets and Variables β Actions β New repository secret and add your gem server token with the name `BUNDLE_PACKAGER__DEV` and the token as the value.
#### 2. Expose `BUNDLE_PACKAGER__DEV` as an environment variable
Then, in your workflow file, expose that configuration item as an environment variable.
```yml{9-10}
# .github/workflows/test.yml
name: Tests
on:
pull_request:
branches:
- main
env:
BUNDLE_PACKAGER__DEV: ${{secrets.BUNDLE_PACKAGER__DEV}}
jobs:
test:
runs-on: ubuntu-latest
steps:
# Testing and deployment steps
```
### Docker and Docker Compose
Build with Docker by passing a build argument from your environment.
```dockerfile{9,11}
# Dockerfile
FROM ruby:3.2.2
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client
WORKDIR /app
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
# get the build argument
ARG BUNDLE_PACKAGER__DEV
# make it available in the docker image
ENV BUNDLE_PACKAGER__DEV=$BUNDLE_PACKAGER__DEV
RUN bundle install
COPY . /app
# do more stuff
```
```bash
# Pass the key to the build argument
docker build --build-arg BUNDLE_PACKAGER__DEV=xxx
# OR
# Set the key as an environment variable on your machine
# Somewhere in your `.bashrc` or `.bash_profile` file
export BUNDLE_PACKAGER__DEV=xxx
# Then pass it to the build argument from there
docker build --build-arg BUNDLE_PACKAGER__DEV=$BUNDLE_PACKAGER__DEV
```
```bash
docker compose build --build-arg BUNDLE_PACKAGER__DEV=xxx
```
### Kamal
Kamal setup is very similar to Docker: include `BUNDLE_PACKAGER__DEV` in your secrets and then use it in your `Dockerfile`.
In your `deploy.yml`:
```yaml
# config/deploy.yml
# Configure builder setup.
builder:
arch: amd64
secrets:
- BUNDLE_PACKAGER__DEV
```
Then in `.kamal/secrets`:
```bash
# .kamal/secrets
# However you set your secrets in Kamal
BUNDLE_PACKAGER__DEV=xxx
```
Finally, in your `Dockerfile`:
```dockerfile
# Dockerfile
# Install application gems
COPY Gemfile Gemfile.lock ./
RUN --mount=type=secret,id=BUNDLE_PACKAGER__DEV BUNDLE_PACKAGER__DEV=$(cat /run/secrets/BUNDLE_PACKAGER__DEV) bundle install && \
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
bundle exec bootsnap precompile --gemfile
```
## On your local development environment
For your local development environment, 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
# Avo Community
gem "avo", ">= 4.0.0"
# If you're on a paid plan, add the feature gems included in your license.
source "https://packager.dev/avo-hq/" do
# all or some of these
gem "avo-authorization", ">= 4.0.0"
gem "avo-advanced_search", ">= 4.0.0"
gem "avo-advanced_file_uploads", ">= 4.0.0"
gem "avo-record_reordering", ">= 4.0.0"
gem "avo-menu_editor", ">= 4.0.0"
gem "avo-menu", ">= 4.0.0"
gem "avo-dashboards", ">= 4.0.0"
gem "avo-http_resource", ">= 4.0.0"
gem "avo-dynamic_filters", ">= 4.0.0"
gem "avo-nested", ">= 4.0.0"
gem "avo-collaboration", ">= 4.0.0"
gem "avo-forms", ">= 4.0.0"
gem "avo-kanban", ">= 4.0.0"
gem "avo-api", ">= 4.0.0"
gem "avo-reactive_fields", ">= 4.0.0"
gem "avo-notifications", ">= 4.0.0"
end
```
Run `bundle install` and `bundler` picks up the token and uses it to authenticate on the server.
## Bundle without the paid gems
If you need to distribute your Rails app without the paid gems, 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", "~> 4.0"
end
end
```
## FAQ
### `Forbidden 403`
If you're seeing this error `Retrying download gem from https://packager.dev/avo-hq/ due to error (1/4): Gem::RemoteFetcher::FetchError bad response Forbidden 403`, this probably means that bundler does not have access to the `BUNDLE_PACKAGER__DEV` environment variable.
Read the guides above on how to set it on your development machine and in deployment scenarios.
### `Forbidden 403` in a sandboxed or cloud environment (Cursor Cloud)
If the token is set correctly but you still get a `403 Forbidden` inside a sandboxed or cloud environment (for example the Claude Code cloud environment, Cursor's background/cloud agents, or any setup with restricted network egress), the request to `packager.dev` is likely being blocked by a network allowlist rather than failing authentication.
Add `packager.dev` to the environment's list of allowed hosts and run `bundle install` again.
---
# 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 and what was the response from our checking server. The license key is hidden by default for security β set `exclude_from_status` to `[]` in your Avo initializer if you need to see it on the status page.
## 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 profile widget
The sidebar footer shows a small profile widget displaying three pieces of information about the current user β `name`, `photo`, and `title` β all read from the object returned by your [`current_user_method`](#customize-the-current-user-method).
- **Name:** Avo calls the `name` method. If the object doesn't respond to it, it tries `email`, then falls back to `Avo user`.
- **Photo:** the `avatar` method, used as the `src` of the photo.
- **Title:** the `avo_title` method, displayed under the name.
## 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.
Here's a full example of a policy file with all the methods Avo checks. Each one is described in detail below.
```ruby
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def index?
true
end
def show?
true
end
def new?
create?
end
def create?
user.admin?
end
def edit?
true
end
def update?
user.admin?
end
def destroy?
user.admin?
end
def act_on?
user.admin?
end
def reorder?
user.admin?
end
def search?
true
end
def preview?
true
end
end
```
## 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`.
There's a concern that removes the duplication and helps you apply the same rules to associations. You should include `Avo::Authorization::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
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 Actions inherit attachment authorization
These attachment authorization methods also apply to file fields in actions that run on the resource using the same policy. For example, if you define `upload_file?` in `PostPolicy` and have an action on `Avo::Resources::Post` with `field :file, as: :file`, the same `upload_file?` policy method will be used to authorize the file upload in that action.
:::
:::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
Pundit is the default client, but you can plug in any authorization library (for example [Action Policy](https://github.com/palkan/action_policy)) by implementing a small adapter class.
:::info Reference implementation
The Pundit adapter examples below follow the same contract as Avo's built-in `:pundit` client. Community examples for Action Policy are available in [this issue](https://github.com/avo-hq/avo/issues/1922).
:::
### How Avo uses your client
Your client is a thin adapter between Avo and your authorization library. Avo calls four methods on it:
#### `policy(user, record)`
Finds the policy for a model class or record. Avo calls this before running authorization checks β for example to resolve `EquipmentPolicy` for the `Equipment` model or a specific record.
```ruby
policy(user, Equipment) # => EquipmentPolicy instance
policy(user, equipment_record) # => EquipmentPolicy instance
```
---
#### `authorize(user, record, action, policy_class:, raise_exception:, **kwargs)`
Checks whether the user can perform an action β for example `"index?"`, `"new?"`, or `"create?"`.
Avo calls this for sidebar items, buttons, menu visibility, and controller requests. The `raise_exception` keyword tells Avo how to handle a denial:
- **UI visibility checks** (sidebar, buttons, menu) β Avo passes `raise_exception: false`. Your client should still **raise** on denial. Avo catches the exception and returns `false` to hide the element.
- **Controller requests** β Avo omits `raise_exception`. Your client should raise on denial and Avo will show the unauthorized page.
---
#### `apply_policy(user, model, policy_class:)`
Scopes a query to records the user is allowed to see. Avo calls this on , , and views to filter the underlying query β for example returning only published posts for non-admin users.
:::tip Menu editor
You can call `authorize` yourself in the menu editor `visible` block. It delegates to the same client configured in `config.authorization_client`.
```ruby
authorize current_user, Team, "index?", raise_exception: false
```
:::
### 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 = "Avo::ActionPolicyAuthorizationClient"
end
```
### Client methods
Each authorization client must expose four methods. **All method signatures must accept keyword arguments you do not use** (for example `raise_exception:` and `policy_class:`). If your `authorize` method does not accept these keywords, Ruby will raise an `ArgumentError`. When that happens inside a UI visibility check, Avo may treat the action as unauthorized without a clear error message.
:::info Accept extra keywords (and ignore them)
Your client should accept extra keyword arguments (usually via `**`) for forward compatibility. For example, Avo may pass `raise_exception:` for UI checks and other keys (like `resource_class:`) for logging. Your client should generally **ignore** these flags and focus on raising the correct Avo errors on missing policy / denial.
:::
:::warning Raise on denial β do not return `false`
When authorization fails, your client's `authorize` method **must raise an exception**. Avo does not use the return value of `authorize`.
When Avo passes `raise_exception: false`, it still expects your client to raise on denial. Avo catches the exception and returns `false` to the caller. If your client returns `false` instead of raising, Avo will treat the action as **authorized**.
This is how the built-in Pundit client works β `Pundit.authorize` always raises `Pundit::NotAuthorizedError` when access is denied.
:::
#### `authorize`
Receives the `user`, `record`, `action`, and optionally `policy_class`, `raise_exception`, and other keyword arguments. Map authorization failures to `Avo::NotAuthorizedError` and missing policies to `Avo::NoPolicyError`.
```ruby
# Pundit example
def authorize(user, record, action, policy_class: nil, **)
Pundit.authorize(user, record, action, policy_class: policy_class)
rescue Pundit::NotDefinedError => error
raise Avo::NoPolicyError, error.message
rescue Pundit::NotAuthorizedError => error
raise Avo::NotAuthorizedError, error.message
end
```
---
#### `policy`
Receives the `user` and `record` and returns the policy instance to use. Return `nil` when no policy exists.
```ruby
def policy(user, record, **)
Pundit.policy(user, record)
end
```
---
#### `policy!`
Receives the `user` and `record` and returns the policy instance. Raise `Avo::NoPolicyError` when no policy is found.
```ruby
def policy!(user, record, **)
Pundit.policy!(user, record)
rescue Pundit::NotDefinedError => error
raise Avo::NoPolicyError, error.message
end
```
---
#### `apply_policy`
Receives the `user`, the query to scope (usually an Active Record relation or model class), and optionally the policy class to use.
```ruby
def apply_policy(user, model, policy_class: nil, **)
scope_from_policy_class = scope_for_policy_class(policy_class)
if scope_from_policy_class.present?
scope_from_policy_class.new(user, model).resolve
else
Pundit.policy_scope!(user, model)
end
rescue Pundit::NotDefinedError => error
raise Avo::NoPolicyError, error.message
end
```
### Action Policy example
Here is a complete Action Policy client that follows Avo's contract:
```ruby
# app/services/avo/action_policy_authorization_client.rb
module Avo
class ActionPolicyAuthorizationClient
include ::ActionPolicy::Behaviour
authorize :user
attr_accessor :user
def authorize(user, record, action, policy_class: nil, **)
self.user = user
authorize!(record, to: action, with: policy_class)
rescue ActionPolicy::Unauthorized => error
raise Avo::NotAuthorizedError, error.message
end
def policy(user, record, **)
policy!(user, record)
rescue Avo::NoPolicyError
nil
end
def policy!(user, record, **)
self.user = user
policy_for(record:)
rescue ActionPolicy::NotFound => error
raise Avo::NoPolicyError, error.message
end
def apply_policy(user, model, policy_class: nil, **)
policy = if policy_class.present?
policy_class.new(model, user:)
else
policy!(user, model)
end
policy.apply_scope(model, type: :active_record_relation)
end
end
end
```
Place your policies under the `Avo` namespace (for example `Avo::EquipmentPolicy`) or configure Action Policy's lookup to match your app.
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.authorization_client = "Avo::ActionPolicyAuthorizationClient"
end
```
```ruby
# app/policies/avo/equipment_policy.rb
module Avo
class EquipmentPolicy < ApplicationPolicy
def index?
true
end
def new?
user.admin?
end
def create?
user.admin?
end
end
end
```
## Explicit authorization
## Rolify integration
Check out this guide to add rolify role management with Avo.
---
# Performance
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
Avo picks its cache store based on the environment:
- **Production** β Avo uses `Rails.cache`, unless it's one of `ActiveSupport::Cache::MemoryStore` or `ActiveSupport::Cache::NullStore`. In that case it falls back to the `:file_store` with a cache path of `tmp/cache`.
- **All other environments** (development, test, and any custom environment) β Avo uses the `:file_store` with a cache path of `tmp/cache`.
:::warning `MemoryStore` in production
Our recommendation is to not use `MemoryStore` in production because it will not be shared between multiple processes (when using Puma). That's why Avo rejects it and falls back to the `:file_store`.
:::
### 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)
```
The `cache_store` configuration option expects a cache store object. The lambda syntax can be useful if different stores are desired on different environments.
## Row caching
Avo caches each record on the view (and each item on the Grid view) for improved performance.
## Caching caveats
Because Avo caches each record on the view, some side-effects may occur. 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.
## 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.
:::
---
# Resources
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.
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
def fields
field :id, as: :id
field :name, as: :text
end
end
```
From this file alone, Avo infers the model (`Post`), the resource name, the routes, and adds the resource to the sidebar. Everything inferred can be overridden through the resource options.
## Generate resources
### Alongside a model
```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: q, m: "or").result(distinct: false) }
# }
def fields
field :id, as: :id
field :make, as: :text
field :mileage, as: :number
end
end
```
The auto-generated controller will look like this:
```ruby
# app/controllers/avo/cars_controller.rb
class Avo::CarsController < Avo::ResourcesController
end
```
The Avo Resource should always be accompanied by a controller.
If you don't want the Avo counterpart, pass `--skip-avo-resource`:
```bash
bin/rails generate model car make:string kms:integer --skip-avo-resource
```
### With the resource generator
For an existing model, generate the resource directly:
```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: q, m: "or").result(distinct: false) }
# }
def fields
field :id, as: :id
end
end
```
If the `Post` model is already well defined with attributes and associations, the resource will be generated with the matching fields:
::: 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: 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
```
:::
If you want the resource to use a different model, pass `--model-class`. For example, to create a `MiniPost` resource backed by the `Post` model:
```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, specifying `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.
:::
### For all your models
To generate Avo resources for all models in your application, run:
```bash
bin/rails generate avo:all_resources
```
The generator scans your `app/models` directory, includes only classes that inherit from `ActiveRecord::Base`, excludes abstract classes (e.g. `ApplicationRecord`) and non-model files (concerns, POROs, `Current`, form objects), and runs the `avo:resource` generator for each match β printing an error message if generation fails for any model.
This is particularly useful when setting up Avo in an existing Rails application or 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, 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.
## Name and describe records
Avo figures out a record's display name by trying the `name`, `title`, and `label` attributes in order, falling back to `id`. If that guess is wrong for your model, point `self.title` to another attribute:
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.title = :slug
end
```
If no single attribute works as a title, assign a block instead β you have access to `record` and `resource`, so you can compose whatever reads best:
```ruby
# app/avo/resources/comment.rb
class Avo::Resources::Comment < Avo::BaseResource
self.title = -> {
ActionView::Base.full_sanitizer.sanitize(record.body).truncate 30
}
end
```
To show a message to your users on the resource's pages, set `self.description` β a string for all views, or a block when the message depends on the `view`, `record`, or `current_user`:
```ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
self.description = "These are the users of the app."
end
```
You can also set the record's avatar with `self.avatar` and the sidebar icon with `self.icon`. To make the resource easier to spot β or to visually group related resources β tint the sidebar icon with `self.color`:
```ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
self.avatar = :avatar
self.icon = "tabler/outline/user"
self.color = :purple
end
```
## Avoid n+1 queries
If a resource displays associations or attachments, eager load them with `self.includes` and `self.attachments` to dodge `n+1` performance issues on the `Index` view:
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.includes = [:user, :tags]
self.attachments = [:cover_photo]
end
```
Use `self.single_includes` and `self.single_attachments` when you need the same eager loading on the and views.
## Control sorting
Records on the `Index` view are sorted by `created_at`, descending. Change the column with `self.default_sort_column` and the direction with `self.default_sort_direction`:
```ruby
# app/avo/resources/task.rb
class Avo::Resources::Task < Avo::BaseResource
self.default_sort_column = :position
self.default_sort_direction = :asc
end
```
If your model has a `default_scope` you don't want applied on the index, unscope it with `self.index_query`:
```ruby
# app/avo/resources/project.rb
class Avo::Resources::Project < Avo::BaseResource
self.index_query = -> { query.unscoped }
end
```
## Customize how records are fetched
If your records are identified by something other than the numeric `id` β a slug from a custom `to_param` method, for example β tell Avo how to find them with `self.find_record_method`:
::: code-group
```ruby [app/avo/resources/post.rb]
class Avo::Resources::Post < Avo::BaseResource
self.find_record_method = -> {
# `id` is an Array in batch contexts (bulk actions), so return a collection there.
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
```
:::
If you use a gem for custom IDs you likely don't need this at all β Avo detects [FriendlyId](https://github.com/norman/friendly_id) automatically, and [prefixed_ids](https://github.com/excid3/prefixed_ids) and [hashid-rails](https://github.com/jcypret/hashid-rails) work out of the box. See the custom IDs guide for the setup for each gem.
## Customize pagination
On large tables, counting all records to render the pagination can get expensive. Switch `self.pagination` to the `:countless` type to skip the count entirely:
```ruby
# app/avo/resources/log_entry.rb
class Avo::Resources::LogEntry < Avo::BaseResource
self.pagination = {
type: :countless
}
end
```
The `slots` key controls how many page links are rendered β see the pagination reference for all the combinations.
## Control the saving flow
If saving deserves a second thought, set `self.confirm_on_save` to ask users for confirmation before persisting:
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.confirm_on_save = true
end
```
After creating or updating a record, Avo redirects to the view. Redirect somewhere else with `self.after_create_path` and `self.after_update_path`:
```ruby
# app/avo/resources/comment.rb
class Avo::Resources::Comment < Avo::BaseResource
self.after_create_path = :index
self.after_update_path = :edit
end
```
For more granular control (custom paths, different responses), use the controller methods instead.
If your forms grow tall, add the `Back` and `Save` buttons to the footer too with `config.buttons_on_form_footers`:
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.buttons_on_form_footers = true
end
```
If you use `devise` and update users without passing a password, stop the validation error with `self.devise_password_optional`.
## Tweak the Index view
Display records as a grid or on a map instead of a table with `self.default_view_type`:
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.default_view_type = :grid
end
```
It also takes a block when the choice depends on the request β see the grid view and map view pages for what each type needs.
A few more knobs for the `Index` view:
- Hide the selection checkboxes with `self.record_selector` for resources that will never be selected.
- Keep the filters panel open while users change filter values with `self.keep_filters_panel_open`.
- For STI models, send users who click a parent record to the child record instead with `self.link_to_child_resource`.
```ruby
# app/avo/resources/comment.rb
class Avo::Resources::Comment < Avo::BaseResource
self.record_selector = false
self.keep_filters_panel_open = true
end
```
For resources whose records are slow to load, switch to `self.index_view_loading = :lazy` so the page shell and controls paint immediately and the records stream in through a Turbo Frame:
```ruby
# app/avo/resources/order.rb
class Avo::Resources::Order < Avo::BaseResource
self.index_view_loading = :lazy
end
```
Search, filters, sorting, pagination, and view switches then stay asynchronous inside that frame while the URL keeps in sync. Loading stays eager on association indexes and on resources with a custom index component.
## Record previews
Let users peek at a record from the `Index` view without opening it. Add a `preview` field to the resource and mark the fields you want in the popover with `show_on: :preview`.
## Manage sidebar presence and shortcuts
The auto-generated sidebar lists every resource. Hide the ones users shouldn't navigate to directly with `self.visible_on_sidebar`, and give frequently visited resources a keyboard shortcut with `self.hotkey`:
```ruby
# app/avo/resources/team_membership.rb
class Avo::Resources::TeamMembership < Avo::BaseResource
self.visible_on_sidebar = false
end
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.hotkey = "g p"
end
```
:::info
`visible_on_sidebar` only affects the auto-generated menu. If you use the menu editor, control visibility with its `visible` block instead.
:::
## Link to the record's public page
It's often desirable to give users a link to a record's public path outside the Avo interface. Configure `self.external_link` with a block returning the URL β your app's path helpers are available:
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.external_link = -> {
main_app.post_path(record)
}
end
```
Avo will display an external link button on the record that takes the user to that URL.
## Swap the view components
Each view is rendered by a ViewComponent (`Avo::Views::ResourceIndexComponent`, `ResourceShowComponent`, `ResourceEditComponent`). Replace any of them per resource with `self.components`:
```ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
self.components = {
"Avo::Views::ResourceIndexComponent": Avo::Views::Users::ResourceIndexComponent
}
end
```
The easiest way to create a compatible component is to eject an existing one. The safely override resource components guide walks through the whole process.
## 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
Resources can be namespaced by nesting them in subdirectories of `app/avo/resources`, mirroring the namespace with `::` in the class name. This is handy for grouping resources that belong to a namespaced model (`Galaxy::Planet`) or that you just want organized under a common prefix (`Billing::Invoice`).
```ruby
# app/avo/resources/galaxy/planet.rb
class Avo::Resources::Galaxy::Planet < Avo::BaseResource
self.title = :name
def fields
field :id, as: :id
field :name, as: :text
field :satellites, as: :has_many
end
end
```
If the resource's namespace matches its model's namespace (`Avo::Resources::Galaxy::Planet` β `Galaxy::Planet`), Avo infers the model class automatically β no `self.model_class` needed. If it doesn't, set `model_class` explicitly, same as with a flat resource:
```ruby
class Avo::Resources::SuperDooperTrooperModel < Avo::BaseResource
self.model_class = "Super::Dooper::Trooper::Model"
end
```
Generate a namespaced resource (and its matching namespaced controller) the same way you'd generate a flat one, just with the namespace in the name:
```bash
bin/rails generate avo:resource Galaxy::Planet
```
That creates `app/avo/resources/galaxy/planet.rb` (`Avo::Resources::Galaxy::Planet`) and `app/controllers/avo/galaxy/planets_controller.rb` (`Avo::Galaxy::PlanetsController`). Namespaces can go as deep as you need β `Universe::Cluster::Star::Comet` generates `app/avo/resources/universe/cluster/star/comet.rb`, and so on.
Namespacing also affects the resource's routes and translation key: `Avo::Resources::Galaxy::Planet` gets the route path `galaxy/planets` and the translation key `avo.resource_translations.galaxy/planet`, following the same underscored, slash-joined convention as the class name.
## 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
# config/initializers/avo.rb
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`
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.
Your resource files will still look the same as they did before.
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
# Your existing configuration for the Post resource
end
```
## Modify controls placement and appearance
Configure where row controls appear on the view β placement, floating behavior, and hover visibility β using `row_controls_config`.
See row controls configuration on table view.
## 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 Resource
An Array Resource is a resource backed by in-memory data instead of a database table. Use it to let Avo display and manage structured data that doesn't come from a model. The `records` method can return an array of hashes, an array of Active Record objects, an `ActiveRecord::Relation`, or an array of `StoreModel` instances.
Generate one with the `--array` flag:
```bash
bin/rails generate avo:resource Movie --array
```
The generated class extends `Avo::Resources::ArrayResource`. Return the data from the `records` method and describe it with fields, like any other resource:
```ruby
# app/avo/resources/movie.rb
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
```
Each hash becomes a record, so `field :name` reads the `name:` key and computed fields can call `record.fun_fact` directly. If you return Active Record objects or a relation instead, Avo uses their real model class and fields behave as they do on a regular resource. Pagination works out of the box.
## Render it inside another resource
The Array Resource pairs with the `Array` field to display array data on another resource β `field :attendees, as: :array` on a `Course`, for example. When rendered through the field, `records` is the last fallback in the data-fetching hierarchy; the field's block and the model's method take precedence. See the Array field documentation for the full hierarchy.
:::warning Limitations
- Sorting is not supported.
- Large datasets can be slow β the array is rebuilt on every request. If that becomes a bottleneck, cache the data inside `records`.
:::
:::info Heavier workloads
If your data comes from an external API or the array approach starts to feel limiting, consider an HTTP Resource instead β it's backed by an endpoint and built for that kind of work.
:::
---
# HTTP Resource
An **HTTP Resource** is a resource backed by an HTTP endpoint instead of an Active Record model. Point it at an external API and Avo will list, show, create, update, and delete records through that API β no database table required.
```ruby
# app/avo/resources/author.rb
class Avo::Resources::Author < Avo::Core::Resources::Http
self.http_adapter = {
endpoint: "https://api.openalex.org/authors",
parse_collection: -> { response["results"] },
parse_count: -> { response["meta"]["count"] }
}
def fields
field :id, as: :id
field :display_name
end
end
```
With only `endpoint` configured, Avo assumes the response body *is* the collection (or record) and reads the total count from `response["total"]`. The parsing options exist to adapt APIs that wrap their payloads differently.
## Install the gem
Add `avo-http_resource` to your Gemfile:
```ruby
# Gemfile
gem "avo-http_resource", source: "https://packager.dev/avo-hq/"
```
Then run `bundle install`. The HTTP Resource becomes available as a new resource type.
## Generate a resource
Pass the `--http` flag to the resource generator:
```bash
bin/rails generate avo:resource Author --http
```
## Parse the API response
Most APIs wrap their data β a `results` key for the collection, a `meta` object for counts. Configure the three parsing procs to tell Avo where things live:
- `parse_collection` returns the array of records from the index response.
- `parse_record` returns a single record from the show response.
- `parse_count` returns the total number of records, used for pagination.
```ruby
# app/avo/resources/author.rb
self.http_adapter = {
endpoint: "https://api.openalex.org/authors",
parse_collection: -> { response["results"] },
parse_record: -> { response },
parse_count: -> { response["meta"]["count"] }
}
```
Each proc runs in an `Avo::ExecutionContext` with access to `raw_response` (the `HTTParty::Response`), `response` (the parsed body), and `headers` (the response headers), so you can dig into nested structures or inspect status codes as needed.
:::info Pagination
Avo pages the API for you β index requests always include `page` and `per_page` query parameters.
:::
## Send authentication headers
Provide `headers` to send credentials or any other header with every request:
```ruby
# app/avo/resources/author.rb
self.http_adapter = {
endpoint: "https://api.openalex.org/authors",
headers: {
"Authorization" => "Bearer #{ENV.fetch("API_KEY")}"
}
}
```
If the headers must be computed at request time β rotating tokens, per-user credentials β pass a proc returning the hash instead.
## Map sorting and filtering to query params
If you want Avo's sorting UI (or any UI state) forwarded to the API, build the query string with `query_params`. The proc has access to controller `params`, and its result is merged into the request's query string:
```ruby
# app/avo/resources/author.rb
self.http_adapter = {
endpoint: "https://api.openalex.org/authors",
query_params: -> {
if params[:sort_by].present? && params[:sort_direction].present?
{ sort: "#{params[:sort_by]}:#{params[:sort_direction]}" }
else
{}
end
}
}
```
## Customize the backing model
Avo generates an `ActiveModel` class for each HTTP Resource behind the scenes. Use `model_class_eval` to define extra behavior on it β for example, obfuscating the ID used in URLs:
```ruby
# app/avo/resources/author.rb
self.http_adapter = {
endpoint: "https://api.openalex.org/authors",
model_class_eval: -> {
define_method :to_param do
Base64.encode64(id)
end
}
}
```
## Handle API errors
When the API returns an error, raise `Avo::HttpError` inside any parsing proc:
```ruby
parse_collection: -> {
raise Avo::HttpError.new response["message"] if response["error"].present?
response["results"]
}
```
The controller rescues the exception and displays the message as a flash error in the UI, so users get meaningful feedback instead of a broken page.
## Customize create, update, and destroy
Out of the box, the HTTP controller persists changes through the resource's client β `POST` to the endpoint on create, `PATCH` to `endpoint/:id` on update, and `DELETE` to `endpoint/:id` on destroy. The default implementation looks like this:
```ruby
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
@response = @resource.client.delete(@record.id)
end
```
If your API needs different paths, extra parameters, or conditional logic, override these methods in the resource's controller:
- `save_record` must return a **boolean** indicating whether the operation succeeded.
- Inspect `action_name` (`"create"` or `"update"`) to tell the two operations apart.
```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")}"
}
response = if action_name == "create"
MyCustomApi.post("/authors", body: @record.as_json, headers: auth_headers)
else
MyCustomApi.patch("/authors/#{@record.id}", body: @record.as_json, headers: auth_headers)
end
response.success?
end
end
```
## Debug console
HTTP Resources ship with an interactive debug console for inspecting exactly what your resource sends and receives. Visit `/http-resource/debug` (e.g. `/avo/http-resource/debug`), pick a resource and an action (`index`, `show`, `count`, `create`, `update`, or `delete`), and fire the request.
For each run you can inspect:
- the sent URL, query params, and (masked) request headers
- the raw response alongside the parsed result
- the request timing
- the output of your `parse_collection`, `parse_record`, and `parse_count` blocks
Errors are surfaced inline per stage β a failed request and a failing `parse_*` block are reported separately β so a broken adapter never crashes the page.
The console is gated behind the `avo-http_resource` license feature and Avo's developer/admin access, the same gate as Avo's own debug tools. On top of that, it only lists β and only runs against β resources the current user is authorized to access, honoring each resource's authorization policy. If you use Avo's authorization, a user can never reach a resource from the console that their policy would otherwise hide; if you don't configure authorization, only the developer/admin gate applies.
:::warning
The `create`, `update`, and `delete` actions hit your real external API. From the console they require an explicit confirmation before running.
:::
### Add it to the sidebar
To surface the console as a tool in the sidebar's **Tools** section, add a sidebar item partial to your app at `app/views/avo/sidebar/items/_http_resource_debugger.html.erb`:
```erb
<%= render Avo::Sidebar::LinkComponent.new(
label: "HTTP debugger",
path: File.join(avo.root_path, "http-resource", "debug"),
icon: "tabler/outline/api"
) %>
```
### JSON API
The same diagnostics are available as JSON for scripting or agent use. `POST` to `/http-resource/debug/run.json` with the params `resource`, `probe_action`, `id`, `page`, `limit`, `query`, `body`, and `confirm`. Write actions (`create`, `update`, `delete`) require `confirm=1` since they hit the real external API.
## Full example
Every adapter option at once, against the [OpenAlex](https://openalex.org) API:
```ruby
# app/avo/resources/author.rb
class Avo::Resources::Author < Avo::Core::Resources::Http
self.http_adapter = {
endpoint: "https://api.openalex.org/authors",
parse_collection: -> {
raise Avo::HttpError.new response["message"] if response["error"].present?
response["results"]
},
parse_record: -> {
raise Avo::HttpError.new response["message"] if response["error"].present?
response
},
parse_count: -> { response["meta"]["count"] },
model_class_eval: -> {
define_method :to_param do
Base64.encode64(id)
end
},
headers: {
"Authorization" => "Bearer #{ENV.fetch("API_KEY")}"
},
query_params: -> {
if params[:sort_by].present? && params[:sort_direction].present?
{ sort: "#{params[:sort_by]}:#{params[:sort_direction]}" }
else
{}
end
}
}
def fields
field :id, as: :id
field :display_name
field :cited_by_count, name: "Total citations"
field :works_count, name: "Total works"
end
end
```
---
# Scopes
Sometimes you need to segment your data beyond just a few filters. You might have a `User` resource but frequently need to see all the **Active users** or **Admin users**. You can use a filter for that, or add a scope β a one-click segment rendered as a tab bar above the records.
## Generate a scope
```bash
bin/rails generate avo:scope admins
```
The generator creates a scope class in `app/avo/scopes`. Point its `scope` option to a scope on your model:
```ruby
# app/avo/scopes/admins.rb
class Avo::Scopes::Admins < Avo::Scopes::BaseScope
self.name = "Admins" # Name displayed on the scopes bar
self.description = "Admins only" # This is the tooltip value
self.scope = :admins # A scope on the model this resource uses
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
```
If you'd rather not define a model scope, `scope` also accepts a proc that modifies the query directly.
## Register the scope on a resource
Because scopes are reusable, you must manually add each 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
```
### Set a default scope
Pass `default: true` when registering a scope to apply it when you navigate to the resource's view. It also accepts a proc, so you can pick the default per user:
```ruby{5-6}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def scopes
scope Avo::Scopes::OddId
# EvenId is the default scope only for admins
scope Avo::Scopes::EvenId, default: -> { current_user.admin? }
end
end
```
### Remove the "All" scope
Avo adds an `All` scope by default. If you don't want it β or you'd rather ship a custom "All" scope of your own β call `remove_scope_all` inside the `scopes` method and mark another scope as the default:
```ruby{4-5}
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def scopes
remove_scope_all
scope Avo::Scopes::Everybody, default: true
scope Avo::Scopes::Admins
end
end
```
## Show record counts
To display a count badge next to a scope's label, set `counter` on the scope class:
```ruby{3}
# app/avo/scopes/active.rb
class Avo::Scopes::Active < Avo::Scopes::BaseScope
self.counter = :lazy
end
```
Use `:lazy` on large tables so the count loads after the page paints instead of slowing down the request, or `:hover` to load it only when the user hovers over the scope tab; `true` (or `:eager`) computes it inline. For finer control β a custom count, showing the badge conditionally, or formatting the number β pass a Hash with `count`, `visible`, and `format` keys. The badge isn't limited to numbers: return a String (text or an emoji) from `count` and pass it through with a `format` of just `value`.
```ruby{3-7}
# app/avo/scopes/active.rb
class Avo::Scopes::Active < Avo::Scopes::BaseScope
self.counter = {
loading: :lazy,
count: -> { query.active.count },
format: -> { "#{value} #{resource.name.pluralize.downcase}" }
}
end
```
## Control who sees a scope
Use the `visible` option to show, hide, or authorize a scope per user:
```ruby{4}
# app/avo/scopes/even_id.rb
class Avo::Scopes::EvenId < Avo::Scopes::BaseScope
# Only show this scope to admins
self.visible = -> { current_user.admin? }
end
```
## Dynamic values
Every option accepts a proc instead of a static value, executed using the Avo::ExecutionContext with access to `query`, `resource`, `scope`, and `scoped_query`. For example, a description that adapts to the resource:
```ruby{3-5}
# app/avo/scopes/even_id.rb
class Avo::Scopes::EvenId < Avo::Scopes::BaseScope
self.description = -> {
"Only #{resource.name.downcase.pluralize} that have an even ID"
}
end
```
:::warning Performance note
`scoped_query` executes the scope when called. If the scope is slow, using it inside a proc impacts every page load. To show record counts, prefer the built-in `counter` option over computing them in `name`.
:::
See the execution context reference for what each option's proc has access to.
## Localization
The shortest path is a locale key. Avo resolves `avo.scope_translations..{name,description}` before falling back to the class attribute, the same way it does for resources and actions:
```yaml
# config/locales/avo.sv.yml
sv:
avo:
scope_translations:
admins:
name: AdministratΓΆrer
description: Endast administratΓΆrer
```
```ruby
# app/avo/scopes/admins.rb
class Avo::Scopes::Admins < Avo::Scopes::BaseScope
self.name = "Admins" # the fallback when no translation is present
self.scope = :admins
# Optional. Defaults to avo.scope_translations.admins
# self.translation_key = "avo.scope_translations.admins"
end
```
A namespaced scope uses the slash-joined path β `Avo::Scopes::Admin::Archived` resolves to `avo.scope_translations.admin/archived`.
### Or assign a callable
The class attribute stays available and still resolves per request, which is what you want when the label depends on something the locale file cannot know:
A scope's `name` and `description` accept a callable, and it is resolved on **every render** in the requesting user's locale. That is all it takes to translate a tab bar: assign a lambda that calls `I18n.t`.
```ruby{3-4}
# app/avo/scopes/admins.rb
class Avo::Scopes::Admins < Avo::Scopes::BaseScope
self.name = -> { I18n.t("avo.scopes.admins.name", default: "Admins") }
self.description = -> { I18n.t("avo.scopes.admins.description", default: "Admins only") }
self.scope = :admins
end
```
```yaml
# config/locales/avo.scopes.sv.yml
sv:
avo:
scopes:
admins:
name: AdministratΓΆrer
description: Endast administratΓΆrer
```
The key path is spelled in full above on purpose: the lookups are `avo.scopes.admins.name` and `avo.scopes.admins.description`.
:::info The namespace is yours
Pick a key namespace your app owns. `avo.scopes.*` reads well beside Avo's own keys and is safe β the Scopes gem itself only ships `avo.scopes.record_count` β but nothing requires it. Any namespace works.
:::
:::warning Always pass a String `default:`
Under a locale your app has not translated, a lookup with no `default:` renders `Translation missing: β¦` as the tab label, and Avo does not fall back to English for you unless you have configured `config.i18n.fallbacks`.
:::
A Symbol is **not** resolved as an i18n key. `self.name = :admins` sets the literal label `:admins` β use a callable.
### Translating a label cannot break scope selection
A scope's URL slug comes from its class path through `self.param`, never from `name`. So `?scope=admins` is identical in every locale, and a bookmark made in one language keeps working in another. You can translate `name` freely.
### The cost of a callable
Because it re-resolves per request β which is exactly what makes it follow the locale β anything expensive inside `name` runs on every page load. An `I18n.t` lookup is cheap. A query is not: to show a record count, use the `counter` option rather than computing it in `name`.
### Translating the gem's own chrome
Separately from your tab labels, the Scopes gem renders one string of its own β the accessible name on a [count pill](#show-record-counts) β and it ships **English only**:
```yaml
# config/locales/avo.scopes.sv.yml
sv:
avo:
scopes:
record_count:
one: "%{count} record"
other: "%{count} records"
```
**Keep the `%{count}` placeholder.** The `All` tab's label is core's `avo.default_scope`, which is already translated in every locale Avo ships β override it there, not under `avo.scopes`.
## Limit index columns per scope
A scope normally changes which **records** appear on the index. It can also change which **columns** appear while it's active β only on the view; the show, new, and edit views keep the resource's normal fields.
There are three ways to do it, in order of precedence:
- Define a `fields` method on the scope to declare the exact index columns from scratch, using the same DSL as a resource's `fields`.
- Set `field_whitelist` to keep the resource's fields but show **only** the listed ids.
- Set `field_blacklist` to keep the resource's fields but **hide** the listed ids.
```ruby{5-9}
# app/avo/scopes/published.rb
class Avo::Scopes::Published < Avo::Scopes::BaseScope
self.scope = -> { query.where(published: true) }
def fields
field :id, as: :id
field :title, as: :text
field :published_at, as: :date_time
end
end
```
:::warning Display only
`field_whitelist` and `field_blacklist` only change which columns render on the index. They are **not** an authorization boundary β the data is still loaded and stays visible on the show and edit views and through the API. To actually restrict access, use a policy or a field's `visible` option.
:::
## Full example
```ruby
# app/avo/scopes/even_id.rb
class Avo::Scopes::EvenId < Avo::Scopes::BaseScope
self.name = "Even"
# 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? }
# Show a record count badge, loaded lazily so it won't slow down the page
self.counter = :lazy
end
```
---
# Record reordering
Record reordering lets your users arrange records in a specific order β like reordering `Slide`s inside a `Carousel` or `MenuItem`s inside a `Menu` β using ordering buttons or drag and drop on the view.
Configure it through the `ordering` class attribute on the resource. The `actions` are plain lambdas, so they pair naturally with an ordering gem like [`acts_as_list`](https://github.com/brendon/acts_as_list) or with your own positioning logic.
```ruby
# app/avo/resources/course_link.rb
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
```
Without any `ordering` configuration, no reordering controls are rendered. Note that `visible_on` has no implicit default β if you omit it, the buttons won't show anywhere, so always set it to `:index`, `:association`, or both.
## Set up reordering with `acts_as_list`
Install and configure the gem as instructed in its [readme](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.
Then add the configuration from the snippet above. The `record` inside each action is the instantiated model, and `move_higher`, `move_lower`, `move_to_top`, and `move_to_bottom` are provided by `acts_as_list`. If you're not using that gem, put your own position-changing logic inside the lambdas β each one also has access to `resource`, `options`, and `params`; the full contract is in the `actions` reference.
That configuration generates a button with a popover containing the ordering buttons.
## Always show the order buttons
If the resource you're updating requires reordering often, keep the buttons visible at all times with `display_inline: true`.
```ruby
# app/avo/resources/course_link.rb
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
```
## Choose where the buttons appear
`visible_on` controls which views render the reordering controls.
If you want them on the resource's view, use `:index`:
```ruby{4}
# app/avo/resources/course_link.rb
class Avo::Resources::CourseLink < Avo::BaseResource
self.ordering = {
visible_on: :index,
}
end
```
If the order only makes sense in the scope of a parent record β like ordering `MenuItem`s for a `Menu` or `Slide`s for a `Carousel` β use `:association` so the buttons show up only in the `has_many` association view:
```ruby{4}
# app/avo/resources/course_link.rb
class Avo::Resources::CourseLink < Avo::BaseResource
self.ordering = {
visible_on: :association,
}
end
```
Use `[:index, :association]` to show them in both places.
## Sort the `Index` view by position
Naturally, you'll want the view to list records in their position order. You may do that in two ways:
1. Add a `default_scope` to your model. If you're only using this ordering scheme in Avo, this is not the recommended way β it applies the scope to every query for that model, which you probably don't want.
2. Use `index_query` to alter the query only in Avo.
```ruby{3-5}
# app/avo/resources/course_link.rb
class Avo::Resources::CourseLink < Avo::BaseResource
self.index_query = -> {
query.order(position: :asc)
}
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
```
## Reorder using drag and drop
Sometimes it's easier to just pick up a record and drop it where you'd like it to be. That's exactly what this feature does.
It's disabled by default. To enable it, add `drag_and_drop: true` and an `insert_at` action to the `ordering` hash β both are required for the drag handles to appear.
```ruby{4,10}
# app/avo/resources/course_link.rb
class Avo::Resources::CourseLink < Avo::BaseResource
self.ordering = {
drag_and_drop: true,
display_inline: true,
visible_on: [:index, :association],
actions: {
higher: -> { record.move_higher },
lower: -> { record.move_lower },
to_top: -> { record.move_to_top },
to_bottom: -> { record.move_to_bottom },
insert_at: -> { record.insert_at position }
}
}
end
```
Inside `insert_at`, the `position` local is the target position (an `Integer`) computed from where the record was dropped.
### Custom `position` attribute
To compute the drop target, Avo reads the current position of the first record in the list β by default via `record.position`, which is what `acts_as_list` provides. If your model exposes its position under a different name, point the `position` option at it.
```ruby{4}
# app/avo/resources/course_link.rb
class Avo::Resources::CourseLink < Avo::BaseResource
self.ordering = {
position: -> { record.position_in_list },
drag_and_drop: true,
display_inline: true,
visible_on: [:index, :association],
actions: {
higher: -> { record.move_higher },
lower: -> { record.move_lower },
to_top: -> { record.move_to_top },
to_bottom: -> { record.move_to_bottom },
insert_at: -> { record.insert_at position }
}
}
end
```
## Authorization
If you're using the authorization feature, please ensure you give the proper permissions using the `reorder?` policy method.
```ruby
# app/policies/course_link_policy.rb
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 some information available on the record page, but not necessarily front-and-center. The `discreet_information` resource option renders one or more small items β icons with tooltips, badges, links, or key-value pairs β next to the record title, without using up a whole field.
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.discreet_information = [
:timestamps,
{
as: :badge,
text: -> { record.published_at ? "Published" : "Draft" },
icon: -> { "heroicons/outline/#{record.published_at ? "eye" : "eye-slash"}" }
}
]
# fields and other resource configuration
end
```
By default the option is unset and nothing is rendered. You can pass a single item or an array of items; each item is either a preconfigured symbol or a `Hash` describing a custom item.
## Show the record's ID
Use the `:id` preconfigured item to display the current record's ID as a key-value pair, saving a field slot.
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.discreet_information = :id
end
```
## Show timestamps without using a field
The reason we built this feature was that we wanted a place to display the `created_at` and `updated_at` timestamps without dedicating a whole field to them.
Set the option to `:timestamps` and an icon is added next to the title. Hovering over it reveals both timestamps in a tooltip.
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.discreet_information = :timestamps
end
```
If the record has neither a `created_at` nor an `updated_at` value, the item is omitted.
If you'd rather show a single timestamp as a visible key-value pair instead of a tooltip, use `:created_at` or `:updated_at`.
## Add a custom item
Pass a `Hash` to take full control of an item. Pick the representation with `as` β `:text` (default), `:icon`, `:badge`, or `:key_value` β then set the options that type uses: `text` for text and badge items, `icon`, `title` for the tooltip, or `key` and `value` for key-value items.
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.discreet_information = {
as: :badge,
text: -> { record.published_at ? "Published" : "Draft" },
title: -> { "Product is #{record.published_at ? "published" : "draft"}" },
icon: "tabler/outline/bulb"
}
end
```
Every option accepts a static value or a block. Blocks run in an `ExecutionContext`, so you have access to `record`, `resource`, and the rest of the common objects.
## Turn an item into a link
If you want an item to navigate somewhere, add a `url`. Use `target` to control where it opens and `data` to set data attributes on the item.
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.discreet_information = {
as: :badge,
text: "View on site",
url: -> { main_app.post_path record },
target: :_blank
}
end
```
## Show an item conditionally
Set `visible` to a boolean or a block if an item should only appear in certain conditions.
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.discreet_information = {
as: :badge,
text: "Draft",
visible: -> { record.published_at.nil? }
}
end
```
## Full example
Here's a possible full configuration for the discreet information area, mixing preconfigured and custom items.
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.discreet_information = [
:id,
:timestamps,
:created_at,
:updated_at,
{
as: :badge,
text: "label",
title: -> { sanitize("View #{record.name} on site", tags: %w[strong]) },
icon: -> { "heroicons/outline/arrow-top-right-on-square" },
url: -> { main_app.root_url },
target: :_blank
},
{
as: :text,
text: -> { "Simple text #{record.id}" },
title: -> { sanitize("View #{record.name} on site", tags: %w[strong]) },
icon: -> { "tabler/outline/external-link" },
url: -> { main_app.root_url },
visible: true
},
{
as: :badge,
text: "Test",
visible: false
},
{
as: :key_value,
key: "Key",
value: "Value"
},
{
as: :icon,
icon: "tabler/outline/cube-3d-sphere",
title: -> { Time.now }
}
]
# fields and other resource configuration
end
```
---
# Custom controls
Avo displays a default set of buttons (controls) on the , , and views and at the end of each table row. Custom controls let you take over any of those areas β relabel or remove the default buttons and add your own links, action buttons, and dropdowns.
Take over an area by assigning a block to the matching resource class attribute β `show_controls`, `edit_controls`, `index_controls`, or `row_controls`:
```ruby
# app/avo/resources/fish.rb
class Avo::Resources::Fish < Avo::BaseResource
self.show_controls = -> do
link_to "View on site", "https://fish.com", target: :_blank
default_controls
end
end
```
Any area you don't customize keeps its default controls:
Inside a block you can mix the built-in controls (like `back_button` or `actions_list`) with your own `link_to` links and `action` buttons. See the API reference for every control and the options each one accepts.
## Customize the show controls
On the view the default configuration is `back_button`, `delete_button`, `detach_button`, `actions_list`, and `edit_button`. Assign a `show_controls` block and declare the controls you want, in the order you want them:
```ruby
# app/avo/resources/fish.rb
class Avo::Resources::Fish < Avo::BaseResource
self.show_controls = -> do
back_button label: "", title: "Go back now"
link_to "Fish.com", "https://fish.com", icon: "heroicons/outline/academic-cap", target: :_blank
link_to "Turbo demo", "/admin/resources/fish/#{params[:id]}?change_to=new-content",
class: "custom-class",
data: {
turbo_frame: "fish_custom_action_demo"
}
delete_button label: "", title: "Delete this fish"
detach_button label: "", title: "Detach this fish"
actions_list label: "Runnables", exclude: [Avo::Actions::ReleaseFish], style: :primary, color: :slate
action Avo::Actions::ReleaseFish, style: :primary, color: :fuchsia, icon: "heroicons/outline/globe"
edit_button label: ""
end
end
```
## Customize the edit controls
On the and views the default configuration is `back_button`, `delete_button`, `actions_list`, and `save_button`. Assign an `edit_controls` block β it applies to both views, so use `view` to differentiate when needed:
```ruby
# app/avo/resources/fish.rb
class Avo::Resources::Fish < Avo::BaseResource
self.edit_controls = -> do
back_button label: "", title: "Go back now"
link_to "Fish.com", "https://fish.com", icon: "heroicons/outline/academic-cap", target: :_blank
delete_button label: "", title: "Delete this fish"
detach_button label: "", title: "Detach this fish"
actions_list exclude: [Avo::Actions::ReleaseFish], style: :primary, color: :slate, label: "Runnables"
action Avo::Actions::ReleaseFish, style: :primary, color: :fuchsia, icon: "heroicons/outline/globe" if view != :new
save_button label: "Save Fish"
end
end
```
## Customize the index controls
On the view the default configuration is `attach_button`, `actions_list`, and `create_button`. Assign an `index_controls` block:
```ruby
# app/avo/resources/fish.rb
class Avo::Resources::Fish < Avo::BaseResource
self.index_controls = -> do
link_to "Fish.com", "https://fish.com", icon: "heroicons/outline/academic-cap", target: :_blank
actions_list exclude: [Avo::Actions::DummyAction], style: :primary, color: :slate, label: "Runnables" if Fish.count > 0
action Avo::Actions::DummyAction, style: :primary, color: :fuchsia, icon: "heroicons/outline/globe" if Fish.count > 0
attach_button label: "Attach one Fish"
create_button label: "Create a new and fresh Fish"
end
end
```
## Customize the row controls
At the end of each table row on the view the default configuration is `order_controls`, `show_button`, `edit_button`, `detach_button`, and `delete_button`. Assign a `row_controls` block:
```ruby
# app/avo/resources/fish.rb
class Avo::Resources::Fish < Avo::BaseResource
self.row_controls = -> do
action Avo::Actions::ReleaseFish, label: "Release #{record.name}", style: :primary, color: :blue,
icon: "heroicons/outline/hand-raised" unless params[:view_type] == "grid"
edit_button title: "Edit this Fish now!"
show_button title: "Show this Fish now!"
delete_button title: "Delete this Fish now!", confirmation_message: "Are you sure you want to delete this Fish?"
actions_list style: :primary, color: :slate, label: "Actions" unless params[:view_type] == "grid"
action Avo::Actions::ReleaseFish, title: "Release #{record.name}", icon: "heroicons/outline/hand-raised", style: :icon
link_to "Information about #{record.name}", "https://en.wikipedia.org/wiki/#{record.name}",
icon: "heroicons/outline/information-circle", target: :_blank, style: :icon
end
end
```
The same controls are displayed on the grid view items too β check `params[:view_type]` (as above) to show a control on only one of the two view types.
## Keep the default controls
If you just want to add a link before or after the default controls, you don't have to re-declare them all β call `default_controls` where you want them to appear:
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
self.show_controls = -> do
# This link will be added before all other controls.
link_to "View on site", post_path(record), target: :_blank
default_controls
end
end
```
## What you can put in a control
Beyond the built-in buttons, a control block accepts your own links and action buttons β and because a `link_to` forwards its `data`, `class`, and `target` to the rendered ``, you can drive Turbo and Stimulus straight from a control.
### Links
`link_to` takes a label and any path β an external URL, a Rails path helper, a `mailto:`, whatever you need:
```ruby
self.show_controls = -> do
# External URL in a new tab
link_to "Docs", "https://avohq.io/docs", icon: "heroicons/outline/book-open", target: :_blank
# Rails path helper for the current record
link_to "View on site", post_path(record), icon: "heroicons/outline/globe-alt"
# mailto: link
link_to "Email author", "mailto:#{record.author.email}", icon: "heroicons/outline/envelope"
# Icon-only β the label becomes the tooltip
link_to "Open in Stripe", "https://dashboard.stripe.com/customers/#{record.stripe_id}",
icon: "heroicons/outline/credit-card", style: :icon
default_controls
end
```
### Built-in controls
Relabel, restyle, or reorder the built-in buttons β `back_button`, `edit_button`, `show_button`, `delete_button`, and the rest:
```ruby
self.show_controls = -> do
back_button label: "", title: "Back" # icon-only
edit_button label: "Edit this fish"
delete_button label: "", icon: "heroicons/outline/trash",
confirmation_message: "Delete this fish for good?"
end
```
### Actions
Pull an action out as its own button, pass it `arguments`, and keep the rest in the dropdown:
```ruby
self.show_controls = -> do
# A single action as a button
action Avo::Actions::ReleaseFish, style: :primary, color: :fuchsia, icon: "heroicons/outline/globe"
# Send data to the action
action Avo::Actions::ExportSelection, arguments: { format: :csv }
# Everything else stays in the dropdown
actions_list exclude: [Avo::Actions::ReleaseFish], label: "More"
default_controls
end
```
### Turbo triggers
A `link_to` forwards its `data` hash to the ``, so any `data-turbo-*` attribute works. Load a response into a Turbo Frame, or turn a link into a `POST`/`DELETE` button:
```ruby
self.show_controls = -> do
# Render the response into a Turbo Frame already on the page
link_to "Preview", preview_fish_path(record),
icon: "heroicons/outline/eye",
data: { turbo_frame: "fish_preview" }
# A link that submits as a POST, with a confirmation dialog
link_to "Archive", archive_fish_path(record),
icon: "heroicons/outline/archive-box",
data: { turbo_method: :post, turbo_confirm: "Archive this fish?" }
default_controls
end
```
### JavaScript triggers
The same `data` pass-through lets you wire a control to a Stimulus controller β set the `controller`, `action`, and any value attributes, and Avo renders them on the link:
```ruby
self.show_controls = -> do
link_to "Copy API key", "#",
icon: "heroicons/outline/clipboard",
data: {
controller: "clipboard",
action: "clipboard#copy",
clipboard_text_value: record.api_key
}
default_controls
end
```
Load the controller through your asset pipeline so Stimulus picks it up.
## Group links and actions in a dropdown
If you have too many controls for the bar, group your custom links and actions in a `list` dropdown:
```ruby
# app/avo/resources/fish.rb
class Avo::Resources::Fish < Avo::BaseResource
self.index_controls = -> do
list label: "Custom Index List", icon: "heroicons/outline/cube-transparent", style: :primary, color: :slate, title: "A custom list" do
link_to "Google", "https://google.com", icon: "heroicons/outline/academic-cap"
action Avo::Actions::Sub::DummyAction, icon: "heroicons/outline/globe"
link_to "Fish.com", "https://fish.com", icon: "heroicons/outline/fire", target: :_blank
end
end
end
```
Within the `list` block you can use `link_to`, `action`, and `divider` β the built-in button controls are not allowed there.
## Conditionally show controls
Actions have a `visible` block that controls their visibility, but a control declared with `action` ignores it. Because the controls declaration is a block, use regular `if`/`else` statements instead:
```ruby{6-8}
# app/avo/resources/fish.rb
class Avo::Resources::Fish < Avo::BaseResource
self.show_controls = -> do
back_button label: "", title: "Go back now"
# visibility conditional
if record.released?
action Avo::Actions::ReleaseFish, style: :primary, color: :fuchsia, icon: "heroicons/outline/globe"
end
edit_button label: ""
end
end
```
:::info
The exception is actions declared inside a `list` dropdown β those do respect the action's `visible` block.
:::
---
# Actions
Actions let you run custom operations on one or many records β or no records at all β straight from the UI. Deactivate a user, send a notification, export a CSV, trigger a background job: anything you can express in Ruby can become an entry in the **Actions** dropdown, optionally with a confirmation modal and a form that collects extra input before running.
An action is a plain Ruby class with a `handle` method, registered on one or more resources:
```ruby
# app/avo/actions/toggle_inactive.rb
class Avo::Actions::ToggleInactive < Avo::BaseAction
self.name = "Toggle Inactive"
def handle(query:, **args)
query.each do |record|
record.update! inactive: !record.inactive
end
succeed "Toggled status for #{query.count} records"
end
end
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def actions
action Avo::Actions::ToggleInactive
end
end
```
With no extra configuration the action shows up in the **Actions** dropdown on the and views (it's hidden on by default), asks "Are you sure you want to run this action?" in a confirmation modal, runs `handle` with the selected records, shows a green success notification, and reloads the page.
## What happens when an action runs
1. **Record selection.** On the view the user selects one or more records (bulk); on the view or row controls the current record is the target. [Standalone](#run-an-action-without-records) actions skip this phase.
2. **Trigger.** The user picks the action from the **Actions** dropdown or a custom control.
3. **Confirmation modal** (optional). A modal shows the action's message and any [fields](#collect-input-with-fields) to fill out. The user runs or cancels. Set `confirmation` to `false` to skip this phase and execute immediately.
4. **Execution.** Your [`handle`](#write-the-handle-method) method runs with the records, field values, current user, and request. You give [feedback](#give-feedback-to-the-user) and pick a [response](#control-what-happens-after-execution).
## Generate an action
```bash
bin/rails generate avo:action toggle_inactive
```
This creates `app/avo/actions/toggle_inactive.rb` with a commented-out skeleton for `visible`, `fields`, and `handle`. Pass `--standalone` to generate an action that doesn't need selected records, and use a namespace to group related actions:
```bash
# Standalone action
bin/rails generate avo:action generate_monthly_report --standalone
# Namespaced action -> app/avo/actions/admin/approve_user.rb
bin/rails generate avo:action admin/approve_user
```
## Register the action on a resource
Declare the actions available on a resource inside its `actions` method:
```ruby
# 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 label: "Communication"
action Avo::Actions::SendWelcomeEmail, icon: "heroicons/outline/envelope"
end
end
```
Pass `icon` to show an icon next to the action in the dropdown, and use `divider` to separate actions into logical groups, with an optional label.
If you want to send custom data to the action, pass `arguments` β a hash (or a proc that returns one) that becomes available everywhere in the action class:
:::code-group
```ruby [app/avo/resources/user.rb]
class Avo::Resources::User < Avo::BaseResource
def actions
action Avo::Actions::ToggleInactive,
arguments: {
special_message: true
}
# Or as a proc to make it dynamic
action Avo::Actions::ToggleInactive,
arguments: -> do
{
special_message: resource.view.index? && current_user.is_admin?
}
end
end
end
```
```ruby [app/avo/actions/toggle_inactive.rb]
class Avo::Actions::ToggleInactive < Avo::BaseAction
def handle(**args)
if arguments[:special_message]
succeed "I love π₯"
else
succeed "Success response βοΈ"
end
end
end
```
:::
:::warning
When using Pundit policies, access to actions is restricted with the `act_on?` method. If you think you should see an action and you don't, check the policy. More info here.
:::
:::info
You may use the custom controls feature to show actions outside the dropdown, as standalone buttons.
:::
## Collect input with fields
An action can define fields, shown to the user in the action's modal. They work the same way as fields on resources. When the action runs on a single record the fields are hydrated from that record; otherwise they're plain form inputs. The submitted values arrive in `handle` as the `fields` argument.
```ruby
# app/avo/actions/toggle_inactive.rb
class Avo::Actions::ToggleInactive < Avo::BaseAction
def fields
field :notify_user, as: :boolean
field :message, as: :textarea
end
end
```
Check out the Fields page for everything fields can do.
## Write the `handle` method
`handle` is where your business logic lives. It receives keyword arguments:
- `query` β the selected record(s); a single record is wrapped in an array for consistency (`records` is an alias)
- `fields` β the values submitted through the action's form fields
- `current_user` β the currently authenticated user
- `resource` β the Avo resource that triggered the action
- `request` β the current `ActionDispatch::Request`
```ruby
# 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:, request:, **args)
query.each do |record|
record.update! inactive: !record.inactive
record.notify(fields[:message]) if fields[:notify_user]
end
succeed "Successfully toggled status for #{query.count} records"
end
end
```
## Give feedback to the user
After the action runs, respond with one or more notifications: `succeed` (green), `inform` (blue), `warn` (orange), or `error` (red). With no explicit feedback, Avo shows an "Action ran successfully" info notification.
```ruby
def handle(**args)
succeed "Success response βοΈ"
warn "Warning response βοΈ"
inform "Info response βοΈ"
error "Error response βοΈ"
end
```
Each method takes an optional `timeout` in milliseconds β or `:forever` to keep the notification open until the user dismisses it:
```ruby
succeed "Task completed successfully", timeout: 5000
warn "Important warning - requires attention", timeout: :forever
```
If you don't want any notification β a redirect scenario, for example β use `silent`.
## Control what happens after execution
Besides feedback, `handle` picks how the UI responds. The default is a full-page `reload`. You can instead:
- `redirect_to` a different path
- `download` a generated file
- `keep_modal_open` to show errors while preserving the user's input
- `close_modal` (alias `do_nothing`) to close the modal and leave the page as is
- `reload_records` to refresh only the affected table rows or grid cards
- `navigate_to_action` to chain into another action
- `append_to_response` to add your own turbo stream responses
See the API reference for each response's behavior and signature.
### Build a multi-step flow
Because `navigate_to_action` can pass `arguments` along, you can chain actions into a wizard: the first action collects which fields to update, the second renders only those fields and performs the update.
:::code-group
```ruby[PreUpdate]
class Avo::Actions::City::PreUpdate < Avo::BaseAction
self.name = "Update"
def fields
field :name, as: :boolean
field :population, as: :boolean
end
def handle(query:, fields:, **args)
navigate_to_action Avo::Actions::City::Update,
arguments: {
cities: query.map(&:id),
render_name: fields[:name],
render_population: fields[:population]
}
end
end
```
```ruby[Update]
class Avo::Actions::City::Update < Avo::BaseAction
self.name = "Update"
self.visible = -> { false }
def fields
field :name, as: :text if arguments[:render_name]
field :population, as: :number if arguments[:render_population]
end
def handle(fields:, **args)
City.find(arguments[:cities]).each do |city|
city.update! fields
end
succeed "City updated!"
end
end
```
:::
You can try this flow on the [avodemo](https://main.avodemo.com/avo/resources/cities): select a record, run the "Update" action, choose the fields, and update them in the follow-up action.
## Customize the modal
The modal's texts can each be a string or a block β blocks run in `Avo::ExecutionContext` with access to `resource`, `record`, `view`, `arguments`, and `query`:
- `name` β the action's display name in the dropdown and modal
- `description` β a short explanation under the name in the modal header
- `message` β the confirmation message
- `confirm_button_label` / `cancel_button_label` β the button labels
```ruby
# app/avo/actions/release_fish.rb
class Avo::Actions::ReleaseFish < Avo::BaseAction
self.name = "Release fish"
self.description = "Release the fish back into the ocean"
self.message = -> {
record.present? ? "Are you sure you want to release #{record.name}?" : "Are you sure you want to release the fish?"
}
self.confirm_button_label = "Release fish"
self.cancel_button_label = "Cancel release"
end
```
If the action is safe to run without asking, set `confirmation` to `false` and it executes immediately on trigger. To stop the modal from closing when the user clicks outside it, set `close_modal_on_backdrop_click` to `false`.
## Run an action without records
Standalone actions aren't tied to selected records β global reports, maintenance tasks, background jobs. They stay enabled in the **Actions** dropdown even with nothing selected and can live alongside regular actions on the same resource.
Generate one with the `--standalone` flag, or set the attribute on an existing action:
```ruby
# app/avo/actions/global_report.rb
class Avo::Actions::GlobalReport < Avo::BaseAction
self.name = "Generate Global Report"
self.standalone = true
end
```
:::tip
Standalone actions pair well with [fields](#collect-input-with-fields) to collect the input the operation needs.
:::
## Control visibility and authorization
By default actions show on every view except . Use `visible` to change where an action appears β for example, only on the view:
```ruby
# app/avo/actions/global_report.rb
class Avo::Actions::GlobalReport < Avo::BaseAction
self.visible = -> { view.index? }
end
```
To restrict *who* can run an action, use `authorize`. Unauthorized actions are hidden, and the controller re-evaluates the check on execution, so a bad actor can't run one by crafting a request:
```ruby
class Avo::Actions::GlobalReport < Avo::BaseAction
self.authorize = -> { current_user.is_admin? }
end
```
Both accept a boolean or a block executed in `Avo::ExecutionContext`.
## Trigger an action from a link
You may want a link somewhere in the UI β a field on the view, a dashboard card β that opens an action's modal directly. Call the action class's `link_arguments` method with a resource instance; it returns the `[path, data]` pair a proper action link needs:
```ruby
# app/avo/resources/city.rb
class Avo::Resources::City < Avo::BaseResource
def fields
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
end
```
If you don't have an instantiated resource at hand β a custom partial on a dashboard, say β build one yourself, with or without a record:
```ruby
path, data = Avo::Actions::City::Update.link_arguments(
resource: Avo::Resources::City.new(record: city)
)
link_to "Update city", path, data: data
```
The `arguments` are encrypted and Base64-encoded before being put in the URL, so it's safe to pass sensitive data.
## Read params from the page that triggered the action
The action's modal is rendered in a new request, so query params present on the or page aren't directly available. To read them β for example to prefill a field β parse `request.referer`:
```ruby
# app/avo/actions/dummy_action.rb
class Avo::Actions::DummyAction < Avo::BaseAction
def fields
field :some_field, as: :hidden, default: -> {
# Parse the URL of the page that triggered the action
parent_params = URI.parse(request.referer).query
.split("&")
.map { |param| param.split("=") }
.to_h
.with_indifferent_access
(parent_params[:hey] == "ya") ? :yes : :no
}
end
end
```
With this in place, visiting `https://example.com/avo/resources/users?hey=ya` and running the action sets the field's default based on the `hey` param.
## StimulusJS
Actions play well with custom Stimulus controllers. Follow the JavaScript guide for more information.
---
# 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).
---
# Cover and avatar
It's common for a record to have a visual representation. A user might have a headshot, a company might have a logo, or a product might have an image. Avo can display these in two ways:
- the **avatar** (`self.avatar`) β a small photo shown on the and views and in the breadcrumbs
- the **cover** (`self.cover`) β a large banner image displayed at the top of the record
## Add an avatar
Use the `avatar` option. It takes two arguments: [`visible_on`](#visible_on) and [`source`](#source).
```ruby
# app/avo/resources/user.rb
self.avatar = {
visible_on: [:show, :forms],
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 its avatar
record.avatar
end
}
}
```
When no avatar is configured (or the source is blank), Avo falls back to the record's initials in places like the breadcrumbs.
To also show the avatar as a column on the view, add the avatar field to the resource.
## Add a cover photo
Use the `cover` option. It takes the same [`visible_on`](#visible_on) and [`source`](#source) arguments, plus a [`size`](#size).
You can point it to an Active Storage field or a custom path.
```ruby
# app/avo/resources/post.rb
self.cover = {
size: :md, # :sm, :md, :lg, or :full
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 its cover photo
record.cover_photo
end
}
}
```
:::warning Renamed in Avo 4
These options were called `profile_photo` and `cover_photo` in Avo 3. See the upgrade guide.
:::
## Options
Both `avatar` and `cover` accept `visible_on` and `source`. `size` applies only to `cover`.
---
# Views
The Avo CRUD feature generates four main views for each resource.
## Index
The page where you see all your resources listed in a table, grid, or map.
Click row to view recordResource custom components
## Show
The page where you see one resource in more detail.
Resource custom components
## Edit
The page where you can edit one resource.
Resource custom components
## New
The page where you can create a new resource.
Resource custom components
## View groups
Three named groups let you target several views at once:
### Display
`:display` is an alias for the `Index` and `Show` views where you can display records and their details.
### Form
`:form` is an alias for the `Edit` and `New` views for creating and editing records. The `create` and `update` requests that submit those forms also count as form views.
### Single
`:single` is an alias for every view except `Index` β that is `Show`, `Edit`, and `New` (plus the `create` and `update` form submissions).
## Preview
The fields marked with `show_on: :preview` will be shown 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
```
:::
## Common `visible_on` configuration values
In many parts of the DSL you'll be able to configure visibility through options like `visible_on`, `show_on`, `hide_on`, `only_on`, or `except_on`. These options control the views where the configuration applies.
You may use the following values:
- `:show`
- `:edit`
- `:new`
- `:index`
- `:forms` β expands to `:new` and `:edit`
- `:display` β expands to `:index` and `:show`
You may also use a combination of views using an array.
- `[:show, :index]`
- `[:show, :forms]`
Field visibility options (`show_on`, `hide_on`, `only_on`, `except_on`) additionally accept `:preview` β see [Preview](#preview) above.
## View types
The view can render records through several view types, each with its own options, documented on its own page:
- Table view β the default tabular layout; row controls placement and per-row styling.
- Grid view β card-based layout with cover, title, body, and badge, for image-heavy resources.
- Map view β plot records with geospatial data on a map.
- Custom view types β register entirely new view types from a plugin.
### Restrict the available view types
By default, Avo displays all the configured view types on the view switcher. For example, if you have `map_view` and `grid_view` configured, both of them, along with the `table_view`, will be available on the view switcher.
However, there might be cases where you only want to make a specific view type available without removing the configurations for other view types. This can be achieved using the `view_types` class attribute on the resource. Note that when only one view type is available, the view switcher will not be displayed.
```ruby{4}
# app/avo/resources/city.rb
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{4}
# app/avo/resources/city.rb
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{4-10}
# app/avo/resources/city.rb
class Avo::Resources::City < Avo::BaseResource
# ...
self.view_types = -> do
if current_user.is_admin?
[:table, :grid]
else
:table
end
end
# ...
end
```
The current pick is persisted in the URL as the `view_type` query parameter, so it survives page reloads and can be bookmarked.
:::warning
Requesting a view type that isn't in the available list raises an error, and rendering a view type that was never registered raises `Avo::ViewTypeComponentNotFoundError`. Keep `view_types`, `default_view_type`, and your registered view types in sync.
:::
---
# Views
The Avo CRUD feature generates four main views for each resource.
## Index
The page where you see all your resources listed in a table, grid, or map.
Click row to view recordResource custom components
## Show
The page where you see one resource in more detail.
Resource custom components
## Edit
The page where you can edit one resource.
Resource custom components
## New
The page where you can create a new resource.
Resource custom components
## View groups
Three named groups let you target several views at once:
### Display
`:display` is an alias for the `Index` and `Show` views where you can display records and their details.
### Form
`:form` is an alias for the `Edit` and `New` views for creating and editing records. The `create` and `update` requests that submit those forms also count as form views.
### Single
`:single` is an alias for every view except `Index` β that is `Show`, `Edit`, and `New` (plus the `create` and `update` form submissions).
## Preview
The fields marked with `show_on: :preview` will be shown 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
```
:::
## Common `visible_on` configuration values
In many parts of the DSL you'll be able to configure visibility through options like `visible_on`, `show_on`, `hide_on`, `only_on`, or `except_on`. These options control the views where the configuration applies.
You may use the following values:
- `:show`
- `:edit`
- `:new`
- `:index`
- `:forms` β expands to `:new` and `:edit`
- `:display` β expands to `:index` and `:show`
You may also use a combination of views using an array.
- `[:show, :index]`
- `[:show, :forms]`
Field visibility options (`show_on`, `hide_on`, `only_on`, `except_on`) additionally accept `:preview` β see [Preview](#preview) above.
## View types
The view can render records through several view types, each with its own options, documented on its own page:
- Table view β the default tabular layout; row controls placement and per-row styling.
- Grid view β card-based layout with cover, title, body, and badge, for image-heavy resources.
- Map view β plot records with geospatial data on a map.
- Custom view types β register entirely new view types from a plugin.
### Restrict the available view types
By default, Avo displays all the configured view types on the view switcher. For example, if you have `map_view` and `grid_view` configured, both of them, along with the `table_view`, will be available on the view switcher.
However, there might be cases where you only want to make a specific view type available without removing the configurations for other view types. This can be achieved using the `view_types` class attribute on the resource. Note that when only one view type is available, the view switcher will not be displayed.
```ruby{4}
# app/avo/resources/city.rb
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{4}
# app/avo/resources/city.rb
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{4-10}
# app/avo/resources/city.rb
class Avo::Resources::City < Avo::BaseResource
# ...
self.view_types = -> do
if current_user.is_admin?
[:table, :grid]
else
:table
end
end
# ...
end
```
The current pick is persisted in the URL as the `view_type` query parameter, so it survives page reloads and can be bookmarked.
:::warning
Requesting a view type that isn't in the available list raises an error, and rendering a view type that was never registered raises `Avo::ViewTypeComponentNotFoundError`. Keep `view_types`, `default_view_type`, and your registered view types in sync.
:::
---
# 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.
With no configuration, every resource renders as a table with the row controls (show, edit, delete, actions) on the right side of each row and standard row styling. Two hooks let you customize it: [row controls placement](#row-controls) and [`row_options`](#style-rows-with-row_options) for per-row HTML attributes.
## Row controls
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, float them over the row, or reveal them only on hover.
### Global configuration
If you want to change the defaults for every resource, set `resource_row_controls_config` in the initializer:
```ruby
# config/initializers/avo.rb
Avo.configure do |config|
config.resource_row_controls_config = {
placement: :left,
float: true,
show_on_hover: true
}
end
```
### Resource configuration
If a single resource needs different behavior, set `row_controls_config` on it β it overrides the global configuration for that resource:
```ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
self.row_controls_config = {
placement: :left
}
end
```
Both accept the same keys β `placement`, `float`, and `show_on_hover`. See the API reference for values and defaults.
:::warning
`float` and `show_on_hover` are designed to work with `placement: :right`. Avo doesn't restrict other placements, but the applied styles are intended for `:right` and unexpected behavior may occur with `:left` or `:both`.
:::
## Style rows with `row_options`
`self.table_view = { row_options: { ... } }` lets you declaratively set HTML attributes on the `
## Scope which fields are discovered
If you only want a few fields discovered, pass `only`. To discover everything except a few, pass `except`.
```rb{6-7}
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
# ...
def fields
discover_columns only: [:title, :body, :published_at]
discover_associations except: [:audit_logs]
end
end
```
:::info
Rich text bodies and tags are discovered by `discover_columns`, while attachments and regular associations are discovered by `discover_associations`. Scope them from the matching method β `discover_associations only: [:tags]` has no effect.
:::
## Pass options to every discovered field
Any other keyword argument is forwarded to every discovered field. This is useful when you'd otherwise repeat the same option 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
```
## Combine manual and discovered fields
Discovery plays nicely with explicit declarations β fields render in the order they're defined. Use `except` to avoid declaring a field twice.
```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
```
## Override how columns map to fields
Avo decides each column's field type by checking, in order: the model's Rails enums, the column's name, then the column's database type. You can override the last two globally from the initializer.
If a column *name* should always get a specific field, add it to `column_names_mapping`:
```rb{5-8}
# config/initializers/avo.rb
Avo.configure do |config|
# ...
config.column_names_mapping = {
published_at: {field: :date_time},
body: {field: :markdown}
}
end
```
If a database *type* should map to a different field, use `column_types_mapping`:
```rb{5-8}
# config/initializers/avo.rb
Avo.configure do |config|
# ...
config.column_types_mapping = {
jsonb: {field: :code, language: "json"},
decimal: {field: :number, decimals: 2}
}
end
```
Both merge on top of Avo's built-in mappings, so you only list the entries you want to change. Everything besides the `field` key is passed to the field as options.
---
# Fields layout
Avo gives you a composable DSL to control how fields are arranged on resource show and edit pages. You nest fields inside panels, split a panel into a main area and a sidebar, group panels under tabs, and control where the resource header appears β all from the `fields` method.
```ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
field :id, as: :id
field :name, as: :text
field :email, as: :text
end
end
```
With no explicit structure, Avo wraps your root-level fields in a computed **main panel** and places a **header** at the top automatically β so a resource looks right before you touch any layout DSL.
## Layout building blocks
| Block | Purpose | Nesting |
| --------- | -------------------------------------------------------------------- | --------------------------- |
| `header` | Title, description, profile photo, discreet information and controls | Root level |
| `panel` | Groups related fields inside a titled container | Root level or inside `tabs` |
| `card` | Lightweight grouping β useful for sectioning fields visually | Root, panel, sidebar or tab |
| `sidebar` | Narrow column for compact fields (boolean, date, badgeβ¦) | Inside a panel |
| `tabs` | Tabbed container that switches between panels and tools | Root level |
As a resource grows, declare the structure explicitly to get full control:
```ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
tool Avo::ResourceTools::UserTool
header # render the header only if you want to add a tool or card above it
panel title: "User information", description: "Some information about this user" do
field :id, as: :id, link_to_record: true
field :first_name, as: :text
field :last_name # omitting the `as:` option renders the field as :text
sidebar do # works only inside a panel
card do
field :active, as: :boolean, only_on: :show
field :created_at, as: :date_time, only_on: :show
end
end
end
tabs do
tab title: "Projects" do
field :projects, as: :has_many
end
field :teams, as: :has_many # some fields (associations) have their own wrappers and we don't need to wrap them in a tab
tab title: "Settings" do
field :role, as: :select, enum: ::User.roles
field :verified, as: :boolean
end
end
end
end
```
## Header
Every resource page gets a header for free: the band of chrome at the top holding the cover image, title, description, avatar, discreet info, and the control buttons (edit, save, delete, actions, back). If you never mention `header`, Avo builds one and pins it to the very top of the page β so you don't declare it unless you want it somewhere else.
Reach for the `header` DSL only when that default top placement is wrong β for example, to slip an intro card, resource tool, or panel above the header, or to sit the header between two panels. Declaring `header` yourself opts out of the automatic one, and it renders exactly where you place it in `fields`:
```ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
self.title = :name
def fields
card do
field :status, as: :badge
end
header # the page header now renders below the card instead of at the top
panel do
field :id, as: :id
field :email, as: :text
end
end
end
```
`header` is a positioning marker, not a content hook: it takes no options, and everything it shows (title, description, cover, avatar, controls) comes from the resource itself. To customize what appears, set the resource's `title`, `description`, cover, and so on β not the `header` call.
:::info
Don't confuse `header` with `heading`. `header` is the page-level chrome positioned here; `heading` is a field type for inline section titles inside a panel or form. When a resource is embedded in a modal, the header is dropped from the edit view β its title and controls move into the modal's own chrome.
:::
## Group fields into panels
Panels are the backbone of Avo's display β most information on a page lives inside one. When you don't declare any panels, Avo groups root-level fields into computed panels for you; add your own `panel` blocks to group related fields under a title and description.
```ruby
# app/avo/resources/user.rb
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 title: "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
```
Set `title` and `description` to label a panel, and `visible` to show or hide a whole panel and its children at once. Use `card` blocks to section fields visually without opening a new panel.
### How Avo computes panels
Avo organizes panels behind the scenes so you don't have to. As long as you haven't declared a `panel` of your own, it splits your fields into those that carry their own panel (most associations, like `field :users, as: :has_many`) and "standalone" fields, preserving declaration order, then wraps each standalone group in a computed panel. A single run of standalone fields becomes the **main panel**:
```ruby
def fields
field :id, as: :id
field :name, as: :text
field :user, as: :belongs_to
field :type, as: :text
end
```
Insert a field that owns its panel (like a `has_many`) between standalone fields and Avo splits them: each standalone group β before and after the association β is wrapped in its own computed panel.
```ruby{5}
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
```
To group fields under your own titled container, add a `panel` block. Declaring a panel yourself turns the automatic computing off β Avo takes it as a sign you're in control of the layout.
### Index view fields
The **Index** table builds its columns from root-level fields and from fields inside root-level `panel` and `card` blocks alike. Only `sidebar` contents and fields inside tabs stay off the table. To keep a panel's fields out of the Index view, hide the whole panel there with `except_on: :index` β or hide individual fields the same way.
```ruby{11}
class Avo::Resources::User < Avo::BaseResource
def fields
# Visible on Index
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
# Hidden on Index
panel title: "User information", except_on: :index 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
```
## Move compact fields to a sidebar
Some fields β booleans, dates, badges β don't need the full width of the main area. Put them in a `sidebar` block, declared inside a panel, to stack them in a narrower column beside the main content. Standalone fields are auto-wrapped in a card β declare a `card` yourself only to title the group β and each panel can have its own sidebar.
```ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
panel do
field :id, as: :id, link_to_record: true
field :first_name, as: :text, placeholder: "John"
field :last_name, as: :text, placeholder: "Doe"
# Custom resource tools work here too
tool UserTimeline
sidebar do
card 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
end
```
Sidebar fields are always stacked β label above value β because the narrower column requires it.
## Organize fields under tabs
When a resource grows past what panels comfortably hold, group panels and tools under `tabs`. Open a `tabs` block, then add `tab` blocks that contain fields exactly as you would at the root. Standalone fields are auto-wrapped, so a `panel` or `card` inside a tab is **optional** β add one only when you want a title or description on that group. Association fields (`has_one`, `has_many`, `has_and_belongs_to_many`) bring their own panels.
```ruby
# app/avo/resources/user.rb
class Avo::Resources::User < Avo::BaseResource
def fields
field :id, as: :id, link_to_record: true
field :email, as: :text, name: "User Email", required: true
tabs do
tab title: "User information", description: "Some information about this user" do
# No panel needed β these fields are auto-wrapped
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
field :teams, as: :has_and_belongs_to_many
field :people, as: :has_many
field :projects, as: :has_and_belongs_to_many
end
end
end
```
The tab `title` is mandatory and labels the switcher; the `description` shows as a tooltip on hover. Both `tabs` groups and individual `tab`s accept a `visible` boolean or lambda, and the whole group takes its own `title` and `description`. Add a `badge` to a tab β a string or a renderable like `Avo::UI::CountComponent` β to show a count or status pill next to its label. Tab and panel titles also localize through the resource's translation key.
### Loading behavior on Show and Edit
On the **Show** page, `has_many`-type fields and tools inside tabs lazy-load only when their tab is displayed, keeping the initial page light. For heavy tabs you'd rather not fetch on every view, set `loading: :manual` to render a **Load** button and defer the fetch until the user asks for it. A tab's own content renders eagerly by default; set `lazy_load` to defer it until the tab is first revealed.
On **Edit**, `has_*` fields stay hidden by default (add `show_on: :edit` to reveal them); all other fields load and hide so form validations on fields in an inactive tab still fire on submit.
### Durable and bookmarkable selection
Tab selection is durable across view changes β each tab group remembers its active tab β and bookmarkable, so a link can carry a specific tab. Both rely on a unique tab-group ID; without one Avo falls back to the group's title, then to its position, so assign an `id` to keep links stable as groups get renamed or reordered:
```ruby {1}
tabs id: :some_random_uniq_id do
field :posts, as: :has_many, show_on: :edit
end
```
To add a record-count badge to association tabs, see the tabs counter indicator recipe.
## Position labels: inline vs. stacked
Every field has two layout modes for how its label sits relative to its value. **Inline** (default) places them side by side; **stacked** puts the label above the value, giving it the full width β handy for wide fields like `key_value`, `trix`, `code`, or `markdown`.
```ruby
field :meta, as: :key_value, stacked: true
```
Fields inside a `sidebar` are stacked automatically. To stack every field across the app, set `config.field_wrapper_layout = :stacked`. See `stacked` in the field options reference and global stacked layout in the guide for the full details.
## Multi-column rows with `width`
To place fields on the same row, pass a `width` percentage to each β adjacent fields below `100` sit side by side.
```ruby
field :first_name, width: 50
field :last_name, width: 50
```
Supported values are `25`, `33`, `50`, `66`, `75`, and `100` (default). See `width` in field options for the full reference.
---
# Area
The `Area` field is used to display a geographical area on a map.
```ruby
field :city_center_area, as: :area
```
:::warning
You need to add the `mapkick-rb` (not `mapkick`) gem to your `Gemfile` and have the `MAPBOX_ACCESS_TOKEN` environment variable with a valid [Mapbox](https://account.mapbox.com/auth/signup/) key.
:::
## Description
The field reads a [GeoJSON](https://geojson.org) `coordinates` array from one database column (a `json` column works great) and renders it as an interactive area map on the view.
```ruby
# Sample value for a polygon
[[[2.3342, 48.8674], [2.3396, 48.8600], [2.3253, 48.8567], [2.3245, 48.8639], [2.3342, 48.8674]]]
```
On the view you'll get a text input where you can edit the coordinates as JSON.
The field is hidden on the view by default.
## Options
## Full example
```ruby
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"
}
```
---
# Array
The `Array` field 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
bin/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{4-9}
# app/avo/resources/course.rb
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
# app/models/course.rb
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:
```ruby
# app/avo/resources/attendee.rb
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 the `Array` field, Avo will fetch data in the following order:
1. Use data returned by the **block** provided in the field.
2. Fetch data from the **associated model method** (e.g., `Course#attendees`).
3. Fall back to the **`records` method** defined in the resource.
This hierarchy provides maximum flexibility and ensures seamless integration with both dynamic and predefined datasets.
---
# Avatar
The `Avatar` field is a field that displays a user's avatar or initials.
```ruby
field :avatar, as: :avatar
```
It does not take any option and is visible only on the view.
---
# Badge
The `badge` field is used to display an easily recognizable status of a record.
```ruby
field :status, as: :badge,
options: {
success: "Done",
danger: "Cancelled",
warning: "On hold",
green: "In review",
purple: "Idea"
}
```
## Description
The Badge field displays a colored indicator with optional icons. You can customize the color through the `options` mapping, and the `style` and `icon` for each value dynamically using procs.
The `Badge` field is intended to be displayed only on **Index** and **Show** views. To update the value shown by the badge field, use another field like Text or Select with `hide_on: [:index, :show]`.
## Options
## Examples
### Using semantic colors
```ruby
field :status, as: :badge,
options: {
success: ["active", "completed"],
info: ["pending", "review"],
danger: ["failed", "cancelled"],
neutral: ["unknown"]
}
```
### Using base colors
```ruby
field :priority, as: :badge,
options: {
green: :low,
amber: :medium,
orange: :high,
red: :urgent
}
```
### Using Badge with a Select field for editing
Since Badge is display-only, pair it with a Select field to allow editing:
```ruby
field :stage,
as: :select,
hide_on: [:show, :index],
options: {
'Discovery': :discovery,
'Idea': :idea,
'Done': :done,
'On hold': 'on hold',
'Cancelled': :cancelled,
'Drafting': :drafting
},
placeholder: 'Choose the stage.'
field :stage,
as: :badge,
options: {
info: ["Discovery", "Idea"],
success: :Done,
warning: "On hold",
danger: "Cancelled",
neutral: :Drafting
},
style: -> { ["Done", "Cancelled"].include?(record.stage) ? "solid" : "subtle" },
icon: -> {
{
"Discovery" => "tabler/outline/zoom",
"Idea" => "tabler/outline/bulb",
"Drafting" => "tabler/outline/file-text",
"Done" => "tabler/outline/circle-check",
"On hold" => "tabler/outline/player-pause",
"Cancelled" => "tabler/outline/xbox-x"
}[record.stage]
}
```
---
# Boolean
The `Boolean` field renders a `input[type="checkbox"]` on **Form** views and a nice green `check` icon/red `X` icon on the **Show** and **Index** views.
```ruby
field :is_published,
as: :boolean,
name: 'Published',
true_value: 'yes',
false_value: 'no'
```
## Options
---
# 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"
}
```
## Options
## Updates
Avo only updates the keys that you send from the client. Other keys in the stored hash are left unchanged.
```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"
}
```
---
# Checkbox List
The `CheckboxList` field renders a list of checkboxes for selecting multiple values from a finite set of options.
Use it when the available options are already known and users should see the choices directly instead of opening a select.
```ruby
field :team_member_ids,
as: :checkbox_list,
options: -> {
User.active.order(:name).map do |user|
{
id: user.id,
title: user.name,
avatar_url: user.avatar_url,
image_format: :circle,
description: user.email
}
end
},
inline_search: true
```
The field submits an array of selected option ids. This works well with Rails collection writers like `team_member_ids=`.
## Options
---
# 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 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
---
# 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
```
Avo detects the attachment type via Active Storage and renders images, audio, and video accordingly.
## 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: '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.
---
# Lexxy
```ruby
field :body, as: :lexxy
```
The `Lexxy` field renders [Lexxy](https://github.com/basecamp/lexxy), Basecamp's modern rich text editor for Action Text, built on Meta's Lexical framework. It produces clean HTML semantics, supports Markdown shortcuts, code syntax highlighting, and rich attachment previews.
It supports [Action Text](https://guides.rubyonrails.org/action_text_overview.html) attributes and plain `string`/`text` columns storing HTML, with file attachments uploaded through [Active Storage](https://guides.rubyonrails.org/active_storage_overview.html) direct uploads.
Lexxy field is hidden from the `Index` view.
## Requirements
- Rails >= 8.0.2 (required by the `lexxy` gem)
:::info Add this field to the `Gemfile`
```ruby
# Gemfile
gem "avo-lexxy_field"
```
:::
:::warning Lexxy takes over Action Text
The `lexxy` gem replaces Trix as the default `form.rich_text_area` editor across your whole app. If you only want Lexxy inside Avo, opt out in the host app:
```ruby
# config/application.rb (Rails 8.0/8.1)
config.lexxy.override_action_text_defaults = false
```
:::
## Options
## Action Text
Lexxy is built for Action Text and works with it out of the box, including file attachments.
```ruby
class Post < ApplicationRecord
has_rich_text :body
end
```
```ruby
field :body, as: :lexxy
```
## Media Library
When the Media Library is enabled, the editor's toolbar gets a button that opens the library in a modal. Picking an asset inserts it into the content β images as Action Text attachments, other files as links.
The button is hidden when the field has attachments disabled (which is the default on plain `string`/`text` columns), since there is nothing to attach the blob to.
## Editor configuration
Lexxy's element attributes (`markdown`, `rich-text`, `headings`, `permitted-attachment-types`, etc.) are documented in the [Lexxy docs](https://lexxy.dev/docs/) and can be configured globally through `Lexxy.configure` presets in your JavaScript, or per field through the field's `html` option.
---
# 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
```
## Options
---
# 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
### 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
```
## Options
## 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{4,8,12,15}
# app/avo/resources/team.rb
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
```
## Authorization
The preview request authorization is controlled with the `preview?` policy method.
---
# Progress bar
The `ProgressBar` field renders a `progress` element on `Index` and `Show` views 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"
}
```
## Options
---
# 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.
:::info Add this field to the `Gemfile`
```ruby
# Gemfile
gem "avo-rhino_field"
```
:::
```ruby
field :body, as: :rhino
```
Rhino is based on [TipTap](https://tiptap.dev/) which is a powerful and flexible WYSIWYG editor.
It supports [ActiveStorage](https://guides.rubyonrails.org/active_storage_overview.html) file attachments, [ActionText](https://guides.rubyonrails.org/action_text_overview.html), and seamlessly integrates with the Media Library.
## Options
---
# 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.'
```
## Options
---
# Stars
The `stars` field renders a star rating display on and views, and interactive clickable stars on and views. It's ideal for ratings, reviews, or any numeric value you want to represent visually as stars.
```ruby
field :rating, as: :stars
```
:::info
This field needs to be backed by a numeric column in your database (e.g., `integer`, `decimal`, or `float`).
:::
## Options
## Examples
```ruby
field :rating, as: :stars
```
```ruby
field :rating, as: :stars, max: 10
```
The field stores a numeric value (e.g., `0` to `5` for a 5-star rating). On edit forms, users can click on stars to set the rating. Filled stars represent the current value, while unfilled stars show the remaining capacity up to the maximum.
---
# Status
The `Status` field renders a colored indicator on index and show views β `loading`, `failed`, `success`, or `neutral`.
Map your values with [`failed_when`](#failed_when), [`loading_when`](#loading_when), and [`success_when`](#success_when). Anything not listed in those arrays falls back to `neutral`.
```ruby
field :status,
as: :status,
failed_when: [:failed],
loading_when: [:running, :pending],
success_when: [:done, :success]
```
Each row in the table uses one of these values. `archived` is not listed in any array, so it falls back to `neutral`.
## 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. It uses the time picker of flatpickr (without the calendar).
```ruby
field :starting_at,
as: :time,
picker_format: 'H:i',
format: "HH:mm",
relative: true,
picker_options: {
time_24hr: true
}
```
## Options
---
# 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.
Declare an association field in your resource's `def fields` and Avo renders it appropriately on each view. A `belongs_to` shows up as a link to the associated record (and a dropdown or search input on forms), while `has_one`, `has_many`, and `has_and_belongs_to_many` render as panels below the resource's fields, with attach, detach, and create controls.
```ruby
# app/avo/resources/post.rb
class Avo::Resources::Post < Avo::BaseResource
def fields
field :user, as: :belongs_to
field :comments, as: :has_many
end
end
```
| Rails association | Avo field |
| --- | --- |
| `belongs_to` | Belongs to |
| `has_one` | Has one |
| `has_one :through` | Has one |
| `has_many` | Has many |
| `has_many :through` | Has many |
| `has_and_belongs_to_many` | Has and belongs to many |
:::warning
It's important to set the `inverse_of` as often as possible to your model's association attribute.
:::
If the target resource has too many records for a dropdown, make the picker search-as-you-type with Searchable associations.
Nested association forms (the `nested` option on those fields) require the **`avo-nested`** gem in addition to your usual Avo gems. Use the same source and credentials as for your other private Avo gems; see Gem server authentication.
## Common options
Association fields share most of their options. Each β links to that option's documentation on the field's page.
| Option | Belongs to | Has one | Has many | HABTM |
| --- | :-: | :-: | :-: | :-: |
| `searchable` | β | β | β | β |
| `attach_scope` | β | β | β | β |
| `use_resource` | β | β | β | β |
| `scope` | β | β | β | β |
| `name` | β | β | β | β |
| `description` | β | β | β | β |
| `loading` | β | β | β | β |
| `linkable` | β | β | β | β |
| `reloadable` | β | β | β | β |
| `nested` | β | β | β | β |
| `attach_using` | β | β | β | β |
| `attach_fields` | β | β | β | β |
| `discreet_pagination` | β | β | β | β |
| `hide_search_input` | β | β | β | β |
| `hide_filter_button` | β | β | β | β |
| `link_to_child_resource` | β | β | β | β |
A few options belong to a single field type: `polymorphic_as` + `types`, `polymorphic_help`, `can_create`, `allow_via_detaching`, and `link_to_record` are all on `belongs_to`. `attach_fields` only persists its values on `:through` associations, where there's a join record to write to β see `has_many` and `has_one`.
## Show or hide the association buttons
The attach, detach, create, destroy, and actions buttons on association panels are controlled through the target resource's policy methods. Find out more on the authorization page.
## 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.
---
# Searchable associations
When a target resource has too many records for a simple dropdown, `searchable` replaces the association field's `