# Avo for Ruby on Rails Documentation - Version 2.0 Generated from Avo documentation v2.0 for LLM consumption # Getting Started Avo is a tool that helps developers and teams build apps 10x faster. It takes the things we always build for every app and abstracts them in familiar configuration files. It has three main parts: 1. [The CRUD UI](#_1-the-crud-ui) 2. [Dashboards](#_2-dashboards) 3. [The custom content](#_3-the-custom-content) ## 1. The CRUD UI If before, we built apps by creating layouts, adding controller methods to extract _data_ from the database, display it on the screen, worrying how we present it to the user, capture the users input as best we can and writing logic to send that data back to the database, Avo takes a different approach. It only needs to know what kind of data you need to expose and what type it is. After that, it takes care of the rest. You **tell it** you need to manage Users, Projects, Products, or any other types of data and what properties they have; `first_name` as `text`, `birthday` as `date`, `cover_photo` as `file` and so on. There are the basic fields like text, textarea, select and boolean, and the more complex ones like trix, markdown, gravatar, and boolean_group. There's even an amazing file field that's tightly integrated with `Active Storage`. **You've never added files integration as easy as this before.** ## 2. Dashboards Most apps need a way of displaying the stats in an aggregated form. Using the same configuration-based approach, Avo makes it so easy to display data in metric cards, charts, and even lets you take over using partial cards. ## 3. Custom content Avo is a shell in which you develop your app. It offers a familiar DSL to configure the app you're building, but sometimes you might have custom needs. That's where the custom content comes in. You can extend Avo in different layers. For example, in the CRUD UI, you may add Custom fields that slot in perfectly in the current panels and in each view. You can also add Resource tools to control the experience using standard Rails partials completely. You can even create Custom tools where you can add all the content you need using Rails partials or View Components. Most of the places where records are listed like Has many associations, attach modals, search, and more are scopable to meet your multi-tenancy scenarios. Most of the views you see are exportable using the `eject` command. StimulusJS is deeply baked into the CRUD UI and helps you extend the UI and make a complete experience for your users. ## Seamless upgrades Avo comes packaged as a [gem](https://rubygems.org/gems/avo). Therefore, it does not pollute your app with its internal files. Instead, everything is tucked away neatly in the package. That makes for a beautiful upgrade experience. You hit `bundle update avo` and get the newest and best of Avo without any file conflicts. ## Next up Please take your time and read the documentation pages to see how Avo interacts with your app and how one should use it. 1. Install Avo in your app 1. Set up the current user 1. Create a Resource 1. Set up authorization 1. Set up licensing 1. [Explore the live demo app](https://main.avodemo.com/) 1. Explore these docs 1. Enjoy building your app without ever worrying about the admin layer ever again 1. Explore the FAQ pages for guides on how to set up your Avo instance. ## Walkthrough videos ### Build a blog admin panel
### Build a booking app
--- # Avo ❤️ Rails & Hotwire In order to provide this all-in-one full-interface experience, we are using Rails' built-in [engines functionality](https://guides.rubyonrails.org/engines.html). ## Avo as a Rails engine Avo is a **Ruby on Rails engine** that runs isolated and side-by-side with your app. You configure it using a familiar DSL and sometimes regular Rails code through controller methods and partials. Avo's philosophy is to have as little business logic in your app as possible and give the developer the right tools to extend the functionality when needed. That means we use a few files to configure most of the interface. When that configuration is not enough, we enable the developer to export (eject) partials or even generate new ones for their total control. ### Prepend engine name in URL path helpers Because it's a **Rails engine** you'll have to follow a few engine rules. One of them is that [routes are isolated](https://guides.rubyonrails.org/engines.html#routes). That means that whenever you're using Rails' [path helpers](https://guides.rubyonrails.org/routing.html#generating-paths-and-urls-from-code) you'll need to prepend the name of the engine. For example, Avo's name is `avo,` and your app's engine name is `main_app`. ```ruby # When referencing an Avo route, use avo link_to 'Users', avo.resources_users_path link_to user.name, avo.resources_user_path(user) # When referencing a path for your app, use main_app link_to "Contact", main_app.contact_path link_to post.name, main_app.posts_path(post) ``` ### Use your helpers inside Avo This is something that we'd like to improve in the future, but the flow right now is to 1. include the helper module inside the controller you need it for and then 2. reference the methods from the `view_context.controller` object in resource files or any other place you'd need them. ```ruby{3-5,10,16} # app/helpers/application_helper.rb module ApplicationHelper def render_copyright_info "Copyright #{Date.today.year}" end end # app/controller/avo/products_controller.rb class Avo::ProductsController < Avo::ResourcesController include ApplicationHelper end # app/avo/resources/products_resource.rb class ProductsResource < Avo::BaseResource field :copyright, as: :text do |model| view_context.controller.render_copyright_info end end ``` ## Hotwire Avo's built with Hotwire, so anytime you'd like to use Turbo Frames, that's supported out of the box. ## StimulusJS Avo comes loaded with Stimulus JS and has a quite deep integration with it by providing useful built-in helpers that improve the development experience. Please follow the Stimulus JS guide that takes an in-depth look at all the possible ways of extending the UI. --- # Licensing Avo has two types of licenses. The **Community edition** is free to use and works best for personal, hobby, and small commercial projects, and the **Pro edition** for when you need more advanced features. ## Community vs. Pro The **Community version** has powerful features that you can use today like Resource management, most feature-rich fields, out-of-the box sorting, filtering and actions and all the associations you need. The **Pro version** has advanced authorization using Pundit, localization support, Custom tools, Custom fields and much [more](https://avohq.io/pricing). [More](https://avohq.io/roadmap) features like Settings screens and Themes are coming soon. The features are separated by their level of complexity and maintenance needs. Selling the Avo Pro edition as a paid upgrade allows us to fund this business and work on it full-time. That way, Avo improves over time, helping developers with more features and customization options. ## One license per site Each license can be used to run one application in one `production` environment on one URL. So when an app is in the `production` environment (`Rails.env.production?` is `true`), we only need to check that the license key and URL match the purchased license you're using for that app. ### More installations/environments per site You might have the same site running in multiple environments (`development`, `staging`, `test`, `QA`, etc.) for non-production purposes. You don't need extra licenses for those environments as long as they are not production environments (`Rails.env.production?` returns `false`). ### Sites You can see your license keys on your [licenses](https://avohq.io/licenses) page. ## Add the license key After you purchase an Avo license, add it to your `config/initializers/avo.rb` file on the `license_key`, and change the license type from `community` to `pro`. ```ruby{3-4} # config/initializers/avo.rb Avo.configure do |config| config.license = 'pro' config.license_key = '************************' # or use ENV['AVO_LICENSE_KEY'] end ``` ## Configure the display of license request timeout error If you want to hide the badge displaying the license request timeout error, you can do it by setting the `display_license_request_timeout_error` configuration to `false`. It defaults to `true`. ```ruby{3} # config/initializers/avo.rb Avo.configure do |config| config.display_license_request_timeout_error = false end ``` ## Purchase a license You can purchase a license on the [purchase](https://avohq.io/purchase/pro) page. ## License validation ### "Phone home" mechanism Avo pings the [HQ](https://avohq.io) (the license validation service) with some information about the current Avo installation. You can find the full payload below. ```ruby # HQ ping payload { license: Avo.configuration.license, license_key: Avo.configuration.license_key, avo_version: Avo::VERSION, rails_version: Rails::VERSION::STRING, ruby_version: RUBY_VERSION, environment: Rails.env, ip: current_request.ip, host: current_request.host, port: current_request.port, app_name: Rails.application.class.to_s.split("::").first, avo_metadata: avo_metadata } ``` That information helps us to identify your license and return a license valid/invalid response to Avo. The requests are made at boot time and every hour when you use Avo on any license type. If you need a special build without the license validation mechanism please get in touch. ## Upgrade your 1.0 license to 2.0 We are grateful to our `1.0` customers for believing in us. So we offer a free and easy upgrade path and **a year of free updates** for version `2.0`. If you have a 1.0 license and want to upgrade to 2.0, you need to log in to [avohq.io](https://avohq.io), and go to the [licenses page](https://avohq.io/subscriptions), and hit the `Upgrade` button next to your license. You'll be redirected to the new subscription screen where you can start the subscription for 2.0. After you add your billing details, you won't get charged immediately, but on the next billing cycle next year. If you choose not to renew the subscription after one year, that's fine; you can cancel at any time, no biggie. You won't get charged and will keep the last version available at the end of that subscription. --- # Technical support Avo is designed to be a self-serve product with [comprehensive documentation](https://docs.avohq.io) and [demo apps](#demo-apps) to be used as references. But, even the best of us get stuck at some point and you might need a nudge in the right direction. There are a few levels of how can get help. 1. [Open Source Software Support Policy](#open-source-software-support-policy) 1. [Self-help](#self-help) 1. [Help from the official team](#official-support) ## Open Source Software Support Avo's Open Source Software (OSS) support primarily revolves around assisting users with issues related to the Avo and other Avo libraries. This involves troubleshooting and providing solutions for problems originating from Avo or its related subcomponents. However, it is crucial to understand that the OSS support does not extend to application-specific issues that do not originate from Avo or its related parts. This includes but is not limited to: - Incorrect application configurations unrelated to Avo. - Conflicts with other libraries or frameworks within your application. - Deployment issues on specific infrastructure or platforms. - Application-specific runtime errors. - Problems caused by third-party plugins or extensions. - Data issues within your application. - Issues related to application performance optimization. - Integration problems with other services or databases. - Design and architecture questions about your specific application. - Language-specific issues are unrelated to Avo or other Avo libraries. We acknowledge that understanding your specific applications and their configuration is essential, but due to the time and resource demands, this goes beyond the scope of our OSS support. :::tip Enhanced support For users seeking assistance with application-specific issues, we offer a few paid technical support plans. These subscriptions provide comprehensive support, including help with application-specific problems. 1. Priority chat support 2. Advanced hands-on support For more information about our support plans, please visit [this](https://avohq.io/support) page. ::: ## Self help This is how you can help yourself. ## Help from the official team You sometimes need help from the authors. There are a few ways to do that. ## Reproduction repository The easiest way for us to troubleshoot and check on an issue is to send us a reproduction repository which we can install and run in our local environments. ```bash # run this command to get a new Rails app with Avo installed rails new -m https://avo.cool/new.rb APP_NAME # run to install avo-pro rails new -m https://avo.cool/new-pro.rb APP_NAME # run to install avo-advanced rails new -m https://avo.cool/new-advanced.rb APP_NAME ``` --- # Installation ## Requirements - Ruby on Rails >= 6.0 - Ruby >= 2.7 - `api_only` set to `false`. More here. - `propshaft` or `sprockets` gem - Have the `secret_key_base` defined in any of the following `ENV["SECRET_KEY_BASE"]`, `Rails.application.credentials.secret_key_base`, or `Rails.application.secrets.secret_key_base` :::warning Zeitwerk autoloading is required. When adding Avo to a Rails app that was previously a Rails 5 app you must ensure that it uses zeitwerk for autoloading and Rails 6 defaults. ```ruby # config/application.rb config.autoloader = :zeitwerk config.load_defaults 6.0 ``` More on this [here](https://github.com/avo-hq/avo/issues/1096). ::: ## Installing Avo Use [this](https://railsbytes.com/public/templates/zyvsME) RailsBytes template for a one-liner install process. `rails app:template LOCATION='https://avohq.io/app-template'` **OR** Take it step by step. 1. Add `gem 'avo'` to your `Gemfile` 1. Run `bundle install`. 1. Run `bin/rails generate avo:install` to generate the initializer and add Avo to the `routes.rb` file. 1. Generate an Avo Resource :::info This will mount the app under `/avo` path. Visit that link to see the result. ::: ## Install from GitHub You may also install Avo from GitHub but when you do that you must compile the assets yourself. You do that using the `rake avo:build-assets` command. When pushing to production, make sure you build the assets on deploy time using this task. ```ruby # Rakefile Rake::Task["assets:precompile"].enhance do Rake::Task["avo:build-assets"].execute end ``` :::info If you don't have the `assets:precompile` step in your deployment process, please adjust that with a different step you might have like `db:migrate`. ::: ## Mount Avo to a subdomain You can use the regular `host` constraint in the `routes.rb` file. ```ruby constraint host: 'avo' do mount Avo::Engine, at: '/' end ``` ## Next steps Please follow the next steps to ensure your app is secured and you have access to all the features you need. 1. Set up authentication and tell Avo who is your `current_user`. This step is required for the authorization feature to work. 1. Set up authorization. Don't let your data be exposed. Give users access to the data they need to see. 1. Set up licensing. --- # Authentication ## Customize the `current_user` method Avo will not assume your authentication provider (the `current_user` method returns `nil`). That means that you have to tell Avo who the `current_user` is. ### Using devise For [devise](https://github.com/heartcombo/devise), you should set it to `current_user`. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.current_user_method = :current_user end ``` ### Use a different authenticator Using another authentication provider, you may customize the `current_user` method to something else. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.current_user_method = :current_admin end ``` If you get the current user from another object like `Current.user`, you may pass a block to the `current_user_method` key. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.current_user_method do Current.user end end ``` ## Customize the sign-out link If your app responds to `destroy_user_session_path`, a sign-out menu item will be added on the bottom sidebar (when you click the three dots). If your app does not respond to this method, the link will be hidden unless you provide a custom sign-out path. There are two ways to customize the sign-out path. ### Customize the current user resource name You can customize just the "user" part of the path name by setting `current_user_resource_name`. For example if you follow the `User` -> `current_user` convention, you might have a `destroy_current_user_session_path` that logs the user out. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.current_user_resource_name = :current_user end ``` Or if your app provides a `destroy_current_admin_session_path` then you would need to set `current_user_resource_name` to `current_admin`. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.current_user_resource_name = :current_admin end ``` ### Customize the entire sign-out path Alternatively, you can customize the sign-out path name completely by setting `sign_out_path_name`. For example, if your app provides `logout_path` then you would pass this name to `sign_out_path_name`. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.sign_out_path_name = :logout_path end ``` If both `current_user_resource_name` and `sign_out_path_name` are set, `sign_out_path_name` takes precedence. ## Filter out requests You probably do not want to allow Avo access to everybody. If you're using [devise](https://github.com/heartcombo/devise) in your app, use this block to filter out requests in your `routes.rb` file. ```ruby authenticate :user do mount Avo::Engine => '/avo' end ``` You may also add custom user validation such as `user.admin?` to only permit a subset of users to your Avo instance. ```ruby authenticate :user, -> user { user.admin? } do mount Avo::Engine => '/avo' end ``` Check out more examples of authentication on [sidekiq's authentication section](https://github.com/mperham/sidekiq/wiki/Monitoring#authentication). ## `authenticate_with` method Alternatively, you can use the `authenticate_with` config attribute. It takes a block and evaluates it in Avo's `ApplicationController` as a `before_action`. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.authenticate_with do authenticate_admin_user end end ``` Note that Avo's `ApplicationController` does not inherit from your app's `ApplicationController`, so any protected methods you defined would not work. Instead, you would need to explicitly write the authentication logic in the block. For example, if you store your `user_id` in the session hash, then you can do: ```ruby # config/initializers/avo.rb Avo.configure do |config| config.authenticate_with do redirect_to '/' unless session[:user_id] == 1 # hard code user ids here end end ``` ## Authorization When you share access to Avo with your clients or large teams, you may want to restrict access to a resource or a subset of resources. You should set up your authorization rules (policies) to do that. Check out the authorization page for details on how to set that up. --- # Authorization When you share access to Avo with your clients or large teams, you may want to restrict access to a resource or a subset of resources. One example may be that only admin-level users may delete or update records. By default, Avo leverages Pundit under the hood to manage the authorization. :::info Pundit alternative Pundit is just the default choice. You may plug in your own client using the instructions [here](#custom-authorization-clients). ::: :::warning You must manually require `pundit` or your authorization library in your `Gemfile`. ```ruby # Minimal authorization through OO design and pure Ruby classes gem "pundit" ``` ::: ## Ensure Avo knows who your current user is Before setting any policies up, please ensure Avo knows your current user. Usually, this 👇 set up should be fine, but follow the authentication guide for more information. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.current_user_method = :current_user end ``` ## Policies Just run the regular pundit `bin/rails g pundit:policy Post` to generate a new policy. **If this is a new app you need to install pundit first bin/rails g pundit:install.** With this new policy, you may control what every type of user can do with Avo. The policy has the default methods for the regular controller actions: `index?`, `show?`, `create?`, `new?`, `update?`, `edit?` and `destroy?`. These methods control whether the resource appears on the sidebar, if the view/edit/destroy buttons are visible or if a user has access to those index/show/edit/create pages. Actions button ## Associations When using associations, you would like to set policies for `creating` new records on the association, allowing to `attach`, `detach`, `create` or `destroy` relevant records. Again, Avo makes this easy using a straightforward naming schema. :::warning Make sure you use the same pluralization as the association name. For a `has_many :users` association use the plural version method `view_users?`, `edit_users?`, `detach_users?`, etc., not the singular version `detach_user?`. ::: ### Example scenario We'll have this example of a `Post` resource with many `Comment`s through the `has_many :comments` association. :::info The `record` variable in policy methods In the `Post` `has_many` `Comments` example, when you want to authorize `show_comments?` in `PostPolicy` you will have a `Comment` instance as the `record` variable, but when you try to authorize the `attach_comments?`, you won't have that `Comment` instance because you want to create one, but we expose the parent `Post` instance so you have more information about that authorization action that you're trying to make. ::: ## Removing duplication :::info A note on duplication Let's take the following example: A `User` has many `Contract`s. And you represent that in your Avo resource. How do you handle authorization to the `ContractResource`? For one, you set the `ContractPolicy.index?` and `ContractPolicy.edit?` methods to `false` so regular users don't have access to all contracts (see and edit), and the `UserPolicy.view_contracts?` and `UserPolicy.edit_contracts?` set to `false`, because, when viewing a user you want to see all the contracts associated with that user and don't let them edit it. You might be thinking that there's code duplication here. "Why do I need to set a different rule for `UserPolicy.edit_contracts?` when I already set the `ContractPolicy.edit?` to `false`? Isn't that going to take precedence?" Now, let's imagine we have a user that is an admin in the application. The business need is that an admin has access to all contracts and can edit them. This is when we go back to the `ContractPolicy.edit?` and turn that to true for the admin user. And now we can separately control who and where a user can edit a contract. ::: You may remove duplication by applying the same policy rule from the original policy. ```ruby class CommentPolicy # ... more policy methods def edit record.user_id == current_user.id end end class PostPolicy # ... more policy methods def edit_comments? Pundit.policy!(user, record).edit? end end ``` Now, whatever action you take for one comment, it will be available for the `edit_comments?` method in `PostPolicy`. From version 2.31 we introduced a concern that removes the duplication and helps you apply the same rules to associations. You should include `Avo::Concerns::PolicyHelpers` in the `ApplicationPolicy` for it to be applied to all policy classes. `PolicyHelpers` allows you to use the method `inherit_association_from_policy`. This method takes two arguments; `association_name` and the policy file you want to be used as a template. ```ruby inherit_association_from_policy :comments, CommentPolicy ``` With just one line of code, it will define the following methods to policy your association: ```ruby def create_comments? CommentPolicy.new(user, record).create? end def edit_comments? CommentPolicy.new(user, record).edit? end def update_comments? CommentPolicy.new(user, record).update? end def destroy_comments? CommentPolicy.new(user, record).destroy? end def show_comments? CommentPolicy.new(user, record).show? end def reorder_comments? CommentPolicy.new(user, record).reorder? end def act_on_comments? CommentPolicy.new(user, record).act_on? end def view_comments? CommentPolicy.new(user, record).index? end ``` Although these methods won't be visible in your policy code, you can still override them. For instance, if you include the following code in your `CommentPolicy`, it will be executed in place of the one defined by the helper: ```ruby inherit_association_from_policy :comments, CommentPolicy def destroy_comments? false end ``` ## Attachments When working with files, it may be necessary to establish policies that determine whether users can `upload`, `download` or `delete` files. Fortunately, Avo simplifies this process by providing a straightforward naming schema for these policies. Both the `record` and the `user` will be available for you to access. :::info AUTHORIZE IN BULK If you want to allow or disallow these methods in bulk you can use a little meta-programming to assign all the same value. ```ruby [:cover_photo, :audio].each do |file| [:upload, :download, :delete].each do |action| define_method "#{action}_#{file}?" do true end end end ``` ::: ## Scopes You may specify a scope for the , , and views. ```ruby{3-9} class PostPolicy < ApplicationPolicy class Scope < Scope def resolve if user.admin? scope.all else scope.where(published: true) end end end end ``` :::warning This scope will be applied only to the view of Avo. It will not be applied to the association view. Example: A `Post` has_many `Comment`s. The `CommentPolicy::Scope` will not affect the `has_many` field. You need to add the `scope` option to the `has_many` field where you can modify the query. ```ruby ``` ::: ## Using different policy methods By default Avo will use the generated Pundit methods (`index?`, `show?`, `create?`, `new?`, `update?`, `edit?` and `destroy?`). But maybe, in your app, you're already using these methods and would like to use different ones for Avo. You may want override these methods inside your configuration with a simple map using the `authorization_methods` key. ```ruby{6-14} Avo.configure do |config| config.root_path = '/avo' config.app_name = 'Avocadelicious' config.license = 'pro' config.license_key = ENV['AVO_LICENSE_KEY'] config.authorization_methods = { index: 'avo_index?', show: 'avo_show?', edit: 'avo_edit?', new: 'avo_new?', update: 'avo_update?', create: 'avo_create?', destroy: 'avo_destroy?', search: 'avo_search?', } end ``` Now, Avo will use `avo_index?` instead of `index?` to manage the **Index** view authorization. ## Raise errors when policies are missing The default behavior of Avo is to allow missing policies for resources silently. So, if you have a `User` model and a `UserResource` but don't have a `UserPolicy`, Avo will not raise errors regarding missing policies and authorize that resource. If, however, you need to be on the safe side of things and raise errors when a Resource is missing a Policy, you can toggle on the `raise_error_on_missing_policy` configuration. ```ruby{7} # config/initializers/avo.rb Avo.configure do |config| config.root_path = '/avo' config.app_name = 'Avocadelicious' config.license = 'pro' config.license_key = ENV['AVO_LICENSE_KEY'] config.raise_error_on_missing_policy = true end ``` Now, you'll have to provide a policy for each resource you have in your app, thus making it a more secure app. ## Custom policies By default, Avo will infer the policy from the model of the resource object. If you wish to use a different policy for a given resource, you can specify it directly in the resource using the `authorization_policy` option. ```ruby class PhotoCommentResource < Avo::BaseResource self.model_class = ::Comment self.authorization_policy = PhotoCommentPolicy # ... end ``` ## Custom authorization clients :::info Check out the [Pundit client](https://github.com/avo-hq/avo/blob/main/lib/avo/services/authorization_clients/pundit_client.rb) for reference. ::: ### Change the authorization client In order to use a different client change the `authorization_client` option in the initializer. The built-in possible values are `nil` and `:pundit`. When you create your own client, pass the class name. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.authorization_client = 'Services::AuthorizationClients::CustomClient' end ``` ### Client methods Each authorization client must expose a few methods. ## Rolify integration Check out this guide to add rolify role management with Avo. --- # Cache Avo uses the application's cache system to enhance performance. The cache system is especially beneficial when dealing with resource index tables and license requests. ## Cache store selection The cache system dynamically selects the appropriate cache store based on the application's environment: ### Production In production, if the existing cache store is one of the following: `ActiveSupport::Cache::MemoryStore` or `ActiveSupport::Cache::NullStore` it will use the default `:file_store` with a cache path of `tmp/cache`. Otherwise, the existing cache store `Rails.cache` will be used. ### Test In testing, it directly uses the `Rails.cache` store. ### Development and other environments In all other environments the `:memory_store` is used. ### Custom selection There is the possibility to force the usage of a custom cache store into Avo. ```ruby # config/initializers/avo.rb config.cache_store = -> { ActiveSupport::Cache.lookup_store(:solid_cache_store) } # or config.cache_store = ActiveSupport::Cache.lookup_store(:solid_cache_store) ``` `cache_store` configuration option is expecting a cache store object, the lambda syntax can be useful if different stores are desired on different environments. :::warning MemoryStore in production Our computed system do not use MemoryStore in production because it will not be shared between multiple processes (when using Puma). ::: ## Solid Cache Avo seamlessly integrates with [Solid Cache](https://github.com/rails/solid_cache). To setup Solid Cache follow these essential steps Add this line to your application's Gemfile: ```ruby gem "solid_cache" ``` And then execute: ```bash $ bundle ``` Or install it yourself as: ```bash $ gem install solid_cache ``` Add the migration to your app: ```bash $ bin/rails solid_cache:install:migrations ``` Then run it: ```bash $ bin/rails db:migrate ``` To set Solid Cache as your Rails cache, you should add this to your environment config: ```ruby config.cache_store = :solid_cache_store ``` Check [Solid Cache repository](https://github.com/rails/solid_cache) for additional valuable information. --- # Resource options Avo effortlessly empowers you to build an entire customer-facing interface for your Ruby on Rails application. One of the most powerful features is how easy you can administer your database records using the CRUD UI. ## Overview Similar to how you configure your database layer using Rails `Model` files and their DSL, Avo's CRUD UI is configured using `Resource` files. Each `Resource` maps out one of your models. There can be multiple `Resource`s associated to the same model if you need that. All resources are located in the `app/avo/resources` directory. Unfortunately, `Resource`s can't be namespaced yet, so they all need to be in the root level of that directory. :::warning All resources from `app/avo/resources` are eager loaded on app boot-time to automatically have them available in your app. This might might interfere with some setups. If that happens you can manually register resources using [this guide](#manually-registering-resources). ::: ## Resources from model generation ```bash bin/rails generate model car make:string mileage:integer ``` Running this command will generate the expected Rails files for a model and for Avo the `CarResource` and `CarsController`. The auto-generated resource file will look like this: ```ruby class CarResource < Avo::BaseResource self.title = :id self.includes = [] # self.search_query = -> do # scope.ransack(id_eq: params[:q], m: "or").result(distinct: false) # end field :id, as: :id # Generated fields from model field :make, as: :text field :mileage, as: :number # add fields here end ``` This behavior can be ommited by using the argument `--skip-avo-resource`. For example if we want to generate a `Car` model but no Avo counterpart we should use the following command: ```bash bin/rails generate model car make:string kms:integer --skip-avo-resource ``` ## Manually defining resources ```bash bin/rails generate avo:resource post ``` This command will generate the `PostResource` file in `app/avo/resources/post_resource.rb` with the following code: ```ruby # app/avo/resources/post_resource.rb class PostResource < Avo::BaseResource self.title = :id self.includes = [] # self.search_query = -> do # scope.ransack(id_eq: params[:q], m: "or").result(distinct: false) # end field :id, as: :id # add fields here end ``` From this config, Avo will infer a few things like the resource's model will be the `Post` model and the name of the resource is `Post`. But all of those inferred things are actually overridable. Now, let's say we already have a model Post well defined with the following attributes: ```ruby # == Schema Information # # Table name: posts # # id :bigint not null, primary key # name :string # body :text # is_featured :boolean # published_at :datetime # user_id :bigint # created_at :datetime not null # updated_at :datetime not null # status :integer default("draft") # class Post < ApplicationRecord enum status: [:draft, :published, :archived] validates :name, presence: true has_one_attached :cover_photo has_one_attached :audio has_many_attached :attachments belongs_to :user, optional: true has_many :comments, as: :commentable has_many :reviews, as: :reviewable acts_as_taggable_on :tags end ``` In this case, the avo resource will generate the fields (without any configuration) from the model attributes and relationships resulting in the following resource: ```ruby class PostResource < Avo::BaseResource self.title = :id self.includes = [] # self.search_query = -> do # scope.ransack(id_eq: params[:q], m: "or").result(distinct: false) # end field :id, as: :id # Generated fields from model field :name, as: :text field :body, as: :textarea field :is_featured, as: :boolean field :published_at, as: :datetime field :user_id, as: :number field :status, as: :select, enum: ::Post.statuses field :cover_photo, as: :file field :audio, as: :file field :attachments, as: :files field :user, as: :belongs_to field :comments, as: :has_many field :reviews, as: :has_many field :tags, as: :tags # add fields here end ``` It's also possible to specify the resource model class. For example, if we want to create a new resource named `MiniPostResource` using the `Post` model we can do that using the following command: ```bash bin/rails generate avo:resource mini-post --model-class post ``` That command will create a new resource with the same attributes as the post resource above with specifying the `model_class`: ```ruby class MiniPostResource < Avo::BaseResource self.model_class = ::Post end ``` :::info You can see the result in the admin panel using this URL `/avo`. The `Post` resource will be visible on the left sidebar. ::: ### Fields `Resource` files tell Avo what models should be displayed in the UI, but not what kinds of data they hold. You do that using fields. One can add more fields to this resource below the `id` field using the `field DATABASE_COLUMN, as: FIELD_TYPE, **FIELD_OPTIONS` signature. ```ruby{5-15} class PostResource < Avo::BaseResource self.title = :id self.includes = [] field :id, as: :id field :name, as: :text, required: true field :body, as: :trix, placeholder: "Add the post body here", always_show: false field :cover_photo, as: :file, is_image: true, link_to_resource: true field :is_featured, as: :boolean field :is_published, as: :boolean do |model| model.published_at.present? end field :user, as: :belongs_to, placeholder: "—" end ``` ## Use multiple resources for the same model ### `model_resource_mapping` Usually, an Avo Resource maps to one Rails model. So there will be a one-to-one relationship between them. But there will be scenarios where you'd like to create another resource for the same model. Let's take as an example the `User` model. You'll have an `UserResource` associated with it. ```ruby # app/models/user.rb class User < ApplicationRecord end # app/avo/resources/user_resource.rb class UserResource < Avo::BaseResource self.title = :name field :id, as: :id, link_to_resource: true field :email, as: :gravatar, link_to_resource: true, as_avatar: :circle field :first_name, as: :text, required: true, placeholder: "John" field :last_name, as: :text, required: true, placeholder: "Doe" end ``` ![](/assets/img/resources/model-resource-mapping-1.jpg) So when you click on the Users sidebar menu item, you get to the `Index` page where all the users will be displayed. The information displayed will be the gravatar image, the first and the last name. Let's say we have a `Team` model with many `User`s. You'll have a `TeamResource` like so: ```ruby{11} # app/models/team.rb class Team < ApplicationRecord end # app/avo/resources/team_resource.rb class TeamResource < Avo::BaseResource self.title = :name field :id, as: :id, link_to_resource: true field :name, as: :text field :users, as: :has_many end ``` From that configuration, Avo will figure out that the `users` field points to the `UserResource` and will use that one to display the users. But, let's imagine that we don't want to display the gravatar on the `has_many` association, and we want to show the name on one column and the number of projects the user has on another column. We can create a different resource named `TeamUserResource` and add those fields. ```ruby # app/avo/resources/team_user_resource.rb class TeamUserResource < Avo::BaseResource self.title = :name field :id, as: :id, link_to_resource: true field :name, as: :text field :projects_count, as: :number end ``` We also need to update the `TeamResource` to use the new `TeamUserResource` for reference. ```ruby # app/avo/resources/team_resource.rb class TeamResource < Avo::BaseResource self.title = :name field :id, as: :id, link_to_resource: true field :name, as: :text field :users, as: :has_many, use_resource: TeamUserResource end ``` ![](/assets/img/resources/model-resource-mapping-2.jpg) But now, if we visit the `Users` page, we will see the fields for the `TeamUserResource` instead of `UserResource`, and that's because Avo fetches the resources in an alphabetical order, and `TeamUserResource` is before `UserResource`. That's definitely not what we want. The same might happen if you reference the `User` in other associations throughout your resource files. To mitigate that, we are going to use the `model_resource_mapping` option to set the "default" resource for a model. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.model_resource_mapping = { 'User': 'UserResource' } end ``` That will "shortcircuit" the regular alphabetical search and use the `UserResource` every time we don't specify otherwise. We can still tell Avo which resource to use in other `has_many` or `has_and_belongs_to_many` associations with the `use_resource` option. ## Setting the title of the resource Initially, the `title` attribute is set to `:id`, so the model's `id` attribute will be used to display the resource in search results and belongs select fields. You usually change it to something more representative, like the model's `title`, `name` or `label` attributes. ```ruby class PostResource < Avo::BaseResource self.title = :name # it will now reference @post.name to show you the title end ``` ### Using a computed title If you don't have a `title`, `name`, or `label` attribute in the database, you can add a getter method to your model where you compose the name. ```ruby{2} # app/avo/resources/comment_resource.rb class CommentResource < Avo::BaseResource self.title = :tiny_name # fieldd go here end # app/models/comment.rb class Comment < ApplicationRecord def tiny_name ActionView::Base.full_sanitizer.sanitize(body).truncate 30 end end ``` ## Resource description You might want to display information about the current resource to your users. Then, using the `description` class attribute, you can add some text to the `Index`, `Show`, `Edit`, and `New` views. Avo message There are two ways of setting the description. The quick way as a `string` and the more customizable way as a `block`. ### Set the description as a string ```ruby{3} class UserResource < Avo::BaseResource self.title = :name self.description = "These are the users of the app." end ``` This is the quick way to set the label, and it will be displayed **only on the `Index` page**. If you want to show the message on all views, use the block method. ### Set the description as a block This is the more customizable method where you can access the `model`, `view`, `user` (the current user), and `params` objects. ```ruby{3-13} class UserResource < Avo::BaseResource self.title = :name self.description = -> do if view == :index "These are the users of the app" else if user.is_admin? "You can update all properties for this user: #{model.id}" else "You can update some properties for this user: #{model.id}" end end end end ``` ## Eager loading If you regularly need access to a resource's associations, you can tell Avo to eager load those associations on the **Index** view using `includes`. That will help you avoid those nasty `n+1` performance issues. ```ruby class PostResource < Avo::BaseResource self.includes = [:user, :tags] end ``` ## Views ### Grid view On **Index**, the most common view type is `:table`, but you might have some data that you want to display in a **grid**. You can change that by setting `default_view_type` to `:grid` and by adding the `grid` block. Avo grid view ```ruby{2} class PostResource < Avo::BaseResource self.default_view_type = :grid end ``` Find out more on the grid view documentation page. ## Custom model class You might have a model that belongs to a namespace or has a different name than the resource. For that scenario, you can use the `@model` option to tell Avo which model to reference. ```ruby{2} class DelayedJobResource < Avo::BaseResource self.model_class = ::Delayed::Job field :id, as: :id # ... other fields go here end ``` ## Routing Avo will automatically generate routes based on the resource name when generating a resource. ``` PostResource -> /avo/resources/posts PhotoCommentResource -> /avo/resources/photo_comments ``` If you change the resource name, you should change the generated controller name too. ## Devise password optional If you use `devise` and update your user models (usually `User`) without passing a password, you will get a validation error. You can use `devise_password_optional` to stop receiving that error. It will [strip out](https://stackoverflow.com/questions/5113248/devise-update-user-without-password/11676957#11676957) the `password` key from `params`. ```ruby class UserResource < Avo::BaseResource self.devise_password_optional = true end ``` Related: - Password field ## Unscoped queries on `Index` You might have a `default_scope` on your model that you don't want to be applied when you render the `Index` view. ```ruby{2} class Project < ApplicationRecord default_scope { order(name: :asc) } end ``` You can unscope the query using the `unscoped_queries_on_index` (defaults to `false`) class variable on that resource. ```ruby{3} class ProjectResource < Avo::BaseResource self.title = :name self.unscoped_queries_on_index = true # fields go here end ``` ## Hide resource from the sidebar When you get started, the sidebar will be auto-generated for you with all the dashboards, resources, and custom tools. However, you may have resources that should not appear on the sidebar, which you can hide using the `visible_on_sidebar` option. ```ruby{3} class TeamMembershipResource < Avo::BaseResource self.title = :id self.visible_on_sidebar = false # fields declaration end ``` :::warning This option is used in the **auto-generated menu**, not in the **menu editor**. You'll have to use your own logic in the `visible` block for that. ::: ## Extending `Avo::ResourcesController` You may need to execute additional actions on the `ResourcesController` before loading the Avo pages. You can create an `Avo::BaseResourcesController` and extend your resource controller from it. ```ruby # app/controllers/avo/base_resources_controller.rb class Avo::BaseResourcesController < Avo::ResourcesController include AuthenticationController::Authentication before_action :is_logged_in? end # app/controllers/avo/posts_controller.rb class Avo::PostsController < Avo::BaseResourcesController end ``` *You can't use `Avo::BaseController` and `Avo::ResourcesController` as **your base controller**. They are defined inside Avo.* ## Show buttons on form footers If you have a lot of fields on a resource, that form might get pretty tall. So it would be useful to have the `Save` button in the footer of that form. You can do that by setting the `buttons_on_form_footers` option to `true` in your initializer. That will add the `Back` and `Save` buttons on the footer of that form for the `New` and `Edit` screens. ```ruby{3} # config/initializers/avo.rb Avo.configure do |config| config.buttons_on_form_footers = true end ``` Buttons on footer ## Customize what happens after a record is created/edited For some resources, it might make sense to redirect to something other than the `Show` view. With `after_create_path` and `after_update_path` you can control that. The valid options are `:show` (default), `:edit`, or `:index`. ```ruby{2-3} class CommentResource < Avo::BaseResource self.after_create_path = :index self.after_update_path = :edit field :id, as: :id field :body, as: :textarea end ``` ## Hide the record selector checkbox You might have resources that will never be selected, and you do not need that checkbox to waste your horizontal space. You can hide it using the `record_selector` class_attribute. ```ruby{2} class CommentResource < Avo::BaseResource self.record_selector = false field :id, as: :id field :body, as: :textarea end ``` Hide the record selector. ## Link to child resource (STI) `self.link_to_child_resource = true|false` Let's take an example. We have a `Person` model and `Sibling` and `Spouse` models that inherit from it (STI). Declare this option on the parent resource. When a user is on the view of your the `PersonResource` and clicks on the view button of a `Person` they will be redirected to a `Child` or `Spouse` resource instead of a `Person` resource. ## Manually registering resources In order to have a more straightforward experience when getting started with Avo, we are eager-loading the `app/avo/resources` directory. That makes all those resources available to your app without you doing anything else. That might make some Rails apps raise some errors. In order to mitigate that we added a way of manually declaring resources. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.resources = [ "UserResource", "FishResource", ] end ``` This tells Avo which resources you use and stops the eager-loading process on boot-time. This means that other resources that are not declared in this array will not show up in your app. --- # Resource controllers In order to benefit from Rails' amazing REST architecture, Avo generates a controller alongside every resource. Generally speaking you don't need to touch those controllers. Everything just works out of the box with configurations added to the resource file. However, sometimes you might need more granular control about what is happening in the controller actions or their callbacks. In that scenario you may take over and override that behavior. ## Request-Response lifecycle Each interaction with the CRUD UI results in a request - response cycle. That cycle passes through the `BaseController`. Each auto-generated controller for your resource inherits from `ResourcesController`, which inherits from `BaseController`. ```ruby class Avo::CoursesController < Avo::ResourcesController end ``` In order to make your controllers more flexible, there are several overridable methods similar to how [devise](https://github.com/heartcombo/devise#controller-filters-and-helpers:~:text=You%20can%20also%20override%20after_sign_in_path_for%20and%20after_sign_out_path_for%20to%20customize%20your%20redirect%20hooks) overrides `after_sign_in_path_for` and `after_sign_out_path_for`. ## Create methods For the `create` method, you can modify the `after_create_path`, the messages, and the actions both on success or failure. ## Update methods For the `update` method, you can modify the `after_update_path`, the messages, and the actions both on success or failure. ## Destroy methods For the `destroy` method, you can modify the `after_destroy_path`, the messages, and the actions both on success or failure. --- # Field options ## Declaring fields Each Avo resource has a `field` method that registers your `Resource`'s fields. Avo ships with various simple fields like `text`, `textarea`, `number`, `password`, `boolean`, `select`, and more complex ones like `markdown`, `key_value`, `trix`, and `code`. We can use the `field` method like so: ```ruby field :name, as: :text ``` The `name` property is the column in the database where Avo looks for information or a property on your model. That will add a few fields in your admin panel. On the view, we will get a new text column. On the view, we will also get a text value of that record's database value. Finally, on the and views, we will get a text input field that will display & update the `name` field on that model. ## Field conventions When we declare a field, we pinpoint the specific database row for that field. Usually, that's a snake case value. Each field has a label. Avo will convert the snake case name to a humanized version. In the following example, the `is_available` field will render the label as *Is available*. ```ruby field :is_available, as: :boolean ``` Field naming convention :::info If having the fields stacked one on top of another is not the right layout, try the resource-sidebar. ::: ## Change field name To customize the label, you can use the `name` property to pick a different label. ```ruby field :is_available, as: :boolean, name: 'Availability' ``` Field naming convention override ## Showing / Hiding fields on different views There will be cases where you want to show fields on different views conditionally. For example, you may want to display a field in the and views and hide it on the and views. For scenarios like that, you may use the visibility helpers `hide_on`, `show_on`, `only_on`, and `except_on` methods. Available options for these methods are: `:new`, `:edit`, `:index`, `:show`, `:forms` (both `:new` and `:edit`) and `:all` (only for `hide_on` and `show_on`). Be aware that a few fields are designed to override those options (ex: the `id` field is hidden in and ). ```ruby field :body, as: :text, hide_on: [:index, :show] ``` ## Field Visibility You might want to restrict some fields to be accessible only if a specific condition applies. For example, hide fields if the user is not an admin. You can use the `visible` block to do that. It can be a `boolean` or a lambda. Inside the lambda, we have access to the `context` object and the current `resource`. The `resource` has the current `model` object, too (`resource.model`). ```ruby field :is_featured, as: :boolean, visible: -> (resource:) { context[:user].is_admin? } # show field based on the context object field :is_featured, as: :boolean, visible: -> (resource:) { resource.name.include? 'user' } # show field based on the resource name field :is_featured, as: :boolean, visible: -> (resource:) { resource.model.published_at.present? } # show field based on a model attribute ``` ### Using `if` for field visibility You might be tempted to use the `if` statement to show/hide fields conditionally. However, that's not the best choice because the fields are registered at boot time, and some features are only available at runtime. Let's take the `context` object, for example. You might have the `current_user` assigned to the `context`, which will not be present at the app's boot time. Instead, that's present at request time when you have a `request` present from which you can find the user. ```ruby{4-7,13-16} # ❌ Don't do class CommentResource < Avo::BaseResource field :id, as: :id if context[:current_user].admin? field :body, as: :textarea field :tiny_name, as: :text, only_on: :index, as_description: true end end # ✅ Do instead class CommentResource < Avo::BaseResource field :id, as: :id with_options visible: -> (resource:) { context[:current_user].admin?} do field :body, as: :textarea field :tiny_name, as: :text, only_on: :index, as_description: true end end ``` So now, instead of relying on a request object unavailable at boot time, you can pass it a lambda function that will be executed on request time with all the required information. :::warning Since 2.30.2 On form submissions, the `visible` block is evaluated in the `create` and `update` controller actions. That's why you have to check if the `resource.model` object is present before trying to use it. ::: ```ruby # `resource.model` is nil when submitting the form on resource creation field :name, as: :text, visible -> (resource: ) { resource.model.enabled? } # Do this instead field :name, as: :text, visible -> (resource: ) { resource.model&.enabled? } ``` ## Computed Fields You might need to show a field with a value you don't have in a database row. In that case, you may compute the value using a block that receives the `model` (the actual database record), the `resource` (the configured Avo resource), and the current `view`. With that information, you can compute what to show on the field in the and views. ```ruby field 'Has posts', as: :boolean do |model, resource, view| model.posts.present? rescue false end ``` :::info Computed fields are displayed only on the and views. ::: This example will display a boolean field with the value computed from your custom block. ## Fields Formatter Sometimes you will want to process the database value before showing it to the user. You may do that using `format_using` block. Notice that this block will have effect on **all** views. You have access to a bunch of variables inside this block, all the defaults that `Avo::ExecutionContext` provides plus `value`, `model`, `key`, `resource`, `view` and `field`. :::warning We removed the `value` argument from `format_using` since version `2.36`. ::: ```ruby field :is_writer, as: :text, format_using: -> { if view == :new || view == :edit value else value.present? ? '👍' : '👎' end } ``` This example snippet will make the `:is_writer` field generate `👍` or `👎` emojis instead of `1` or `0` values on display views and the values `1` or `0` on form views. Fields formatter Another example: ```ruby field :company_url, as: :text, format_using: -> { link_to(value, value, target: "_blank") } do |model, *args| main_app.companies_url(model) end ``` ### Formatting with Rails helpers You can also format using Rails helpers like `number_to_currency` (note that `view_context` is used to access the helper): ```ruby field :price, as: :number, format_using: -> { view_context.number_to_currency(value) } ``` ## Modify the value before saving it to the database Similar to `format_using` we added `update_using` which will process the value sent from the UI before setting it on the model. ```ruby # Cast the text version of the field to actual JSOn to save to the database. field :metadata, as: :code, update_using: -> { # You have access to the following variables: # - value # - resource # - record # - view # - view_context # - context # - params # - request ActiveSupport::JSON.decode(value) } ``` ## Sortable fields One of the most common operations with database records is sorting the records by one of your fields. For that, Avo makes it easy using the `sortable` option. Add it to any field to make that column sortable in the view. ```ruby field :name, as: :text, sortable: true ``` Sortable fields ## Custom sortable block When using computed fields or `belongs_to` associations, you can't set `sortable: true` to that field because Avo doesn't know what to sort by. However, you can use a block to specify how the records should be sorted in those scenarios. ```ruby{4-7} class UserResource < Avo::BaseResource field :is_writer, as: :text, sortable: ->(query, direction) { # Order by something else completely, just to make a test case that clearly and reliably does what we want. query.order(id: direction) }, hide_on: :edit do |model, resource, view, field| model.posts.to_a.size > 0 ? "yes" : "no" end end ``` The block receives the query and the direction in which the sorting should be made and must return back a `query`. In the example of a `Post` that `has_many` `Comment`s, you might want to order the posts by which one received a comment the latest. You can do that using this query. ::: code-group ```ruby{5} [app/avo/resources/post_resource.rb] class PostResource < Avo::BaseResource field :last_commented_at, as: :date, sortable: ->(query, direction) { query.includes(:comments).order("comments.created_at #{direction}") } end ``` ```ruby{4-6} [app/models/post.rb] class Post < ApplicationRecord has_many :comments def last_commented_at comments.last&.created_at end end ``` ::: ## Placeholder Some fields support the `placeholder` option, which will be passed to the inputs on and views when they are empty. ```ruby field :name, as: :text, placeholder: 'John Doe' ``` Placeholder option ## Required When you want to mark a field as mandatory, you may use the `required` option to add an asterisk to that field, indicating that it's mandatory. ```ruby field :name, as: :text, required: true ``` Required option :::warning This option is only a cosmetic one. It will not add the validation logic to your model. You must add that yourself (`validates :name, presence: true`). ::: :::info For Avo version 2.14 and higher Avo will automatically detect your validation rules and mark the field as required by default. ::: You may use a block as well. It will be executed in the `ViewRecordHost` and you will have access to the `view`, `record`, `params`, `context`, `view_context`, and `current_user`. ```ruby field :name, as: :text, required: -> { view == :new } # make the field required only on the new view and not on edit ``` ## Readonly When you need to prevent the user from editing a field, the `readonly` option will render it as `disabled` on and views and the value will not be passed to that record in the database. This prevents a bad actor to go into the DOM, enable that field, update it, and then submit it, updating the record. ```ruby field :name, as: :text, readonly: true ``` Readonly option ### Readonly as a block You may use a block as well. It will be executed in the `ViewRecordHost` and you will have access to the `view`, `record`, `params`, `context`, `view_context`, and `current_user`. ```ruby field :id, as: :number, readonly: -> { view == :edit } # make the field readonly only on the new edit view ``` ## Disabled When you need to prevent the user from editing a field, the `disabled` option will render it as `disabled` on and views. This does not, however, prevent the user from enabling the field in the DOM and send an arbitrary value to the database. ```ruby field :name, as: :text, disabled: true ``` Disabled option ## Default Value When you need to give a default value to one of your fields on the view, you may use the `default` block, which takes either a fixed value or a block. ```ruby # using a value field :name, as: :text, default: 'John' # using a callback function field :level, as: :select, options: { 'Beginner': :beginner, 'Advanced': :advanced }, default: -> { Time.now.hour < 12 ? 'advanced' : 'beginner' } ``` ## Help text Sometimes you will need some extra text to explain better what the field is used for. You can achieve that by using the `help` method. The value can be either text or HTML. ```ruby # using the text value field :custom_css, as: :code, theme: 'dracula', language: 'css', help: "This enables you to edit the user's custom styles." # using HTML value field :password, as: :password, help: 'You may verify the password strength here.' ``` Help text :::info Since version `2.19`, the `default` block is being evaluated in the `ResourceViewRecordHost`. ::: ## Nullable When a user uses the **Save** button, Avo stores the value for each field in the database. However, there are cases where you may prefer to explicitly instruct Avo to store a `NULL` value in the database row when the field is empty. You do that by using the `nullable` option, which converts `nil` and empty values to `NULL`. You may also define which values should be interpreted as `NULL` using the `null_values` method. ```ruby # using default options field :updated_status, as: :status, failed_when: [:closed, :rejected, :failed], loading_when: [:loading, :running, :waiting], nullable: true # using custom null values field :body, as: :textarea, nullable: true, null_values: ['0', '', 'null', 'nil', nil] ``` ## Link to resource Sometimes, on the view, you may want a field in the table to be a link to that resource so that you don't have to scroll to the right to click on the icon. You can use `link_to_resource` to change a table cell to be a link to that resource. ```ruby # for id field field :id, as: :id, link_to_resource: true # for text field field :name, as: :text, link_to_resource: true # for gravatar field field :email, as: :gravatar, link_to_resource: true ``` As link to resource You can add this property on `Id`, `Text`, and `Gravatar` fields. Optionally you can enable the global config `id_links_to_resource`. More on that on the id links to resource docs page. Related: - ID links to resource - Resource controls on the left side ## Align text on Index view It's customary on tables to align numbers to the right. You can do that using the `index_text_align` option. Valid values are `:right` or `:center`. ```ruby{2} class ProjectResource < Avo::BaseResource field :users_required, as: :number, index_text_align: :right end ``` Index text align ## Stacked layout For some fields, it might make more sense to use all of the horizontal area to display it. You can do that by changing the layout of the field wrapper using the `stacked` option. ```ruby field :meta, as: :key_value, stacked: true ``` #### `inline` layout (default) ![](/assets/img/fields/field_wrapper_layout_inline.jpg) #### `stacked` layout ![](/assets/img/fields/field_wrapper_layout_stacked.jpg) ## Global `stacked` layout You may also set all the fields to follow the `stacked` layout by changing the `field_wrapper_layout` initializer option from `:inline` (default) to `:stacked`. ```ruby Avo.configure do |config| config.field_wrapper_layout = :stacked end ``` Now, all fields will have the stacked layout throughout your app. --- # Records ordering A typical scenario is when you need to set your records into a specific order. Like re-ordering `Slide`s inside a `Carousel` or `MenuItem`s inside a `Menu`. The `ordering` class attribute is your friend for this. You can set four actions `higher`, `lower`, `to_top` or `to_bottom`, and the `display_inline` and `visible_on` options. The actions are simple lambda functions but coupled with your logic or an ordering gem, and they can be pretty powerful. ## Configuration I'll demonstrate the ordering feature using the `act_as_list` gem. Install and configure the gem as instructed in the [tutorials](https://github.com/brendon/acts_as_list#example). Please ensure you [give all records position attribute values](https://github.com/brendon/acts_as_list#adding-acts_as_list-to-an-existing-model), so the gem works fine. Next, add the order actions like below. ```ruby class CourseLinkResource < Avo::BaseResource self.ordering = { visible_on: :index, actions: { higher: -> { record.move_higher }, lower: -> { record.move_lower }, to_top: -> { record.move_to_top }, to_bottom: -> { record.move_to_bottom }, } } end ``` The `record` is the actual instantiated model. The `move_higher`, `move_lower`, `move_to_top`, and `move_to_bottom` methods are provided by `act_as_list`. If you're not using that gem, you can add your logic inside to change the position of the record. The actions have access to `record`, `resource`, `options` (the `ordering` class attribute) and `params` (the `request` params). That configuration will generate a button with a popover containing the ordering buttons. Avo ordering ## Always show the order buttons If the resource you're trying to update requires re-ordering often, you can have the buttons visible at all times using the `display_inline: true` option. ```ruby class CourseLinkResource < Avo::BaseResource self.ordering = { display_inline: true, visible_on: :index, actions: { higher: -> { record.move_higher }, lower: -> { record.move_lower }, to_top: -> { record.move_to_top }, to_bottom: -> { record.move_to_bottom }, } } end ``` Avo ordering ## Display the buttons in the `Index` view or association view A typical scenario is to order the records only in the scope of a parent record, like order the `MenuItems` for a `Menu` or `Slides` for a `Slider`. So you wouldn't need to have the order buttons on the `Index` view but only in the association section. To control that, you can use the `visible_on` option. The possible values are `:index`, `:association` or `[:index, :association]` for both views. ## Change the scope on the `Index` view Naturally, you'll want to apply the `order(position: :asc)` condition to your query. You may do that in two ways. 1. Add a `default_scope` to your model. If you're using this ordering scheme only in Avo, then, this is not the recommended way, because it will add that scope to all queries for that model and you probably don't want that. 2. Use the [`resolve_query_scope`](https://docs.avohq.io/2.0/customization.html#custom-query-scopes) to alter the query in Avo. ```ruby{2-4} class CourseLinkResource < Avo::BaseResource self.resolve_query_scope = ->(model_class:) do model_class.order(position: :asc) end self.ordering = { display_inline: true, visible_on: :index, # :index or :association actions: { higher: -> { record.move_higher }, # has access to record, resource, options, params lower: -> { record.move_lower }, to_top: -> { record.move_to_top }, to_bottom: -> { record.move_to_bottom } } } end --- # Tabs and panels Once your Avo resources reach a certain level of complexity, you might feel the need to better organize the fields, associations, and resource tools into groups. You can already use the `heading` to separate the fields inside a panel, but maybe you'd like to do more. ## Panels Panel First, we should talk a bit about panels. They are the backbone of Avo's display infrastructure. Most of the information that's on display is wrapped inside a panel. They help to give Avo that uniform design on every page. They are also available as a view component `Avo::PanelComponent` for custom tools, and you can make your own pages using it. When using the fields DSL for resources, all fields declared in the root will be grouped into a "main" panel, but you can add your panels. ```ruby class UserResource < Avo::BaseResource field :id, as: :id, link_to_resource: true field :email, as: :text, name: "User Email", required: true panel name: "User information", description: "Some information about this user" do field :first_name, as: :text, required: true, placeholder: "John" field :last_name, as: :text, required: true, placeholder: "Doe" field :active, as: :boolean, name: "Is active", show_on: :show end end ``` Root fields and panel fields You can customize the panel `name` and panel `description`. ### Index view fields By default, only the fields declared in the root will be visible on the `Index` view. ```ruby{3-7} class UserResource < Avo::BaseResource # Only these fields will be visible on the `Index` view field :id, as: :id, link_to_resource: true field :email, as: :text, name: "User Email", required: true field :name, as: :text, only_on: :index do |model| "#{model.first_name} #{model.last_name}" end # These fields will be hidden on the `Index` view panel name: "User information", description: "Some information about this user" do field :first_name, as: :text, required: true, placeholder: "John" field :last_name, as: :text, required: true, placeholder: "Doe" field :active, as: :boolean, name: "Is active", show_on: :show end end ``` Index view ## Tabs Tabs are a new layer of abstraction over panels. They enable you to group panels and tools together under a single pavilion and toggle between them. ```ruby class UserResource < Avo::BaseResource field :id, as: :id, link_to_resource: true field :email, as: :text, name: "User Email", required: true tabs do tab "User information", description: "Some information about this user" do panel do field :first_name, as: :text, required: true, placeholder: "John" field :last_name, as: :text, required: true, placeholder: "Doe" field :active, as: :boolean, name: "Is active", show_on: :show end end field :teams, as: :has_and_belongs_to_many field :people, as: :has_many field :spouses, as: :has_many field :projects, as: :has_and_belongs_to_many end end ``` Avo tabs To use tabs, you need to open a `tabs` group block. Next, you add your `tab` block where you add fields and panels like you're used to on resource root. Most fields like `text`, `number`, `gravatar`, `date`, etc. need to be placed in a `panel`. However, the `has_one`, `has_many`, and `has_and_belongs_to_many` have their own panels, and they don't require a `panel` or a `tab`. The tab `name` is mandatory is what will be displayed on the tab switcher. The tab `description` is what will be displayed in the tooltip on hover. Avo tab name and description ### Tabs on Show view Tabs have more than an aesthetic function. They have a performance function too. On the `Show` page, if you have a lot of `has_many` type of fields or tools, they won't load right away, making it a bit more lightweight for your Rails app. Instead, they will lazy-load only when they are displayed. ### Tabs on Edit view All visibility rules still apply on' Edit', meaning that `has_*` fields will be hidden by default. However, you can enable them by adding `show_on: :edit`. All other fields will be loaded and hidden on page load. This way, when you submit a form, if you have validation rules in place requiring a field that's in a hidden tab, it will be present on the page on submit-time. ## Display as pills
When you have a lot of tabs in one group the tab switcher will overflow on the right-hand side. It will become scrollable to allow your users to get to the last tabs in the group. ![](/assets/img/tabs-and-panels/scrollable-tabs.gif) If you want to be able to see all your tabs in one group at a glance you may change the display to `:pills`. The pills will collapse and won't overflow off the page. ![](/assets/img/tabs-and-panels/tabs-as-pills.gif) ### Display all tabs as pills If you want to display all tabs as pills update your initializer's `tabs_style`. ```ruby Avo.configure do |config| config.tabs_style = :pills end ``` ### Display only some tabs as pills If you only need to display certain tabs as pills you can do that using the `style` option. ```ruby tabs style: :pills do # tabs go here end ``` --- # Resource Sidebar By default, all declared fields are going to be stacked vertically in the main area. But there are some fields with information that needs to be displayed in a smaller area, like boolean, date, and badge fields. Those fields don't need all that horizontal space and can probably be displayed in a different space. That's we created the **resource sidebar**. ## Adding fields to the sidebar Using the `sidebar` block on a resource you may declare fields the same way you woul don the root level. ```ruby class UserResource < Avo::BaseResource field :id, as: :id, link_to_resource: true field :first_name, as: :text, placeholder: "John" field :last_name, as: :text, placeholder: "Doe" sidebar do field :email, as: :gravatar, link_to_resource: true, as_avatar: :circle, only_on: :show field :active, as: :boolean, name: "Is active", only_on: :show end end ``` ![](/assets/img/resource-sidebar/sidebar.jpg) :::info For this initial iteration you may use the `field` and `heading` helpers. ::: The fields will be stacked in a similar way in a narrower area on the side of the main panel. You may notice that inside each field, the tabel and value zones are also stacked one on top of the other to allow for a larger area to display the field value. --- # Customizable controls ![](/assets/img/resources/customizable-controls/index.jpg) One of the things that we wanted to support from day one is customizable controls on resource pages. :::warning At the moment, only the `Show` view has customizable controls. ::: ## Default buttons By default, Avo displays a few buttons for the user to use on the , , and views which you can override using the appropriate resource options. ## Show page On the view the default configuration is `back_button`, `delete_button`, `detach_button`, `actions_list`, and `edit_button`. You can override them using `show_controls`. ## Customize the controls To start customizing the buttons, add a `show_controls` block and start adding the desired controls. ```ruby class FishResource < Avo::BaseResource self.show_controls = -> do back_button label: "", title: "Go back now" link_to "Fish.com", "https://fish.com", icon: "heroicons/outline/academic-cap", target: :_blank link_to "Turbo demo", "/admin/resources/fish/#{params[:id]}?change_to=🚀🚀🚀 New content here 🚀🚀🚀", class: ".custom-class", data: { turbo_frame: "fish_custom_action_demo" } delete_button label: "", title: "something" detach_button label: "", title: "something" actions_list exclude: [ReleaseFish], style: :primary, color: :slate action ReleaseFish, style: :primary, color: :fuchsia, icon: "heroicons/outline/globe" edit_button label: "" end end ``` ## Controls A control is an item that you can place in a designated area. They can be one of the default ones like `back_button`, `delete_button`, or `edit_button` to custom ones like `link_to` or `action`. You may use the following controls: :::warning The way `show_controls` works is like a shortcut the the actions that you already declared on your resource, so you should also declare it on the resource as you normally would in order to have it here. ```ruby{6,10} class FishResource < Avo::BaseResource self.title = :name self.show_controls = -> do # In order to use it here action ReleaseFish, style: :primary, color: :fuchsia end # Also declare it here action ReleaseFish, arguments: { both_actions: "Will use them" } end ::: ## Control Options ## Conditionally hiding/showing actions Actions have the `visible` block where you can control the visibility of an action. In the context of `show_controls` that block is not taken into account, but yiou can use regular `if`/`else` statements because the action declaration is wrapped in a block. ```ruby{6-8} class FishResource < Avo::BaseResource self.show_controls = -> do back_button label: "", title: "Go back now" # visibility conditional if record.something? action ReleaseFish, style: :primary, color: :fuchsia, icon: "heroicons/outline/globe" end edit_button label: "" end end ``` --- # Associations One of the most amazing things about Ruby on Rails is how easy it is to create [Active Record associations](https://guides.rubyonrails.org/association_basics.html) between models. We try to keep the same simple approach in Avo too. :::warning It's important to set the `inverse_of` as often as possible to your model's association attribute. ::: - Belongs to - Has one - Has many - Has many through - Has and blongs to many ## Single Table Inheritance (STI) When you have models that share behavior and fields with STI, Rails will cast the model as the final class no matter how you query it. ```ruby # app/models/user.rb class User < ApplicationRecord end # app/models/super_user.rb class SuperUser < User end # User.all.map(&:class) => [User, SuperUser] ``` For example, when you have two models, `User` and `SuperUser` with STI, when you call `User.all`, Rails will return an instance of `User` and an instance of `SuperUser`. That confuses Avo in producing the proper resource of `User`. That's why when you deal with STI, the final resource `SuperUserResource` should receive the underlying `model_class` so Avo knows which model it represents. ```ruby{4} class SuperUserResource < Avo::BaseResource self.title = :name self.includes = [] self.model_class = ::SuperUser field :id, as: :id field :name, as: :text end ``` ## Link to child resource when using STI Let's take another example. We have a `Person` model and `Sibling` and `Spouse` models that inherit from it. You may want to use the `PersonResource` to list all the records, but when your user clicks on a person, you want to use the inherited resources (`SiblingResource` and `SpouseResource`) to display the details. The reason is that you may want to display different fields or resource tools for each resource type. There are two ways you can use this: 1. `self.link_to_child_resource = true` Declare this option on the parent resource. When a user is on the view of your the `PersonResource` and clicks on the view button of a `Person` they will be redirected to a `Child` or `Spouse` resource instead of a `Person` resource. 2. `field :peoples, as: :has_many, link_to_child_resource: true` Use it on a `has_many` field. On the `PersonResource` you may want to show all the related people on the page, but when someone click on a record, they are redirected to the inherited `Child` or `Spouse` resource. ## Add custom labels to the associations' pages You might want to change the name that appears on the association page. For example, if you're displaying a `team_members` association, your users will default see `Team members` as the title, but you'd like to show them `Members`. You can customize that using fields localization. Custom label --- # Belongs to ```ruby field :user, as: :belongs_to ``` You will see three field types when you add a `BelongsTo` association to a model. ## Options ## Overview On the `Index` and `Show` views, Avo will generate a link to the associated record containing the `@title` value. Belongs to index Belongs to show On the `Edit` and `New` views, Avo will generate a dropdown element with the available records where the user can change the associated model. Belongs to edit ## Polymorphic `belongs_to` To use a polymorphic relation, you must add the `polymorphic_as` and `types` properties. ```ruby{12} class CommentResource < Avo::BaseResource self.title = :id field :id, as: :id field :body, as: :textarea field :excerpt, as: :text, show_on: :index, as_description: true do |model| ActionView::Base.full_sanitizer.sanitize(model.body).truncate 60 rescue "" end field :commentable, as: :belongs_to, polymorphic_as: :commentable, types: [::Post, ::Project] end ``` ## Polymorphic help When displaying a polymorphic association, you will see two dropdowns. One selects the polymorphic type (`Post` or `Project`), and one for choosing the actual record. You may want to give the user explicit information about those dropdowns using the `polymorphic_help` option for the first dropdown and `help` for the second. ```ruby{16-17} class CommentResource < Avo::BaseResource self.title = :id field :id, as: :id field :body, as: :textarea field :excerpt, as: :text, show_on: :index, as_description: true do |model| ActionView::Base.full_sanitizer.sanitize(model.body).truncate 60 rescue "" end field :reviewable, as: :belongs_to, polymorphic_as: :reviewable, types: [::Post, ::Project, ::Team], polymorphic_help: "Choose the type of record to review", help: "Choose the record you need." end ``` Belongs to ploymorphic help ## Searchable `belongs_to` There might be the case that you have a lot of records for the parent resource, and a simple dropdown won't cut it. This is where you can use the `searchable` option to get a better search experience for that resource. ```ruby{7} class CommentResource < Avo::BaseResource self.title = :id field :id, as: :id field :body, as: :textarea field :user, as: :belongs_to, searchable: true end ``` Belongs to searchable Belongs to searchable `searchable` works with `polymorphic` `belongs_to` associations too. ```ruby{7} class CommentResource < Avo::BaseResource self.title = :id field :id, as: :id field :body, as: :textarea field :commentable, as: :belongs_to, polymorphic_as: :commentable, types: [::Post, ::Project], searchable: true end ``` :::info Avo uses the search feature behind the scenes, so **make sure the target resource has the `search_query` option configured**. ::: ```ruby # app/avo/resources/post_resource.rb class PostResource < Avo::BaseResource self.search_query = -> do scope.ransack(id_eq: params[:q], name_cont: params[:q], body_cont: params[:q], m: "or").result(distinct: false) end end # app/avo/resources/project_resource.rb class ProjectResource < Avo::BaseResource self.search_query = -> do scope.ransack(id_eq: params[:q], name_cont: params[:q], country_cont: params[:q], m: "or").result(distinct: false) end end ``` ## Belongs to attach scope When you edit a record that has a `belongs_to` association, on the edit screen, you will have a list of records from which you can choose a record to associate with. For example, a `Post` belongs to a `User`. So on the post edit screen, you will have a dropdown (or a search field if it's [searchable](#searchable-belongs-to)) with all the available users. But that's not ideal. For example, maybe you don't want to show all the users in your app but only those who are not admins. You can use the `attach_scope` option to keep only the users you need in the `belongs_to` dropdown field. You have access to the `query` that you can alter and return it and the `parent` object, which is the actual record where you want to assign the association (the true `Post` in the below example). ```ruby # app/models/user.rb class User < ApplicationRecord scope :non_admins, -> { where "(roles->>'admin')::boolean != true" } end # app/avo/resources/post_resource.rb class PostResource < Avo::BaseResource field :user, as: :belongs_to, attach_scope: -> { query.non_admins } end ``` For scenarios where you need to add a record associated with that resource (you create a `Post` through a `Category`), the `parent` is unavailable (the `Post` is not persisted in the database). Therefore, Avo makes the `parent` an instantiated object with its parent populated (a `Post` with the `category_id` populated with the parent `Category` from which you started the creation process) so you can better scope out the data (you know from which `Category` it was initiated). ## Allow detaching via the association When you visit a record through an association, that `belongs_to` field is disabled. There might be cases where you'd like that field not to be disabled and allow your users to change that association. You can instruct Avo to keep that field enabled in this scenario using `allow_via_detaching`. ```ruby{11} class CommentResource < Avo::BaseResource self.title = :id field :id, as: :id field :body, as: :textarea field :commentable, as: :belongs_to, polymorphic_as: :commentable, types: [::Post, ::Project], allow_via_detaching: true end ``` --- # Has One :::warning It's important to set the `inverse_of` as often as possible to your model's association attribute. ::: # Has One The `HasOne` association shows the unfolded view of your `has_one` association. It's like peaking on the `Show` view of that associated record. The user can also access the `Attach` and `Detach` buttons. ```ruby field :admin, as: :has_one ``` Has one ## Options ## Show on edit screens By default, `has_and_belongs_to_many` is only visible on the `Show` page. If you want to enable it on the `Edit` page, too, you need to add the `show_on: :edit` option. :::warning Adding associations on the `New` screen is not currently supported. The association needs some information from the parent record that hasn't been created yet (because the user is on the `New` screen). ::: You may use the redirect helpers to have the following flow: 1. User is on the `New` view. They can't see the association panels yet. 1. User creates the record. 1. They get redirected to the `Show`/`Edit` view, where they can see the association panels. 1. User attaches associations. --- # Has Many By default, the `HasMany` field is visible only on the `Show` view. You will see a new panel with the model's associated records below the regular fields panel. ```ruby field :projects, as: :has_many ``` ## Options ## Search query scope If the resource used for the `has_many` association has the `search_query` block configured, Avo will use that to scope out the search query to that association. For example, if you have a `Team` model that `has_many` `User`s, now you'll be able to search through that team's users instead of all of them. You can target that search using `params[:via_association]`. When the value of `params[:via_association]` is `has_many`, the search has been mad inside a has_many association. For example, if you want to show the records in a different order, you can do this: ```ruby self.search_query = -> do if params[:via_association] == 'has_many' scope.ransack(id_eq: params[:q], m: "or").result(distinct: false).order(name: :asc) else scope.ransack(id_eq: params[:q], m: "or").result(distinct: false) end end ``` ## Has Many Through The `HasMany` association also supports the `:through` option. ```ruby{3} field :members, as: :has_many, through: :memberships ``` ## Show on edit screens By default, `has_and_belongs_to_many` is only visible on the `Show` page. If you want to enable it on the `Edit` page, too, you need to add the `show_on: :edit` option. :::warning Adding associations on the `New` screen is not currently supported. The association needs some information from the parent record that hasn't been created yet (because the user is on the `New` screen). ::: You may use the redirect helpers to have the following flow: 1. User is on the `New` view. They can't see the association panels yet. 1. User creates the record. 1. They get redirected to the `Show`/`Edit` view, where they can see the association panels. 1. User attaches associations. ## Add scopes to associations When displaying `has_many` associations, you might want to scope out some associated records. For example, a user might have multiple comments, but on the user's `Show` page, you don't want to display all the comments, but only the approved ones. ```ruby{5,16,22} # app/models/comment.rb class Comment < ApplicationRecord belongs_to :user, optional: true scope :approved, -> { where(approved: true) } end # app/models/user.rb class User < ApplicationRecord has_many :comments end # app/avo/resources/user_resource.rb class UserResource < Avo::BaseResource # Before v2.5.0 field :comments, as: :has_many, scope: -> { approved } end # app/avo/resources/user_resource.rb class UserResource < Avo::BaseResource # After v2.5.0 field :comments, as: :has_many, scope: -> { query.approved } end ``` The `comments` query on the user `Index` page will have the `approved` scope attached. Association scope With version 2.5.0, you'll also have access to the `parent` record so that you can use that to scope your associated models even better. All the `has_many` associations have the `attach_scope` option available too. ## Show/hide buttons You will want to control the visibility of the attach/detach/create/destroy/actions buttons visible throughout your app. You can use the policy methods to do that. Find out more on the authorization page. Associations authorization --- # Has And Belongs To Many The `HasAndBelongsToMany` association works similarly to `HasMany`. ```ruby field :users, as: :has_and_belongs_to_many ``` ## Options ## Search query scope If the resource used for the `has_many` association has the `search_query` block configured, Avo will use that to scope out the search query to that association. For example, if you have a `Team` model that `has_many` `User`s, now you'll be able to search through that team's users instead of all of them. You can target that search using `params[:via_association]`. When the value of `params[:via_association]` is `has_many`, the search has been mad inside a has_many association. For example, if you want to show the records in a different order, you can do this: ```ruby self.search_query = -> do if params[:via_association] == 'has_many' scope.ransack(id_eq: params[:q], m: "or").result(distinct: false).order(name: :asc) else scope.ransack(id_eq: params[:q], m: "or").result(distinct: false) end end ``` ## Show on edit screens By default, `has_and_belongs_to_many` is only visible on the `Show` page. If you want to enable it on the `Edit` page, too, you need to add the `show_on: :edit` option. :::warning Adding associations on the `New` screen is not currently supported. The association needs some information from the parent record that hasn't been created yet (because the user is on the `New` screen). ::: You may use the redirect helpers to have the following flow: 1. User is on the `New` view. They can't see the association panels yet. 1. User creates the record. 1. They get redirected to the `Show`/`Edit` view, where they can see the association panels. 1. User attaches associations. ### Searchable `has_and_belongs_to_many`
Similar to `belongs_to`, the `has_many` associations support the `searchable` option. ## Add scopes to associations When displaying `has_many` associations, you might want to scope out some associated records. For example, a user might have multiple comments, but on the user's `Show` page, you don't want to display all the comments, but only the approved ones. ```ruby{5,16,22} # app/models/comment.rb class Comment < ApplicationRecord belongs_to :user, optional: true scope :approved, -> { where(approved: true) } end # app/models/user.rb class User < ApplicationRecord has_many :comments end # app/avo/resources/user_resource.rb class UserResource < Avo::BaseResource # Before v2.5.0 field :comments, as: :has_many, scope: -> { approved } end # app/avo/resources/user_resource.rb class UserResource < Avo::BaseResource # After v2.5.0 field :comments, as: :has_many, scope: -> { query.approved } end ``` The `comments` query on the user `Index` page will have the `approved` scope attached. Association scope With version 2.5.0, you'll also have access to the `parent` record so that you can use that to scope your associated models even better. All the `has_many` associations have the `attach_scope` option available too. ## Show/hide buttons You will want to control the visibility of the attach/detach/create/destroy/actions buttons visible throughout your app. You can use the policy methods to do that. Find out more on the authorization page. Associations authorization --- # Dashboards :::warning You must manually require the `chartkick` gem in your `Gemfile`. ```ruby # Create beautiful JavaScript charts with one line of Ruby gem "chartkick" ``` ::: There comes the point in your app's life when you need to display the data in an aggregated form like a metric or chart. That's what Avo's Dashboards are all about. ## Generate a dashboard Run `bin/rails g avo:dashboard my_dashboard` to get a shiny new dashboard. ```ruby class MyDashboard < Avo::Dashboards::BaseDashboard self.id = 'my_dashboard' self.name = 'Dashy' self.description = 'The first dashbaord' self.grid_cols = 3 card ExampleMetric card ExampleAreaChart card ExampleScatterChart card PercentDone card AmountRaised card ExampleLineChart card ExampleColumnChart card ExamplePieChart card ExampleBarChart divider label: "Custom partials" card ExampleCustomPartial card MapCard end ``` Avo Dashboard ## Settings Each dashboard is a file. It holds information about itself like the `id`, `name`, `description`, and how many columns its grid has. The `id` field has to be unique. The `name` is what the user sees in big letters on top of the page, and the `description` is some text you pass to give the user more details regarding the dashboard. Using the ' grid_cols ' parameter, you may organize the cards in a grid with `3`, `4`, `5`, or `6` columns using the `grid_cols` parameter. The default is `3`. ## Cards All cards have a few base settings and a few custom ones. ### Base settings All cards have some standard settings like `id`, which must be unique, `label` and `description`. The `label` will be the title of your card, and `description` will show a tiny question mark icon on the bottom right with a tooltip with that description. Each card has its own `cols` and `rows` settings to control the width and height of the card inside the dashboard grid. They can have values from `1` to `6`. ```ruby{2-7} class UsersMetric < Avo::Dashboards::MetricCard self.id = 'users_metric' self.label = 'Users count' self.description = 'Users description' self.cols = 1 self.rows = 1 self.display_header = true end ``` Avo Dashboard Metric ### Control the aggregation using ranges You may also want to give the user the ability to query data in different ranges. You can control what's passed in the dropdown using the' ranges' attribute. The array passed here will be parsed and displayed on the card. All integers are transformed to days, and other string variables will be passed as they are. You can also set a default range using the `initial_range` attribute. ```ruby{4,5} class UsersMetric < Avo::Dashboards::MetricCard self.id = 'users_metric' self.label = 'Users count' self.initial_range = 30 self.ranges = { "7 days": 7, "30 days": 30, "60 days": 60, "365 days": 365, Today: "TODAY", "Month to date": "MTD", "Quarter to date": "QTD", "Year to date": "YTD", All: "ALL" } end ``` ### Keep the data fresh If this dashboard is something you keep on the big screen, you must keep the data fresh at all times. That's easy using `refresh_every`. You pass the number of seconds you need to be refreshed and forget about it. Avo will do it for you. ```ruby{3} class UsersMetric < Avo::Dashboards::MetricCard self.id = 'users_metric' self.refresh_every = 10.minutes end ``` ### Hide the header In cases where you need to embed some content that should fill the whole card (like a map, for example), you can choose to hide the label and ranges dropdown. ```ruby{3} class UsersMetric < Avo::Dashboards::MetricCard self.id = 'users_metric' self.display_header = false end ``` Avo Dashboard Map card ### Override card options from the dashboard We found ourselves in the position to add a few cards that were the same card but with a slight difference. Ex: Have one `Users count` card and another `Active users count` card. They both count users, but the latter has an `active: true` condition applied. Before, we'd have to duplicate that card and modify the `query` method slightly but end up with duplicated boilerplate code. For those scenarios, we created the `options` attribute. It allows you to send arbitrary options to the card from the parent. ```ruby{6-8} class Dashy < Avo::Dashboards::BaseDashboard self.id = "dashy" self.name = "Dashy" card UsersCount card UsersCount, options: { active_users: true } end ``` Now we can pick up that option in the card and update the query accordingly. ```ruby{9-11} class UsersCount < Avo::Dashboards::MetricCard self.id = "users_metric" self.label = "Users count" # You have access to context, params, range, current dashboard, and current card def query scope = User if options[:active_users].present? scope = scope.active end result scope.count end end ``` That gives you an extra layer of control without code duplication and the best developer experience. #### Control the base settings from the parent Evidently, you don't want to show the same `label`, `description`, and other details for that second card from the first card;. Therefore, you can control the `label`, `description`, `cols`, `rows`, and `refresh_every` options from the parent declaration. ```ruby{7-11} class Dashy < Avo::Dashboards::BaseDashboard self.id = "dashy" self.name = "Dashy" card UsersCount card UsersCount, label: "Active users", description: "Active users count", cols: 2, rows: 2, refresh_every: 2.minutes, options: { active_users: true } end ``` ## Cards visibility It's common to show the same dashboard to multiple types of users (admins, regular users). In that scenario you might want to hide some cards for the regular users and show them just to the admins. You can use the `visible` option to do that. It can be a `boolean` or a `block` where you can access the `params`, `current_user`, `context`, `parent`, and `card` object. ```ruby{4-6} class UsersCount < Avo::Dashboards::MetricCard self.id = "users_metric" self.label = "Users count" self.visible = -> do # You have access to context, params, parent (the current dashboard), and current card true end def query result User.count end end ``` You may also control the visibility from the dashboard class. ```ruby class Dashy < Avo::Dashboards::BaseDashboard self.name = "Dashy" card UsersCount, visible: -> { true } end ``` ## Card types You can add three types of cards to your dashboard: `metric`, `chartkick`, and `partial`. ### Metric card The metric card is your friend when you only need to display a simple significant number on your dashboard. Generate one run `bin/rails g avo:card:metric users_metric`. Avo Dashboard Metric #### Calculate results To calculate your result, you may use the `query` method. After you run your query, use the `result` method to store the value that will be displayed on the card. In the `query` method you have access to a few variables like `context` (the App context), `params` (the request params), `range` (the range that was requested), `dashboard` (the current dashboard the card is on), and current `card`. ```ruby{13-34,36} class UsersMetric < Avo::Dashboards::MetricCard self.id = 'users_metric' self.label = 'Users count' self.description = 'Some tiny description' self.cols = 1 # self.rows = 1 # self.initial_range = 30 # self.ranges = [7, 30, 60, 365, 'TODAY', 'MTD', 'QTD', 'YTD', 'ALL'] # self.prefix = '$' # self.suffix = '%' # self.refresh_every = 10.minutes def query from = Date.today.midnight - 1.week to = DateTime.current if range.present? if range.to_s == range.to_i.to_s from = DateTime.current - range.to_i.days else case range when 'TODAY' from = DateTime.current.beginning_of_day when 'MTD' from = DateTime.current.beginning_of_month when 'QTD' from = DateTime.current.beginning_of_quarter when 'YTD' from = DateTime.current.beginning_of_year when 'ALL' from = Time.at(0) end end end result User.where(created_at: from..to).count end end ``` #### Decorate the data using `prefix` and `suffix` Some metrics might want to add a `prefix` or a `suffix` to display the data better. ```ruby{3,4} class UsersMetric < Avo::Dashboards::MetricCard self.id = 'users_metric' self.prefix = '$' self.suffix = '%' end ``` Avo Dashboard Prefix & suffix ### Chartkick card A picture is worth a thousand words. So maybe a chart a hundred? Who knows? But creating charts in Avo is very easy with the help of the [chartkick](https://github.com/ankane/chartkick) gem. You start by running `bin/rails g avo:card:chartkick users_chart`. ```ruby class UserSignups < Avo::Dashboards::ChartkickCard self.id = 'user_signups' self.label = 'User signups' self.chart_type = :area_chart self.description = 'Some tiny description' self.cols = 2 # self.rows = 1 # self.chart_options = { library: { plugins: { legend: { display: true } } } } # self.flush = true # self.legend = false # self.scale = false # self.legend_on_left = false # self.legend_on_right = false def query points = 16 i = Time.new.year.to_i - points base_data = Array .new(points) .map do i += 1 [i.to_s, rand(0..20)] end .to_h data = [ { name: 'batch 1', data: base_data.map { |k, v| [k, rand(0..20)] }.to_h }, { name: 'batch 2', data: base_data.map { |k, v| [k, rand(0..40)] }.to_h }, { name: 'batch 3', data: base_data.map { |k, v| [k, rand(0..10)] }.to_h } ] result data end end ``` Chartkick card #### Chart types Using the `self.chart_type` class attribute, you can change the chart type. Supported types are `line_chart`, `pie_chart`, `column_chart`, `bar_chart`, `area_chart`, and `scatter_chart`. #### Customize chart Because the charts are being rendered with padding initially, we offset that before rendering to make the chart look good on the card. To disable that, you can set `self.flush = false`. That will set the chart loose for you to customize further. After you set `flush` to `false`, you can add/remove the `scale` and `legend`. You can also place the legend on the left or right using `legend_on_left` and `legend_on_right`. These are just some of the predefined options we provide out of the box, but you can send different [chartkick options](https://github.com/ankane/chartkick#options) to the chart using `chart_options`. If you'd like to use [Groupdate](https://github.com/ankane/groupdate), [Hightop](https://github.com/ankane/hightop), and [ActiveMedian](https://github.com/ankane/active_median) you should require them in your `Gemfile`. Only `chartkick` is required by default. `chart.js` is supported for the time being. So if you need support for other types, please reach out or post a PR (🙏 PRs are much appreciated). ### Partial card You can use a partial card to add custom content to a card. Generate one by running `bin/rails g avo:card:partial custom_card`. That will create the card class and the partial for it. ```ruby{5} class ExampleCustomPartial < Avo::Dashboards::PartialCard self.id = "users_custom_card" self.cols = 1 self.rows = 4 self.partial = "avo/cards/custom_card" # self.display_header = true end ``` Custom partial card You may embed a piece of content from another app using an iframe. You can hide the header using the `self.display_header = false` option. That will render the embedded content flush to the container. ```ruby{5} # app/avo/cards/map_card.rb class MapCard < Avo::Dashboards::PartialCard self.id = "map_card" self.label = "Map card" self.partial = "avo/cards/map_card" self.display_header = false self.cols = 2 self.rows = 4 end ``` ```html ``` Avo Dashboard Map card ## Dividers You may want to separate the cards. You can use dividers to do that. ```ruby{16} class Dashy < Avo::Dashboards::BaseDashboard self.id = 'dashy' self.name = 'Dashy' self.description = 'The first dashbaord' self.grid_cols = 3 card ExampleMetric card ExampleAreaChart card ExampleScatterChart card PercentDone card AmountRaised card ExampleLineChart card ExampleColumnChart card ExamplePieChart card ExampleBarChart divider label: "Custom partials" card ExampleCustomPartial card MapCard end ``` Avo Dashboard Divider Dividers can be a simple line between your cards or have some text on them that you control using the `label` option. When you don't want to show the line, you can enable the `invisible` option, which adds the divider but does not display a border or label. ## Dividers visibility You might want to conditionally show/hide a divider based on a few factors. You can do that using the `visible` option. ```ruby class Dashy < Avo::Dashboards::BaseDashboard self.name = "Dashy" card UsersCount, visible: -> { # You have access to context, params, parent (the current dashboard) true } end ``` ## Dashboards visibility You might want to hide specific dashboards from certain users. You can do that using the `visible` option. The option can be a boolean `true`/`false` or a block where you have access to the `params`, `current_user`, `context`, and `dashboard`. If you don't pass anything to `visible`, the dashboard will be available for anyone. ```ruby{5-11} class ComplexDash < Avo::Dashboards::BaseDashboard self.id = "complex_dash" self.name = "Complex dash" self.description = "Complex dash description" self.visible = -> do current_user.is_admin? # or params[:something] == 'something else' # or context[:your_param] == params[:something_else] end card UsersCount end ``` ## Dashboards authorization You can set authorization rules for dashboards using the `authorize` block. ```ruby{3-6} class Dashy < Avo::Dashboards::BaseDashboard self.id = 'dashy' self.authorization = -> do # You have access to current_user, params, request, context, adn view_context. current_user.is_admin? end end ``` --- # Cards Cards are one way of quickly adding custom content for your users. You can add three types of cards to your dashboard: `partial`, `metric`, and `chartkick`. ## Base settings All cards have some standard settings like `id`, which must be unique, `label` and `description`. The `label` will be the title of your card, and `description` will show a tiny question mark icon on the bottom right with a tooltip with that description. Each card has its own `cols` and `rows` settings to control the width and height of the card inside the dashboard grid. They can have values from `1` to `6`. ```ruby{2-7} class UsersMetric < Avo::Dashboards::MetricCard self.id = 'users_metric' self.label = 'Users count' self.description = 'Users description' self.cols = 1 self.rows = 1 self.display_header = true end ``` Avo Dashboard Metric #### Control the aggregation using ranges You may also want to give the user the ability to query data in different ranges. You can control what's passed in the dropdown using the' ranges' attribute. The array passed here will be parsed and displayed on the card. All integers are transformed to days, and other string variables will be passed as they are. You can also set a default range using the `initial_range` attribute. The ranges have been changed a bit since **version 2.8**. The parameter you pass to the `range` option will be directly passed to the [`options_for_select`](https://apidock.com/rails/v5.2.3/ActionView/Helpers/FormOptionsHelper/options_for_select) helper, so it behaves more like a regular `select_tag`. ```ruby{4-15} class UsersMetric < Avo::Dashboards::MetricCard self.id = 'users_metric' self.label = 'Users count' self.initial_range = 30 self.ranges = { "7 days": 7, "30 days": 30, "60 days": 60, "365 days": 365, Today: "TODAY", "Month to date": "MTD", "Quarter to date": "QTD", "Year to date": "YTD", All: "ALL" } end ``` #### Keep the data fresh If this dashboard is something that you keep on the big screen, you need to keep the data fresh at all times. That's easy using `refresh_every`. You pass the number of seconds you need to be refreshed and forget about it. Avo will do it for you. ```ruby{3} class UsersMetric < Avo::Dashboards::MetricCard self.id = 'users_metric' self.refresh_every = 10.minutes end ``` #### Hide the header In cases where you need to embed some content that should fill the whole card (like a map, for example), you can choose to hide the label and ranges dropdown. ```ruby{3} class UsersMetric < Avo::Dashboards::MetricCard self.id = 'users_metric' self.display_header = false end ``` Avo Dashboard Map card ### Metric card The metric card is your friend when you only need to display a simple big number on your dashboard. To generate one run `bin/rails g avo:card:metric users_metric`. Avo Dashboard Metric #### Calculate results To calculate your result, you may use the `query` method. After you make the query, use the `result` method to store the value displayed on the card. In the `query` method you have access to a few variables like `context` (the App context), `params` (the request params), `range` (the range that was requested), `dashboard` (the current dashboard the card is on), and current `card`. ```ruby{23-47,36} class UsersMetric < Avo::Dashboards::MetricCard self.id = 'users_metric' self.label = 'Users count' self.description = 'Some tiny description' self.cols = 1 # self.rows = 1 # self.initial_range = 30 # self.ranges = { # "7 days": 7, # "30 days": 30, # "60 days": 60, # "365 days": 365, # Today: "TODAY", # "Month to date": "MTD", # "Quarter to date": "QTD", # "Year to date": "YTD", # All: "ALL", # } # self.prefix = '$' # self.suffix = '%' # self.refresh_every = 10.minutes def query from = Date.today.midnight - 1.week to = DateTime.current if range.present? if range.to_s == range.to_i.to_s from = DateTime.current - range.to_i.days else case range when 'TODAY' from = DateTime.current.beginning_of_day when 'MTD' from = DateTime.current.beginning_of_month when 'QTD' from = DateTime.current.beginning_of_quarter when 'YTD' from = DateTime.current.beginning_of_year when 'ALL' from = Time.at(0) end end end result User.where(created_at: from..to).count end end ``` #### Decorate the data using `prefix` and `suffix` Some metrics might want to add a `prefix` or a `suffix` to display the data better. ```ruby{3,4} class UsersMetric < Avo::Dashboards::MetricCard self.id = 'users_metric' self.prefix = '$' self.suffix = '%' end ``` Avo Dashboard Prefix & suffix ### Chartkick card A picture is worth a thousand words. So maybe a chart a hundred? Who knows? But creating charts in Avo is very easy with the help of the [chartkick](https://github.com/ankane/chartkick) gem. You start by running `bin/rails g avo:card:chartkick users_chart`. ```ruby class UserSignups < Avo::Dashboards::ChartkickCard self.id = 'user_signups' self.label = 'User signups' self.chart_type = :area_chart self.description = 'Some tiny description' self.cols = 2 # self.rows = 1 # self.chart_options = { library: { plugins: { legend: { display: true } } } } # self.flush = true # self.legend = false # self.scale = false # self.legend_on_left = false # self.legend_on_right = false def query points = 16 i = Time.new.year.to_i - points base_data = Array .new(points) .map do i += 1 [i.to_s, rand(0..20)] end .to_h data = [ { name: 'batch 1', data: base_data.map { |k, v| [k, rand(0..20)] }.to_h }, { name: 'batch 2', data: base_data.map { |k, v| [k, rand(0..40)] }.to_h }, { name: 'batch 3', data: base_data.map { |k, v| [k, rand(0..10)] }.to_h } ] result data end end ``` Chartkick card #### Chart types Using the `self.chart_type` class attribute you can change the type of the chart. Supported types are `line_chart`, `pie_chart`, `column_chart`, `bar_chart`, `area_chart`, and `scatter_chart`. #### Customize chart Because the charts are being rendered with padding initially, we offset that before rendering to make the chart look good on the card. To disable that, you can set `self.flush = false`. That will set the chart loose for you to customize further. After you set `flush` to `false`, you can add/remove the `scale` and `legend`. You can also place the legend on the left or right using `legend_on_left` and `legend_on_right`. These are just some of the predefined options we provide out of the box, but you can send different [chartkick options](https://github.com/ankane/chartkick#options) to the chart using `chart_options`. If you'd like to use [Groupdate](https://github.com/ankane/groupdate), [Hightop](https://github.com/ankane/hightop), and [ActiveMedian](https://github.com/ankane/active_median) you should require them in your `Gemfile`. Only `chartkick` is required by default. `chart.js` is supported for the time being. So if you need support for other types, please reach out or post a PR (🙏 PRs are much appreciated). ### Partial card You can use a partial card to add custom content to a card. Generate one by running `bin/rails g avo:card:partial custom_card`. That will create the card class and the partial for it. ```ruby{5} class ExampleCustomPartial < Avo::Dashboards::PartialCard self.id = "users_custom_card" self.cols = 1 self.rows = 4 self.partial = "avo/cards/custom_card" # self.display_header = true end ``` Custom partial card You can embed a piece of content from another app using an iframe. You can hide the header using the `self.display_header = false` option. That will render the embedded content flush to the container. ```ruby{5} # app/avo/cards/map_card.rb class MapCard < Avo::Dashboards::PartialCard self.id = "map_card" self.label = "Map card" self.partial = "avo/cards/map_card" self.display_header = false self.cols = 2 self.rows = 4 end ``` ```html ``` Avo Dashboard Map card --- # Customization options ## Change the app name On the main navbar next to the logo, Avo generates a link to the homepage of your app. The label for the link is usually computed from your Rails app name. You can customize that however, you want using `config.app_name = 'Avocadelicious'`. Since Avo 2.32.6 the `app_name` option is callable using a block. This is useful if you want to reference a `I18n.t` method or something more dynamic. ```ruby Avo.configure do |config| config.app_name = -> { I18n.t "app_name" } end ``` ## Timezone and Currency Your data-rich app might have a few fields where you reference `date`, `datetime`, and `currency` fields. You may customize the global timezone and currency with `config.timezone = 'UTC'` and `config.currency = 'USD'` config options. ## Resource Index view There are a few customization options to change how resources are displayed in the **Index** view. ### Resources per page You may customize how many resources you can view per page with `config.per_page = 24`. Per page config ### Per page steps Similarly customize the per-page steps in the per-page picker with `config.per_page_steps = [12, 24, 48, 72]`. Per page config ### Resources via per page For `has_many` associations you can control how many resources are visible in their `Index view` with `config.via_per_page = 8`. ### Default view type The `ResourceIndex` component supports two view types `:table` and `:grid`. You can change that by `config.default_view_type = :table`. Read more on the grid view configuration page.
Table view Table view
Grid view Grid view
## ID links to resource On the **Index** view, each row has the controls component at the end, which allows the user to go to the **Show** and **Edit** views and delete that entry. If you have a long row and a not-so-wide display, it might not be easy to scroll to the right-most section to click the **Show** link. You can enable the `id_links_to_resource` config option to make it easier. ```ruby{4} Avo.configure do |config| config.root_path = '/avo' config.app_name = 'Avocadelicious' config.id_links_to_resource = true end ``` That will render all `id` fields in the **Index** view as a link to that resource. As link to resource ## Resource controls on the left side By default, the resource controls are located on the right side of the record rows, which might be hidden if there are a lot of columns. You might want to move the controls to the left side in that situation using the `resource_controls_placement` option. ```ruby{2} Avo.configure do |config| config.resource_controls_placement = :left end ``` Resource controls on the left side ## Container width ```ruby{2-3} Avo.configure do |config| config.full_width_index_view = false config.full_width_container = false end ``` Avo's default main content is constrained to a regular [Tailwind CSS container](https://tailwindcss.com/docs/container). If you have a lot of content or prefer to display it full-width, you have two options. ### Display the `Index` view full-width Using `full_width_index_view: true` tells Avo to display the **Index** view full-width. ### Display all views full-width Using `full_width_container: true` tells Avo to display all views full-width. ## Cache resources on the `Index` view Avo caches each resource row (or Grid item for Grid view) for performance reasons. You can disable that cache using the `cache_resources_on_index_view` configuration option. The cache key is using the record's `id` and `created_at` attributes and the resource file `md5`. :::info If you use the `visibility` option to show/hide fields based on the user's role, you should disable this setting. ::: ```ruby{2} Avo.configure do |config| config.cache_resources_on_index_view = false end ``` ## Context In the `Resource` and `Action` classes, you have a global `context` object to which you can attach a custom payload. For example, you may add the `current_user`, the current request `params`, or any other arbitrary data. You can configure it using the `set_context` method in your initializer. The block you pass in will be instance evaluated in `Avo::ApplicationController`, so it will have access to the `current_user` method or `Current` object. ```ruby{2-8} Avo.configure do |config| config.set_context do { foo: 'bar', params: request.params, } end end ``` :::warning `_current_user` It's recommended you don't store your current user here but using the `current_user_method` config. ::: You can access the context data with `::Avo::App.context` object. ## Eject views If you want to change one of Avo's built-in views, you can eject it, update it and use it in your admin. ### Prepared templates We prepared a few templates to make it. `bin/rails generate avo:eject :logo` will eject the `_logo.html.erb` partial. ``` ▶ bin/rails generate avo:eject :logo Running via Spring preloader in process 20947 create app/views/avo/logo/_logo.html.erb ``` A list of prepared templates: - `:logo` ➡️   `app/views/avo/partials/_logo.html.erb` - `:head` ➡️   `app/views/avo/partials/_head.html.erb` - `:header` ➡️   `app/views/avo/partials/_header.html.erb` - `:footer` ➡️   `app/views/avo/partials/_footer.html.erb` - `:scripts` ➡️   `app/views/avo/partials/_scripts.html.erb` - `:sidebar_extra` ➡️   `app/views/avo/partials/_sidebar_extra.html.erb` #### Logo In the `app/views/avo/partials` directory, you will find the `_logo.html.erb` partial, which you may customize however you want. It will be displayed in place of Avo's logo. Avo logo customization #### Header The `_header.html.erb` partial enables you to customize the name and link of your app. Avo header customization #### Footer The `_footer.html.erb` partial enables you to customize the footer of your admin. Avo footer customization #### Scripts The `_scripts.html.erb` partial enables you to insert scripts in the footer of your admin. ### Eject any template You can eject any partial from Avo using the partial path. ``` ▶ bin/rails generate avo:eject app/views/layouts/avo/application.html.erb create app/views/layouts/avo/application.html.erb ``` :::warning Once ejected, the views will not receive updates on new Avo releases. You must maintain them yourself. ::: ## Breadcrumbs By default, Avo ships with breadcrumbs enabled. Avo breadcrumbs You may disable them using the `display_breadcrumbs` configuration option. ```ruby{2} Avo.configure do |config| config.display_breadcrumbs = false end ``` The first item on the breadcrumb is **Home** with the `root_path` URL. You can customize that using the `set_initial_breadcrumbs` block. ```ruby{2-5} Avo.configure do |config| config.set_initial_breadcrumbs do add_breadcrumb "Casa", root_path add_breadcrumb "Something else", something_other_path end end ``` Avo uses the [breadcrumbs_on_rails](https://github.com/weppos/breadcrumbs_on_rails) gem under the hood. ### Breadcrumbs for custom pages You can add breadcrumbs to custom pages in the controller action. ```ruby{3} class Avo::ToolsController < Avo::ApplicationController def custom_tool add_breadcrumb "Custom tool" end end ``` ## Page titles When you want to update the page title for a custom tool or page, you only need to assign a value to the `@page_title` instance variable in the controller method. ```ruby{3} class Avo::ToolsController < Avo::ApplicationController def custom_tool @page_title = "Custom tool page title" end end ``` Avo uses the [meta-tags](https://github.com/kpumuk/meta-tags) gem to compile and render the page title. ## Home path When a user clicks your logo inside Avo or goes to the `/avo` URL, they will be redirected to one of your resources. You might want to change that path to something else, like a custom page. You can do that with the `home_path` configuration. ```ruby{2} Avo.configure do |config| config.home_path = "/avo/dashboard" end ``` ### Use a lambda function for the home_path You can also use a lambda function to define that path. ```ruby{2} Avo.configure do |config| config.home_path = -> { avo.dashboard_path(:dashy) } end ``` When you configure the `home_path` option, the `Get started` sidebar item will be hidden in the development environment. Now, users will be redirected to `/avo/dashboard` whenever they click the logo. You can use this configuration option alongside the `set_initial_breadcrumbs` option to create a more cohesive experience. ```ruby{2-5} Avo.configure do |config| config.home_path = "/avo/dashboard" config.set_initial_breadcrumbs do add_breadcrumb "Dashboard", "/avo/dashboard" end end ``` ## Mount Avo under a nested path You may need to mount Avo under a nested path, something like `/uk/admin`. In order to do that, you need to consider a few things. 1. Move the engine mount point below any route for custom tools. ```ruby{7,10} Rails.application.routes.draw do # other routes authenticate :user, ->(user) { user.is_admin? } do scope :uk do scope :admin do get "dashboard", to: "avo/tools#dashboard" # custom tool added before engine end mount Avo::Engine, at: Avo.configuration.root_path # engine mounted last end end end ``` 2. The `root_path` configuration should only be the last path segment. ```ruby # 🚫 Don't add the scope to the root_path Avo.configure do |config| config.root_path = "/uk/admin" end # ✅ Do this instead Avo.configure do |config| config.root_path = "/admin" end ``` 3. Use full paths for other configurations. ```ruby Avo.configure do |config| config.home_path = "/uk/admin/dashboard" config.set_initial_breadcrumbs do add_breadcrumb "Dashboard", "/uk/admin/dashboard" end end ``` ## Custom `view_component` path You may not keep your view components under `app/components` and want the generated field `view_component`s to be generated in your custom directory. You can change that using the `view_component_path` configuration key. ```ruby Avo.configure do |config| config.view_component_path = "app/frontend/components" end ``` ## Custom query scopes You may want to change Avo's queries to add sorting or use gems like [friendly](https://github.com/norman/friendly_id). You can do that using `resolve_query_scope` for multiple records and `resolve_find_scope` when fetching one record. ### Custom scope for `Index` page Using `resolve_query_scope` you tell Avo how to fetch the records for the `Index` view. ```ruby class UserResource < Avo::BaseResource self.resolve_query_scope = ->(model_class:) do model_class.order(last_name: :asc) end end ``` ### Custom scope for `Show` and `Edit` pages :::warning The `resolve_find_scope` method is deprecated in favor of `find_record_method` (below). ::: :::details If you're following the `friendly_id` example, you must also add the `friendly_id` configuration to the model definition. ```ruby class User < ApplicationRecord extend FriendlyId friendly_id :name, use: :slugged end ``` ::: ### Custom find method for `Show` and `Edit` pages Using `find_record_method` you tell Avo how to fetch one record for `Show` and `Edit` views and other contexts where a record needs to be fetched from the database. This is very useful when you use something like `friendly` gem, custom `to_param` methods on your model, and even the wonderful `prefix_id` gem. ### Custom `to_param` method The following example shows how you can update the `to_param` (to use the post name) method on the `User` model to use a custom attribute and then update the `UserResource` so it knows how to search for that model. ::: code-group ```ruby [app/avo/resources/user_resource.rb] class PostResource < Avo::BaseResource self.find_record_method = ->(model_class:, id:, params:) do # If the id is an integer use the classic `find` method. # But if it's not an integer, search for that post by the slug. id.to_i == 0 ? model_class.find_by_slug(id) : model_class.find(id) end end ``` ```ruby [app/models/post.rb] class Post < ApplicationRecord before_save :update_slug def to_param slug || id end def update_slug self.slug = name.parameterize end end ``` ::: #### Using the `friendly` gem ::: code-group ```ruby [app/avo/resources/user_resource.rb] class UserResource < Avo::BaseResource self.find_record_method = ->(model_class:, id:, params:) do # We have to add .friendly to the query model_class.friendly.find! id end end ``` ```ruby [app/models/user.rb] class User < ApplicationRecord extend FriendlyId friendly_id :name, use: :slugged end ``` ::: #### Using `prefixed_ids` gem You really don't have to do anything on Avo's side for this to work. You only need to add the `has_prefix_id` the model as per the documentation. Avo will know how to search for the record. ```ruby class Course < ApplicationRecord has_prefix_id :course end ``` ## Disable features You might want to disable some Avo features. You can do that using the `disabled_features` option. ```ruby{3} # config/initializers/avo.rb Avo.configure do |config| config.disabled_features = [:global_search] end ``` After this setting, the global search will be hidden for users. Supported options: - `global_search` ## Customize profile name, photo, and title You might see on the sidebar footer a small profile widget. The widget displays three types of information about the user; `name`, `photo`, and `title`. ### Customize the name of the user Avo checks to see if the object returned by your `current_user_method` responds to a `name` method. If not, it will try the `email` method and then fall back to `Avo user`. ### Customize the profile photo Similarly, it will check if that current user responds to `avatar` and use that as the `src` of the photo. ### Customize the title of the user Lastly, it will check if it responds to the `avo_title` method and uses that to display it under the name. ### Customize the sign-out link Please follow this guide in authentication. ## Skip show view
In the CRUD interface Avo adds the view by default. This means that when your users will see the view icon to go to that detail page and they will be redirected to the page when doing certain tasks (update a record, run an action, etc.). You might not want that behavior and you might not use the view at all and prefer to skip that and just use the view. Adding `config.resource_default_view = :edit` to your `avo.rb` configuration file will tell Avo to skip it and use the view as the default resource view. ```ruby{3} # config/initializers/avo.rb Avo.configure do |config| config.resource_default_view = :edit end ``` ![](/assets/img/customization/skip_show_view.gif) --- # Grid view
Avo grid view Some resources are best displayed in a grid view. We can do that with Avo using a `cover`, a `title`, and a `body`. ## Enable grid view To enable grid view for a resource, you need to add the `grid` block. That will add the view switcher to the **Index** view. ```ruby class PostResource < Avo::BaseResource # ... grid do cover :cover_photo, as: :file, link_to_resource: true title :name, as: :text, required: true, link_to_resource: true body :excerpt, as: :text end end ``` Avo view switcher ## Make default view To make the grid the default way of viewing a resource **Index**, we have to use the `default_view_type` class attribute. ```ruby{7} class PostResource < Avo::BaseResource self.default_view_type = :grid end ``` ## Fields configuration Besides the regular `field` methods, you should add a new `grid` block configuring the grid fields. The main difference is that the fields are not declared using the `field` class method but three new ones `cover`, `title`, and `body` ```ruby{9-13} class PostResource < Avo::BaseResource self.default_view_type = :grid field :id, as: :id field :name, as: :text, required: true field :body, as: :textarea field :cover_photo, as: :file, is_image: true grid do cover :cover_photo, as: :file, is_image: true title :name, as: :text body :body, as: :textarea end end ``` That will render the `Post` resource index view as a **Grid view** using the selected fields. Avo will also display a button to toggle between the view types `:grid` and `:table`. These fields take the same options as those in the `fields` method, so you can configure them however you want. For example, in the **Grid view**, you might want to truncate the `:body` to a certain length and use an external image for the cover you compute on the fly. And also, render the `:cover` and the `:title` fields as links to that resource with `link_to_resource: true`. ```ruby grid do cover :logo, as: :external_image, link_to_resource: true do |model| if model.url.present? "//logo.clearbit.com/#{URI.parse(model.url).host}?size=180" end end title :name, as: :text, link_to_resource: true body :excerpt, as: :text do |model| begin ActionView::Base.full_sanitizer.sanitize(model.body).truncate 130 rescue => exception '' end end end ``` ## Use a computed field for the `cover` field A common use case is to have the assets stored on a separate model and would like to display an image from that related association. ```ruby class Post < ApplicationRecord has_many :post_assets end class PostAssets < ApplicationRecord belongs_to :post has_one_attached :image end ``` Luckily, the `grid` display can be a computed field too ```ruby grid do cover :image, as: :file, is_image: true, link_to_resource: true do |model| # we find the first asset association and use it's image attachment model.post_assets.first.image end end ``` --- # Map view Some resources that contain geospatial data can benefit from being displayed on a map. For resources to be displayed to the map view they require a `coordinates` field, but that's customizable. ## Enable map view To enable map view for a resource, you need to add the `map_view` class attribtue to a resource. That will add the view switcher to the view. Avo view switcher ```ruby class CityResource < Avo::BaseResource # ... self.map_view = { mapkick_options: { controls: true }, record_marker: -> { { latitude: record.coordinates.first, longitude: record.coordinates.last, tooltip: record.name } }, table: { visible: true, layout: :right } } end ``` ## Make it the default view To make the map view the default way of viewing a resource on , we have to use the `default_view_type` class attribute. ```ruby{7} class CityResource < Avo::BaseResource self.default_view_type = :map end ``` --- # Menu editor One common task you need to do is organize your sidebar resources into menus. You can easily do that using the menu editor in the initializer. When you start with Avo, you'll get an auto-generated sidebar by default. That sidebar will contain all your resources, dashboards, and custom tools. To customize that menu, you have to add the `main_menu` key to your initializer. ```ruby{3-22} # config/initializers/avo.rb Avo.configure do |config| config.main_menu = -> { section "Resources", icon: "heroicons/outline/academic-cap" do group "Academia" do resource :course resource :course_link end group "Blog", collapsable: true, collapsed: true do dashboard :dashy resource :post resource :comment end end section I18n.t('avo.other'), icon: "heroicons/outline/finger-print", collapsable: true, collapsed: true do link_to 'Avo HQ', path: 'https://avohq.io', target: :_blank link_to 'Jumpstart Rails', path: 'https://jumpstartrails.com/', target: :_blank end } end ``` Avo main menu For now, Avo supports editing only two menus, `main_menu` and `profile_menu`. However, that might change in the future by allowing you to write custom menus for other parts of your app. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.main_menu = -> { section I18n.t("avo.dashboards"), icon: "dashboards" do dashboard :dashy, visible: -> { true } dashboard :sales, visible: -> { true } group "All dashboards", visible: false do all_dashboards end end section "Resources", icon: "heroicons/outline/academic-cap" do group "Academia" do resource :course resource :course_link end group "Blog" do resource :posts resource :comments end group "Other" do resource :fish end end section "Tools", icon: "heroicons/outline/finger-print" do all_tools end group do link_to "Avo", path: "https://avohq.io" link_to "Google", path: "https://google.com", target: :_blank end } config.profile_menu = -> { link_to "Profile", path: "/profile", icon: "user-circle" } end ``` ## Menu item types A few menu item types are supported `link_to`, `section`, `group`, `resource`, and `dashboard`. There are a few helpers too, like `all_resources`, `all_dashboards`, and `all_tools`. ### `all_` helpers ```ruby section "App", icon: "heroicons/outline/beaker" do group "Dashboards", icon: "dashboards" do all_dashboards end group "Resources", icon: "resources" do all_resources end group "All tools", icon: "tools" do all_tools end end ``` :::warning The `all_resources` helper is taking into account your authorization rules, so make sure you have `def index?` enabled in your resource policy. ::: Avo menu editor ## Item visibility The `visible` option is available on all menu items. It can be a boolean or a block that has access to a few things: - the `current_user`. Given that you set a way for Avo to know who the current user is, that will be available in that block call - the `context` object. - the `params` object of that current request - the [`view_context`](https://apidock.com/rails/AbstractController/Rendering/view_context) object. The `view_context` object lets you use the route helpers. eg: `view_context.main_app.posts_path`. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.main_menu = -> { resource :user, visible: -> do context[:something] == :something_else end } end ``` ## Add `data` attributes to items You may want to add special data attributes to some items and you can do that using the `data` option. For example you may add `data: {turbo: false}` to make a regular request for a link. ```ruby{4} # config/initializers/avo.rb Avo.configure do |config| config.main_menu = -> { resource :user, data: {turbo: false} } end ``` ## Using authorization rules When you switch from a generated menu to a custom one, you might want to keep using the same authorization rules as before. To quickly do that, use the `authorize` method in the `visible` option. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.main_menu = -> { resource :team, visible: -> do # authorize current_user, MODEL_THAT_NEEDS_TO_BE_AUTHORIZED, METHOD_THAT_NEEDS_TO_BE_AUTHORIZED authorize current_user, Team, "index?", raise_exception: false end } end ``` ## Icons For [`Section`](#section)s, you can use icons to make them look better. You can use some local ones that we used throughout the app and all [heroicons](https://heroicons.com/) designed by [Steve Schoger](https://twitter.com/steveschoger). In addition, you can use the `solid` or `outline` versions. We used the `outline` version throughout the app. ```ruby section "Resources", icon: "heroicons/outline/academic-cap" do resource :course end section "Resources", icon: "heroicons/solid/finger-print" do resource :course end section "Resources", icon: "heroicons/outline/adjustments" do resource :course end ``` Avo menu editor ### Icons on resource, dashboard, and link_to items Since [2.36](https://avohq.io/releases/2.36) you can add icons to other menu items like `resource`, `dashboard`, and `link_to`. ```ruby link_to "Avo", "https://avohq.io", icon: "globe" ``` ## Collapsable sections and groups When you have a lot of items they can take up a lot of vertical space. You can choose to make those sidebar sections collapsable by you or your users. ```ruby section "Resources", icon: "resources", collapsable: true do resource :course end ``` Avo menu editor That will add the arrow icon next to the section to indicate it's collapsable. So when your users collapse and expand it, their choice will be stored in Local Storage and remembered in that browser. ### Default collapsed state You can however, set a default collapsed state using the `collapsed` option. ```ruby section "Resources", icon: "resources", collapsable: true, collapsed: true do resource :course end ``` Avo menu editor You might want to allow your users to hide certain items from view. ## Authorization If you use the authorization feature, you will need an easy way to authorize your items in the menu builder. For that scenario, we added the `authorize` helper. ```ruby{3} Avo.configure do |config| config.main_menu = -> { resource :team, visible: -> { # authorize current_user, THE_RESOURCE_MODEL, THE_POLICY_METHOD, raise_exception: false authorize current_user, Team, "index?", raise_exception: false } } end ``` Use it in the `visible` block by giving it the `current_user` (which is available in that block), the class of the resource, the method that you'd like to authorize for (default is `index?`), and tell it not to throw an exception. Now, the item visibility will use the `index?` method from the `TeamPolicy` class. ## Profile menu The profile menu allows you to add items to the menu displayed in the profile component. **The sign-out link is automatically added for you.** You may add the `icon` option to the `profile_menu` links. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.profile_menu = -> { link_to "Profile", path: "/profile", icon: "user-circle" } end ``` Avo profile menu ## Forms in profile menu It's common to have forms that `POST` to a path to do sign ut a user. For this scenario we added the `method` and `params` option to the profile item `link_to`, so if you have a custom sign out path you can do things like this. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.profile_menu = -> { link_to "Sign out", path: main_app.destroy_user_session_path, icon: "user-circle", method: :post, params: {custom_param: :here} } end ``` ## Custom content in the profile menu You might, however, wnat to add a very custom form or more items to the profile menu. For that we prepared the `_profile_menu_extra.html.erb` partial for you. ```bash bin/rails generate avo:eject :profile_menu_extra ``` This will eject the partial and you can add whatever custom content you might need. ```erb <%# Example link below %> <%#= render Avo::ProfileItemComponent.new label: 'Profile', path: '/profile', icon: 'user-circle' %> ``` --- # Search Finding what you're looking for fast is essential. That's why Avo leverages [ransack's](https://github.com/activerecord-hackery/ransack) powerful query language. :::info While we show you examples using `ransack`, you can use other search engines, so `ransack` is not mandatory. ::: :::warning If you're using the authorization feature, ensure [you authorize the action](#authorize-search). ::: First, you need to add `ransack` as a dependency to your app (breaking change from Avo v1.10). ```ruby # Gemfile gem 'ransack' ``` ## Enable search for a resource To enable search for a resource, you need to add the `search_query` class variable to the resource file. ```ruby{3-5} class UserResource < Avo::BaseResource self.title = :name self.search_query = -> do scope.ransack(id_eq: params[:q], first_name_cont: params[:q], last_name_cont: params[:q], m: "or").result(distinct: false) end # fields go here end ``` The `search_query` block passes over the `params` object that holds the `q` param, the actual query string. It also provides the `scope` variable on which you run the query. That ensures that the authorization scopes have been appropriately applied. In this block, you may configure the search however strict or loose you need it. Check out [ransack's search matchers](https://github.com/activerecord-hackery/ransack#search-matchers) to compose the query better. :::warning If you're using ransack version 4 and up you must add `ransackable_attributes` and maybe more to your model in order for it to work. Read more about it [here](https://activerecord-hackery.github.io/ransack/going-further/other-notes/#authorization-allowlistingdenylisting). ::: ## Authorize search Since Avo 2.29 search is authorized in policy files using the `search?` method. ```ruby class UserPolicy < ApplicationPolicy def search? true end end ``` If the `search?` method returns false, the search operation for that resource is not going to show up in the global search and the search box on index is not going to be displayed. If you're using `search?` already in your policy file, you can alias it to some other method in you initializer using the `config.authorization_methods` config. More about that on the authorization page. ```ruby Avo.configure do |config| config.authorization_methods = { search: 'avo_search?', } end ``` ## Configure the search result ### Label By default, the search results will be displayed as text. The text label will be the title column you previously configured. Blank search You may configure that to be something more complex using the `as_label` option. That will take the final value of that field and display it as the label of the search result. ```ruby{9-11} class PostResource < Avo::BaseResource self.title = :name self.search_query = -> do scope.ransack(id_eq: params[:q], m: "or").result(distinct: false) end field :id, as: :id field :name, as: :text, required: true, as_label: true field :complex_name, as: :text, hide_on: :all, as_label: true do |model| "[#{model.id}]#{model.name}" end end ``` Search label Notice the `hide_on: :all` option used to hide the computed `complex_name` attribute from the rest of the views. That is because you **may or may not** want to show that attribute in other views. ### Description
Search Description is a pro feature
You might want to show more than just the title in the search result. Avo provides the `as_description` option to add some more information. ```ruby{12-16} class PostResource < Avo::BaseResource self.title = :name self.search_query = -> do scope.ransack(id_eq: params[:q], m: "or").result(distinct: false) end field :id, as: :id field :name, as: :text, required: true, as_label: true field :complex_name, as: :text, hide_on: :all, as_label: true do |model| "[#{model.id}]#{model.name}" end field :excerpt, as: :text, as_description: true do |model| ActionView::Base.full_sanitizer.sanitize(model.body).truncate 130 rescue "" end end ``` Search description ### Avatar * Search Avatar is a [Pro feature](https://avohq.io/purchase/pro). You may improve the results listing by adding an avatar to each search result. You do that by using the `as_avatar` attribute. This attribute has three options `:square`, `:rounded` or `:circle`. That influences the final roundness of the avatar. ```ruby{17} class PostResource < Avo::BaseResource self.title = :name self.search_query = -> do scope.ransack(id_eq: params[:q], m: "or").result(distinct: false) end field :id, as: :id field :name, as: :text, required: true, as_label: true field :complex_name, as: :text, hide_on: :all, as_label: true do |model| "[#{model.id}]#{model.name}" end field :excerpt, as: :text, as_description: true do |model| ActionView::Base.full_sanitizer.sanitize(model.body).truncate 130 rescue "" end field :cover_photo, as: :file, is_image: true, as_avatar: :rounded end ``` Search avatar ### Header Help Text You may improve the results listing header by adding a piece of text highlighting the fields you are looking for or any other instruction for the user. You do that by using the `search_query_help` attribute. This attribute takes a string and appends it to the title of the resource. Search Header Help ```ruby{6} class PostResource < Avo::BaseResource self.title = :name self.search_query = -> do scope.ransack(id_eq: params[:q], m: "or").result(distinct: false) end self.search_query_help = "- search by id" field :id, as: :id end ``` ## Resource search When a resource has the `search_query` attribute, a new search input will be displayed on the `Index` view. ![](/assets/img/search/resource_search.jpg) ## Global search Avo also has a global search feature. It will search through all the resources that have the `search_query` attribute present. You open the global search input by clicking the trigger on the navbar or by using the CMD + K keyboard shortcut (Ctrl + K on windows). Global search trigger ### Hide the global search If you, by any chance, want to hide the global search, you can do so using this setting 👇 ```ruby{3} # config/initializers/avo.rb Avo.configure do |config| config.disabled_features = [:global_search] end ``` ## Hide a resource from the global search You might have a resource that you'd like to be able to perform a search on when on its `Index` page but not have it present in the global search. You can hide it using `hide_from_global_search = true`. ```ruby{8} class TeamMembershipResource < Avo::BaseResource self.title = :id self.includes = [:user, :team] self.visible_on_sidebar = false self.search_query = -> do scope.ransack(id_eq: params[:q], m: "or").result(distinct: false) end self.hide_from_global_search = true field :id, as: :id field :user, as: :belongs_to field :team, as: :belongs_to end ``` ### Scope out global or resource searches You may want to perform different searches on the `global` search from the `resource` search. You may use the `params[:global]` flag to figure that out. ```ruby{8} class OrderResource < Avo::BaseResource self.search_query = -> do if params[:global] # Perform global search scope.ransack(id_eq: params[:q], m: "or").result(distinct: false) else # Perform resource search scope.ransack(id_eq: params[:q], details_cont: params[:q], m: "or").result(distinct: false) end end end ``` ## Search result path By default, when a user clicks on a search result, they will be redirected to that record, but you can change that using the `search_result_path` option. ```ruby class CityResource < Avo::BaseResource self.search_result_path = -> { # Return any path here. You have access to the search `record` the user clicked on. avo.resources_city_path record, custom: "yup" } end ``` --- # Filters Filters allow you to better scope the index queries for records you are looking for. ## Defining filters Avo has two types of filters available at the moment [Boolean filter](#boolean-filter) and [Select filter](#select-filter). Avo filters ### Filter values Because the filters get serialized back and forth, the final `value`/`values` in the `apply` method will be stringified or have the keys stringified if they are hashes. You can declare them as regular hashes (with the keys symbolized) in the `options` method, but they will get stringified in the end. ## Boolean Filter You generate one running `bin/rails generate avo:filter featured_filter`, creating a filter configuration file. ```ruby class FeaturedFilter < Avo::Filters::BooleanFilter self.name = 'Featured filter' # `values` comes as a hash with stringified keys # Eg: # { # 'is_featured': true # } def apply(request, query, values) return query if values['is_featured'] && values['is_unfeatured'] if values['is_featured'] query = query.where(is_featured: true) elsif values['is_unfeatured'] query = query.where(is_featured: false) end query end def options { is_featured: "Featured", is_unfeatured: "Unfeatured" } end # Optional method to set the default state. # def default # { # is_featured: true # } # end end ``` Each filter file comes with a `name`, `apply`, and `options` methods. The `name` method lets you set the name of the filter. The `apply` method is responsible for filtering out the records by giving you access to modify the `query` object. The `apply` method also gives you access to the current `request` object and the passed `values`. The `values` object is a `Hash` containing all the configured `options` with the option name as the key and `true`/`false` as the value. ```ruby # Example values payload { 'is_featured': true, 'is_unfeatured': false, } ``` The `options` method defines the available values of your filter. They should return a `Hash` with the option id as a key and option label as value. ### Default value You can set a default value to the filter, so it has a predetermined state on load. To do that, return the state you desire from the `default` method. ```ruby{23-27} class FeaturedFilter < Avo::Filters::BooleanFilter self.name = 'Featured status' def apply(request, query, values) return query if values['is_featured'] && values['is_unfeatured'] if values['is_featured'] query = query.where(is_featured: true) elsif values['is_unfeatured'] query = query.where(is_featured: false) end query end def options { is_featured: "Featured", is_unfeatured: "Unfeatured" } end def default { is_featured: true } end end ``` ## Select Filter Select filters are similar to Boolean ones. For example, you generate one running `rails generate avo:filter published_filter --select`. The most significant difference from the **Boolean filter** is in the `apply` method. You only get back one `value` attribute, which represents which entry from the `options` method is selected. A finished, select filter might look like this. ```ruby class PublishedFilter < Avo::Filters::SelectFilter self.name = 'Published status' # `value` comes as a string # Eg: 'published' def apply(request, query, value) case value when 'published' query.where.not(published_at: nil) when 'unpublished' query.where(published_at: nil) else query end end def options { published: "Published", unpublished: "Unpublished" } end # Optional method to set the default state. # def default # :published # end end ``` ### Default value The select filter supports setting a default too. That should be a string or symbol with the select item. It will be stringified by Avo automatically. ```ruby{22-24} class PublishedFilter < Avo::Filters::SelectFilter self.name = 'Published status' def apply(request, query, value) case value when 'published' query.where.not(published_at: nil) when 'unpublished' query.where(published_at: nil) else query end end def options { 'published': 'Published', 'unpublished': 'Unpublished', } end def default :published end end ``` ## Multiple select filter You may also use a multiple select filter. ```ruby class PostStatusFilter < Avo::Filters::MultipleSelectFilter self.name = "Status" # `value` comes as an array of strings # Ex: ['admins', 'non_admins'] def apply(request, query, value) if value.include? 'admins' query = query.admins end if value.include? 'non_admins' query = query.non_admins end query end def options { admins: "Admins", non_admins: "Non admins", } end # Optional method to set the default state. # def default # ['admins', 'non_admins'] # end end ``` Avo multiple select filter ## Dynamic options The select filter can also take dynamic options: ```ruby{15-17} class AuthorFilter < Avo::Filters::SelectFilter self.name = 'Author' def apply(request, query, value) query = query.where(author_id: value) if value.present? query end # Example `applied_filters` # applied_filters = { # "CourseCountryFilter" => { # "USA" => true, # "Japan" => true, # "Spain" => false, # "Thailand" => false, # } # } def options # Here you have access to the `applied_filters` object too Author.select(:id, :name).each_with_object({}) { |author, options| options[author.id] = author.name } end end ``` ## Text Filter You can add complex text filters to Avo by running `rails generate avo:filter name_filter --text`. ```ruby class NameFilter < Avo::Filters::TextFilter self.name = "Name filter" self.button_label = "Filter by name" # `value` comes as text # Eg: 'avo' def apply(request, query, value) query.where('LOWER(name) LIKE ?', "%#{value}%") end # def default # 'avo' # end end ``` ## Default value You may set default values for the `options` you set. For example you may set which option to be selected for the [select filter](#select_filter) and which checkboxes to be set for the [boolean filter](#boolean_filter). In the `default` method you have access to the `request`, `params`, `context`, `view_context`, and `current_user` objects. ## Registering filters To add a filter to one of your resources, you need to declare it on the resource using the `filter` method to which you pass the filter class. ```ruby{8} class PostResource < Avo::BaseResource self.title = :name self.search = :id field :id, as: :id # other fields filter PublishedFilter end ``` ## Dynamic filter options You might want to compose more advanced filters, like when you have two filters, one for the country and another for cities, and you'd like to have the cities one populated with cities from the selected country. Let's take the `CourseResource` as an example. ```ruby{3-5,7-14} # app/models/course.rb class Course < ApplicationRecord def self.countries ["USA", "Japan", "Spain", "Thailand"] end def self.cities { USA: ["New York", "Los Angeles", "San Francisco", "Boston", "Philadelphia"], Japan: ["Tokyo", "Osaka", "Kyoto", "Hiroshima", "Yokohama", "Nagoya", "Kobe"], Spain: ["Madrid", "Valencia", "Barcelona"], Thailand: ["Chiang Mai", "Bangkok", "Phuket"] } end end ``` We will create two filters—one for choosing countries and another for cities. ```ruby{3-4} # app/avo/resources/course_resource.rb class CourseResource < Avo::BaseResource filter CourseCountryFilter filter CourseCityFilter end ``` The country filter is pretty straightforward. Set the query so the `country` field to be one of the selected countries and the `options` are the available countries as `Hash`. ```ruby{6,10} # app/avo/filters/course_country_filter.rb class CourseCountryFilter < Avo::Filters::BooleanFilter self.name = "Course country filter" def apply(request, query, values) query.where(country: values.select { |country, selected| selected }.keys) end def options Course.countries.map { |country| [country, country] }.to_h end end ``` The cities filter has a few more methods to manage the data better, but the gist is the same. The `query` makes sure the records have the city value in one of the cities that have been selected. The `options` method gets the selected countries from the countries filter (`CourseCountryFilter`) and formats them to a `Hash`. ```ruby{6,10} # app/avo/filters/course_city_filter.rb class CourseCityFilter < Avo::Filters::BooleanFilter self.name = "Course city filter" def apply(request, query, values) query.where(city: values.select { |city, selected| selected }.keys) end def options cities_for_countries countries end private # Get a hash of cities for certain countries # Example payload: # countries = ["USA", "Japan"] def cities_for_countries(countries_array = []) countries_array .map do |country| # Get the cities for this country Course.cities.stringify_keys[country] end .flatten # Prepare to transform to a Hash .map { |city| [city, city] } # Turn to a Hash .to_h end # Get the value of the selected countries # Example payload: # applied_filters = { # "CourseCountryFilter" => { # "USA" => true, # "Japan" => true, # "Spain" => false, # "Thailand" => false, # } # } def countries if applied_filters["CourseCountryFilter"].present? # Fetch the value of the countries filter applied_filters["CourseCountryFilter"] # Keep only the ones selected .select { |country, selected| selected } # Pluck the name of the coutnry .keys else # Return empty array [] end end end ``` Avo filters The `countries` method above will check if the `CourseCountryFilter` has anything selected. If so, get the names of the chosen ones. This way, you show only the cities from the selected countries and not all of them. ## React to filters Going further with the example above, a filter can react to other filters. For example, let's say that when a user selects `USA` from the list of countries, you want to display a list of cities from the USA (that's already happening in `options`), and you'd like to select the first one on the list. You can do that with the `react` method. ```ruby{13-28} # app/avo/filters/course_city_filter.rb class CourseCityFilter < Avo::Filters::BooleanFilter self.name = "Course city filter" def apply(request, query, values) query.where(city: values.select { |city, selected| selected }.keys) end def options cities_for_countries countries end # applied_filters = { # "CourseCountryFilter" => { # "USA" => true, # "Japan" => true, # "Spain" => false, # "Thailand" => false, # } # } def react # Check if the user selected a country if applied_filters["CourseCountryFilter"].present? && applied_filters["CourseCityFilter"].blank? # Get the selected countries, get their cities, and select the first one. selected_countries = applied_filters["CourseCountryFilter"].select do |name, selected| selected end # Get the first city cities = cities_for_countries(selected_countries.keys) first_city = cities.first.first # Return the first city as selected [[first_city, true]].to_h end end private # Get a hash of cities for certain countries # Example payload: # countries = ["USA", "Japan"] def cities_for_countries(countries_array = []) countries_array .map do |country| # Get the cities for this country Course.cities.stringify_keys[country] end .flatten # Prepare to transform to a Hash .map { |city| [city, city] } # Turn to a Hash .to_h end # Get the value of the selected countries # Example `applied_filters` payload: # applied_filters = { # "CourseCountryFilter" => { # "USA" => true, # "Japan" => true, # "Spain" => false, # "Thailand" => false, # } # } def countries if applied_filters["CourseCountryFilter"].present? # Fetch the value of the countries filter applied_filters["CourseCountryFilter"] # Keep only the ones selected .select { |country, selected| selected } # Pluck the name of the coutnry .keys else # Return empty array [] end end end ``` After all, filters are applied, the `react` method is called, so you have access to the `applied_filters` object. Using the applied filter payload, you can return the value of the current filter. ```ruby def react # Check if the user selected a country if applied_filters["CourseCountryFilter"].present? && applied_filters["CourseCityFilter"].blank? # Get the selected countries, get their cities, and select the first one. selected_countries = applied_filters["CourseCountryFilter"] .select do |name, selected| selected end # Get the first city cities = cities_for_countries(selected_countries.keys) first_city = cities.first.first # Return the first city selected as a Hash [[first_city, true]].to_h end end ``` Besides checking if the countries filter is populated (`applied_filters["CourseCountryFilter"].present?`), we also want to allow the user to customize the cities filter further, so we need to check if the user has added a value to that filter (`applied_filters["CourseCityFilter"].blank?`). If these conditions are true, the country filter has a value, and the user hasn't selected any values from the cities filter, we can react to it and set a value as the default one. Avo filters Of course, you can modify the logic and return all kinds of values based on your needs. ## Empty message text There might be times when you will want to show a message to the user when you're not returning any options. You may customize that message using the `empty_message` option. Avo filters ```ruby{4} # app/avo/filters/course_city_filter.rb class CourseCityFilter < Avo::Filters::BooleanFilter self.name = "Course city filter" self.empty_message = "Please select a country to view options." def apply(request, query, values) query.where(city: values.select { |city, selected| selected }.keys) end def options if countries.present? [] else ["Los Angeles", "New York"] end end private def countries # logic to fetch the countries end end ``` ## Keep filters panel open There are scenarios where you wouldn't want to close the filters panel when you change the values. For that, you can use the `keep_filters_panel_open` resource option. ```ruby{2} class CourseResource < Avo::BaseResource self.keep_filters_panel_open = true field :id, as: :id field :name, as: :text field :country, as: :select, options: Course.countries.map { |country| [country, country] }.to_h field :city, as: :select, options: Course.cities.values.flatten.map { |country| [country, country] }.to_h field :links, as: :has_many, searchable: true, placeholder: "Click to choose a link" filter CourseCountryFilter filter CourseCityFilter end ``` Avo filters ## Visibility You may want to manipulate your filter visibility on screens. You can do that using the `self.visible` attribute. Inside the visible block you can acces the following variables: ```ruby self.visible = -> do # You have access to: # block # context # current_user # params # parent_model # parent_resource # resource # view # view_context end ``` ## Filters arguments Filters can have different behaviors according to their host resource. In order to achieve that, arguments must be passed like on the example below: ```ruby{9-11} class FishResource < Avo::BaseResource self.title = :name field :id, as: :id field :name, as: :text field :user, as: :belongs_to field :type, as: :text, hide_on: :forms filter NameFilter, arguments: { case_insensitive: true } end ``` Now, the arguments can be accessed inside `NameFilter` ***`apply` method*** and on the ***`visible` block***! ```ruby{4-6,8-14} class NameFilter < Avo::Filters::TextFilter self.name = "Name filter" self.button_label = "Filter by name" self.visible = -> do arguments[:case_insensitive] end def apply(request, query, value) if arguments[:case_insensitive] query.where("LOWER(name) LIKE ?", "%#{value.downcase}%") else query.where("name LIKE ?", "%#{value}%") end end end ``` ## Manually create encoded URLs You may want to redirect users to filtered states of the view from other places in your app. In order to create those filtered states you may use these helpers functions or Rails helpers. ### Rails helpers ### Standalone helpers ## Persistent filters By default, when a user visits an view of a resource the filters payload will be empty, so they will be set on their [default values](#default-value). --- # Actions Avo actions allow you to perform specific tasks on one or more of your records. For example, you might want to mark a user as active/inactive and optionally send a message that may be customized by the person that wants to run the action. Once you attach an action to a resource using the `action` method, it will appear in the **Actions** dropdown. By default, actions appear on the `Index`, `Show`, and `Edit` views. Versions previous to 2.9 would only display the actions on the `Index` and `Show` views. !Actions dropdown :::info Since version you may use the customizable controls feature to show the actions outside the dropdown. ::: ## Overview You generate one running `bin/rails generate avo:action toggle_inactive`, creating an action configuration file. ```ruby class ToggleInactive < Avo::BaseAction self.name = 'Toggle inactive' field :notify_user, as: :boolean, default: true field :message, as: :text, default: 'Your account has been marked as inactive.' def handle(**args) models, fields, current_user, resource = args.values_at(:models, :fields, :current_user, :resource) models.each do |model| if model.active model.update active: false else model.update active: true end # Optionally, you may send a notification with the message to that user from inside the action UserMailer.with(user: model).toggle_inactive(fields["message"]).deliver_later end succeed 'Perfect!' end end ``` You may add fields to the action just as you do it in a resource. Adding fields is optional. You may have actions that don't have any fields attached. ```ruby field :notify_user, as: :boolean field :message, as: :textarea, default: 'Your account has been marked as inactive.' ``` :::warning Files authorization If you're using the `file` field on an action and attach it to a resource that's using the authorization feature, please ensure you have the `upload_{FIELD_ID}?` policy method returning `true`. Otherwise, the `file` input might be hidden. More about this on the authorization page. ::: !Actions The `handle` method is where the magic happens. That is where you put your action logic. In this method, you will have access to the selected `models` (if there's only one, it will be automatically wrapped in an array) and the values passed to the `fields`. ```ruby def handle(**args) models, fields = args.values_at(:models, :fields) models.each do |model| if model.active model.update active: false else model.update active: true end # Optionally, you may send a notification with the message to that user. UserMailer.with(user: model).toggle_inactive(fields["message"]).deliver_later end succeed 'Perfect!' end ``` ## Registering actions To add an action to one of your resources, you need to declare it on the resource using the `action` method. ```ruby{8} class UserResource < Avo::BaseResource self.title = :name self.search = [:id, :first_name, :last_name] field :id, as: :id # other fields action ToggleActive end ``` ## Action responses After an action runs, you may use several methods to respond to the user. For example, you may respond with just a message or with a message and an action. The default response is to reload the page and show the _Action ran successfully_ message. ### Message responses You will have four message response methods at your disposal `succeed`, `error`, `warn`, and `inform`. These will render the user green, red, orange, and blue alerts. ```ruby{4-7} def handle(**args) # Demo handle action succeed "Success response ✌️" warn "Warning response ✌️" inform "Info response ✌️" error "Error response ✌️" end ``` :::warning Since Avo 2.20 we deprecated the `fail` method in favor of `error`. ::: Avo alert responses ### Run actions silently You may want to run an action and show no notification when it's done. That is useful for redirect scenarios. You can use the `silent` response for that. ```ruby def handle(**args) # Demo handle action redirect_to "/admin/some-tool" silent end ``` ## Response types After you notify the user about what happened through a message, you may want to execute an action like `reload` (default action) or `redirect_to`. You may use message and action responses together. ```ruby{14} def handle(**args) models = args[:models] models.each do |model| if model.admin? error "Can't mark inactive! The user is an admin." else model.update active: false succeed "Done! User marked as inactive!" end end reload end ``` The available action responses are: ## Customization ```ruby{2-6} class TogglePublished < Avo::BaseAction self.name = 'Mark inactive' self.message = 'Are you sure you want to mark this user as inactive?' self.confirm_button_label = 'Mark inactive' self.cancel_button_label = 'Not yet' self.no_confirmation = true ``` ### Customize the message You may update the `self.message` class attribute to customize the message if there are no fields present. #### Callable message Since version `2.21` you can pass a block to `self.message` where you have access to a baunch of variables. ```ruby class ReleaseFish < Avo::BaseAction self.message = -> { # you have access to: # - params # - current_user # - context # - view_context # - request # - resource # - record "Are you sure you want to release the #{record.name}?" } end ``` ### Customize the buttons You may customize the labels for the action buttons using `confirm_button_label` and `cancel_button_label`. Avo button labels ### No confirmation actions You will be prompted by a confirmation modal when you run an action. If you don't want to show the confirmation modal, pass in the `self.no_confirmation = true` class attribute. That will execute the action without showing the modal at all. ## Standalone actions You may need to run actions that are not necessarily tied to a model. Standalone actions help you do just that. Add `self.standalone` to an existing action or generate a new one using the `--standalone` option (`bin/rails generate avo:action global_action --standalone`). ```ruby{3} class DummyAction < Avo::BaseAction self.name = "Dummy action" self.standalone = true def handle(**args) fields, current_user, resource = args.values_at(:fields, :current_user, :resource) # Do something here succeed 'Yup' end end ``` ## Actions visibility You may want to hide specific actions on screens, like a standalone action on the `Show` screen. You can do that using the `self.visible` attribute. ```ruby{4} class DummyAction < Avo::BaseAction self.name = "Dummy action" self.standalone = true self.visible = -> { view == :index } def handle(**args) fields, current_user, resource = args.values_at(:fields, :current_user, :resource) # Do something here succeed 'Yup' end end ``` By default, actions are visible on the `Index`, `Show`, and `Edit` views, but you can enable them on the `New` screen, too (from version 2.9.0). ```ruby self.visible = -> { view == :new } # Or use this if you want them to be visible on any view self.visible = -> { true } ``` Inside the visible block you can access the following variables: ```ruby self.visible = -> do # You have access to: # block # context # current_user # params # parent_resource (can access the parent_model by parent_resource.model) # resource (can access the model by resource.model) # view # view_context end ``` ## Actions authorization :::warning Using the Pundit policies, you can restrict access to actions using the `act_on?` method. If you think you should see an action on a resource and you don't, please check the policy method. More info here ::: ## Actions arguments Actions can have different behaviors according to their host resource. In order to achieve that, arguments must be passed like on the example below: ```ruby{9-11} class FishResource < Avo::BaseResource self.title = :name field :id, as: :id field :name, as: :text field :user, as: :belongs_to field :type, as: :text, hide_on: :forms action DummyAction, arguments: { special_message: true } end ``` Now, the arguments can be accessed inside `DummyAction` ***`handle` method*** and on the ***`visible` block***! ```ruby{4-6,8-14} class DummyAction < Avo::BaseAction self.name = "Dummy action" self.standalone = true self.visible = -> do arguments[:special_message] end def handle(**args) if arguments[:special_message] succeed "I love 🥑" else succeed "Success response ✌️" end end end ``` --- # Localization (i18n) Avo leverages Rails' powerful I18n translations module. When you run `bin/rails avo:install`, Rails will generate for you the `avo.en.yml` translation file. This file will automatically be injected into the I18n translations module. ## Localizing resources Let's say you want to localize a resource. All you need to do is add a `self.translation_key` class attribute in the `Resource` file. That will tell Avo to use that translation key to localize this resource. That will change the labels of that resource everywhere in Avo. ```ruby{4} # app/avo/resources/user_resource.rb class UserResource < Avo::BaseResource self.title = :name self.translation_key = 'avo.resource_translations.user' end ``` ```yaml{6-10} # avo.es.yml es: avo: dashboard: 'Dashboard' # ... other translation keys resource_translations: user: zero: 'usuarios' one: 'usuario' other: 'usuarios' ``` ## Localizing fields Similarly, you can even localize fields. All you need to do is add a `translation_key:` option on the field declaration. ```ruby{7} # app/avo/resources/project_resource.rb class ProjectResource < Avo::BaseResource self.title = :name field :id, as: :id # ... other fields field :files, as: :files, translation_key: 'avo.field_translations.file' end ``` ```yaml{6-10} # avo.es.yml es: avo: dashboard: 'Dashboard' # ... other translation keys field_translations: file: zero: 'archivos' one: 'archivo' other: 'archivos' ``` ## Setting the locale Setting the locale for Avo is simple. Just use the `config.locale = :en` config attribute. Default is `nil` and will fall back to whatever you have configured in `application.rb`. ```ruby{2} Avo.configure do |config| config.locale = :en # default is nil end ``` That will change the locale only for Avo requests. The rest of your app will still use your locale set in `application.rb`. If you wish to change the locale for the whole app, you can use the `set_locale=pt-BR` param. That will set the default locale until you restart your server. Suppose you wish to change the locale only for one request using the `force_locale=pt-BR` param. That will set the locale for that request and keep the `force_locale` param while you navigate Avo. Remove that param when you want to go back to your configured `default_locale`. Check out our guide for multilingual records. ## Re-generate the locale When updating Avo, please run `bin/rails generate avo:locales` to re-generate the locales file. ## FAQ If you try to localize your resources and fields and it doesn't seem to work, please be aware of the following. ### Advanced localization is a Pro feature Localizing strings in Avo will still work using Rails' `I18n` mechanism, but localizing files and resources require a `Pro` or above license. The reasoning is that deep localization is a more advanced feature that usually falls in the commercial realm. So if you create commercial products or apps for clients and make revenue using Avo, we'd love to get your support to maintain it and ship new features going forward. ### The I18n.t method defaults to the name of that field/resource Internally the localization works like so `I18n.t(translation_key, count: 1, default: default)` where the `default` is the computed field/resource name. So check the structure of your translation keys. ```yaml # config/locales/avo.pt-BR.yml pt-BR: avo: field_translations: file: zero: 'arquivos' one: 'arquivo' other: 'arquivos' resource_translations: user: zero: 'usuários' one: 'usuário' other: 'usuários' ``` --- # Branding ```ruby Avo.configure do |config| config.branding = { colors: { background: "248 246 242", 100 => "#C5F1D4", 400 => "#3CD070", 500 => "#30A65A", 600 => "#247D43", }, chart_colors: ['#FFB435', "#FFA102", "#CC8102", '#FFB435', "#FFA102", "#CC8102"], logo: "/avo-assets/logo.png", logomark: "/avo-assets/logomark.png", placeholder: "/avo-assets/placeholder.svg", favicon: "/avo-assets/favicon.ico" } end ``` Using the branding feature, you can easily change the look of your app. You tweak it inside your `avo.rb` initializer in the `branding` attribute. It takes a hash with a few properties. ## Configure brand color To customize the primary color of Avo, you must configure the `colors` key with four color variants. `100` for color hints, `500` for the base primary color, and `400` and `600` values for highlights. ```ruby{4-8} Avo.configure do |config| config.branding = { colors: { background: "248 246 242", 100 => "#C5F1D4", 400 => "#3CD070", 500 => "#30A65A", 600 => "#247D43", } } end ``` You may also customize the color of Avo's background using the `background` key. ![](/assets/img/branding/green.jpg) ![](/assets/img/branding/red.jpg) ![](/assets/img/branding/orange.jpg) :::info The color format can be hex (starting with `#`) or rgb (three groups split by a space, not a comma). ::: Avo uses [Tailwinds color system](https://tailwindcss.com/docs/customizing-colors). You can generate your own using the tools below. - [Palettte](https://palettte.app/) - [ColorBox](https://colorbox.io/) - [TailwindInk](https://tailwind.ink/) Here are a few for you to choose from. ```ruby config.branding = { colors: { # BLUE 100 => "#CEE7F8", 400 => "#399EE5", 500 => "#0886DE", 600 => "#066BB2", # RED 100 => "#FACDD4", 400 => "#F06A7D", 500 => "#EB3851", 600 => "#E60626", # GREEN 100 => "#C5F1D4", 400 => "#3CD070", 500 => "#30A65A", 600 => "#247D43", # ORANGE 100 => "#FFECCC", 400 => "#FFB435", 500 => "#FFA102", 600 => "#CC8102", } } ``` ## Customize the chart colors For your dashboard, you can further customize the colors of the charts. You can do that using the `chart_colors` option. Pass in an array of colors, and Avo will do the rest. ```ruby Avo.configure do |config| config.branding = { chart_colors: ['#FFB435', "#FFA102", "#CC8102", '#FFB435', "#FFA102", "#CC8102"], } end ``` ![](/assets/img/branding/chart-colors.jpg) :::warning The chart colors should be hex colors. They are forwarded to chart.js ::: ## Customize the logo We want to make it easy to change the logo for your app, so we added the `logo` and `logomark` options to the branding feature. The `logo` should be the "big" logo you want to display on the desktop version of your app, and `logomark` should be a squared-aspect image that Avo displays on the mobile version. ![](/assets/img/branding/logomark.gif) ## Customize the missing image placeholder When you view the data in the view in a grid, when the `cover` field does not have an image, an avocado is going to be displayed instead as a placeholder. You might want to change that to something else using the `placeholder` option. ```ruby Avo.configure do |config| config.branding = { placeholder: "/YOUR_PLACEHOLDER_IMAGE.jpg", } end ``` ## Customize the favicon We want to make it easy to change the logo for your app, so we added the `favicon` option to the branding feature. Overwrite it using an `.ico` file. --- # Custom pages (custom tools) You may use custom tools to create custom sections or views to add to your app. ## Generate tools `bin/rails generate avo:tool dashboard` will generate the necessary files to show the new custom tool. ```bash{2-6} ▶ bin/rails generate avo:tool dashboard create app/views/avo/sidebar/items/_dashboard.html.erb insert app/controllers/avo/tools_controller.rb create app/views/avo/tools/dashboard.html.erb route namespace :avo do get "dashboard", to: "tools#dashboard" end ``` ### Controller If this is your first custom tool, a new `ToolsController` will be generated for you. Within this controller, Avo created a new method. ```ruby class Avo::ToolsController < Avo::ApplicationController def dashboard end end ``` You can keep this action in this controller or move it to another controller and organize it differently. ### Route ```ruby{2-4} Rails.application.routes.draw do namespace :avo do get "dashboard", to: "tools#dashboard" end authenticate :user, ->(user) { user.admin? } do mount Avo::Engine => Avo.configuration.root_path end end ``` The route generated is wrapped inside a namespace with the `Avo.configuration.root_path` name. Therefore, you may move it inside your authentication block next to the Avo mounting call. ### Sidebar item The `_dashboard.html.erb` partial will be added to the `app/views/avo/sidebar/items` directory. All the files in this directory will be loaded by Avo and displayed in the sidebar. They are displayed alphabetically, so you may change their names to reorder the items. ### Customize the sidebar If you want to customize the sidebar partial further, you can eject and update it to your liking. We're planning on creating a better sidebar customization experience later this year. ## Add assets You might want to import assets (javascript and stylesheets files) when creating custom tools or fields. You can do that so easily from v1.3. Please follow this guide to bring your assets with your asset pipeline. ## Using helpers from your app You'll probably want to use some of your helpers in your custom tools. To have them available inside your custom controllers inherited from Avo's `ApplicationController`, you need to include them using the `helper` method. ```ruby{3-5,10} # app/helpers/home_helper.rb module HomeHelper def custom_helper 'hey from custom helper' end end # app/controllers/avo/tools_controller.rb class Avo::ToolsController < Avo::ApplicationController helper HomeHelper def dashboard @page_title = "Dashboard" end end ``` ```erb{13} # app/views/avo/tools/dashboard.html.erb
<%= render Avo::PanelComponent.new title: 'Dashboard', display_breadcrumbs: true do |c| %> <% c.with_tools do %>
This is the panels tools section.
<% end %> <% c.with_body do %>

What a nice new tool 👋

<%= custom_helper %>
<% end %> <% end %>
``` ### Using path helpers Because you're in a Rails engine, you will have to prepend the engine object to the path. #### For Avo paths Instead of writing `resources_posts_path(1)` you have to write `avo.resources_posts_path(1)`. #### For the main app paths When you want to reference paths from your main app, instead of writing `posts_path(1)`, you have to write `main_app.posts_path`. --- # Custom fields Avo ships with 20+ well polished and ready to be used, fields out of the box. When you need a field that is not provided by default, Avo makes it easy to add it. ## Generate a new field Every new field comes with three [view components](https://viewcomponent.org/), `Edit` (which is also used in the `New` view), and `Show` and `Index`. There's also a `Field` configuration file. `bin/rails generate avo:field progress_bar` generates the files for you. :::info Please restart your rails server after adding a new custom field. ::: ```bash{2-9} ▶ bin/rails generate avo:field progress_bar create app/components/avo/fields/progress_bar_field create app/components/avo/fields/progress_bar_field/edit_component.html.erb create app/components/avo/fields/progress_bar_field/edit_component.rb create app/components/avo/fields/progress_bar_field/index_component.html.erb create app/components/avo/fields/progress_bar_field/index_component.rb create app/components/avo/fields/progress_bar_field/show_component.html.erb create app/components/avo/fields/progress_bar_field/show_component.rb create app/avo/fields/progress_bar_field.rb ``` The `ProgressBarField` file is what registers the field in your admin. ```ruby class ProgressBarField < Avo::Fields::BaseField def initialize(name, **args, &block) super(name, **args, &block) end end ``` Now you can use your field like so: ```ruby{6} # app/avo/resources/progress_bar_field.rb class ProjectResource < Avo::BaseResource self.title = :name field :id, as: :id, link_to_resource: true field :progress, as: :progress_bar end ``` Progress custom field The generated view components are basic text fields for now. ```erb{1,9,14} # app/components/avo/fields/progress_bar_field/edit_component.html.erb <%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal do %> <%= @form.text_field @field.id, class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)), placeholder: @field.placeholder, disabled: @field.readonly %> <% end %> # app/components/avo/fields/progress_bar_field/index_component.html.erb <%= index_field_wrapper field: @field do %> <%= @field.value %> <% end %> # app/components/avo/fields/progress_bar_field/show_component.html.erb <%= show_field_wrapper field: @field, index: @index do %> <%= @field.value %> <% end %> ``` You can customize them and add as much or as little content as needed. More on customization [below](#customize-the-views). ## Field options This file is where you may add field-specific options. ```ruby{3-6,11-14} # app/avo/fields/progress_bar_field.rb class ProgressBarField < Avo::Fields::BaseField attr_reader :max attr_reader :step attr_reader :display_value attr_reader :value_suffix def initialize(name, **args, &block) super(name, **args, &block) @max = 100 @step = 1 @display_value = false @value_suffix = nil end end ``` The field-specific options can come from the field declaration as well. ```ruby{11-14,23} # app/avo/fields/progress_bar_field.rb class ProgressBarField < Avo::Fields::BaseField attr_reader :max attr_reader :step attr_reader :display_value attr_reader :value_suffix def initialize(name, **args, &block) super(name, **args, &block) @max = args[:max] || 100 @step = args[:step] || 1 @display_value = args[:display_value] || false @value_suffix = args[:value_suffix] || nil end end # app/avo/resources/progress_bar_field.rb class ProjectResource < Avo::BaseResource self.title = :name field :id, as: :id, link_to_resource: true field :progress, as: :progress_bar, step: 10, display_value: true, value_suffix: "%" end ``` ## Field Visibility If you need to hide the field in some view, you can use the visibility helpers. ```ruby{16} # app/avo/fields/progress_bar_field.rb class ProgressBarField < Avo::Fields::BaseField attr_reader :max attr_reader :step attr_reader :display_value attr_reader :value_suffix def initialize(name, **args, &block) super(name, **args, &block) @max = args[:max] || 100 @step = args[:step] || 1 @display_value = args[:display_value] || false @value_suffix = args[:value_suffix] || nil hide_on :forms end end ``` ## Customize the views No let's do something about those views. Let's add a progress bar to the `Index` and `Show` views. ```erb{1,15} # app/components/avo/fields/progress_bar_field/show_component.html.erb <%= show_field_wrapper field: @field, index: @index do %> <% if @field.display_value %>
<%= @field.value %><%= @field.value_suffix if @field.value_suffix.present? %>
<% end %> <% end %> # app/components/avo/fields/progress_bar_field/index_component.html.erb <%= index_field_wrapper field: @field do %> <% if @field.display_value %>
<%= @field.value %><%= @field.value_suffix if @field.value_suffix.present? %>
<% end %> <% end %> ``` Progress bar custom field on index For the `Edit` view, we're going to do something different. We'll implement a `range` input. ```erb{1} # app/components/avo/fields/progress_bar_field/edit_component.html.erb <%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal do %> <% if @field.display_value %>
<%= @field.value %><%= @field.value_suffix if @field.value_suffix.present? %>
<% end %> <%= @form.range_field @field.id, class: 'w-full', placeholder: @field.placeholder, disabled: @field.readonly, min: 0, # add the field-specific options max: @field.max, step: @field.step, %> <% end %> ``` Progress bar custom field edit ## Field assets Because there isn't just one standardized way of handling assets in Rails, we decided we won't provide **asset loading** support for custom fields for now. That doesn't mean that you can't use custom assets (javascript or CSS files), but you will have to load them in your own pipeline in dedicated Avo files. In the example above, we added javascript on the page just to demonstrate the functionality. In reality, you might add that to a stimulus controller inside your own Avo dedicated pipeline (webpacker or sprockets). Some styles were added in the asset pipeline directly. ```css progress { @apply h-2 bg-white border border-gray-400 rounded shadow-inner; } progress[value]::-webkit-progress-bar { @apply bg-white border border-gray-500 rounded shadow-inner; } progress[value]::-webkit-progress-value{ @apply bg-green-600 rounded; } progress[value]::-moz-progress-bar { @apply bg-green-600 rounded appearance-none; } ``` ## Use pre-built Stimulus controllers Avo ships with a few Stimulus controllers that help you build more dynamic fields. ### Hidden input controller This controller allows you to hide your content and add a trigger to show it. You'll find it in the Trix field. Hidden input controller You should add the `:always_show` `attr_reader` and `@always_show` instance variables to your field. ```ruby{3,8} # app/avo/fields/color_picker_field.rb class ColorPickerField < Avo::Fields::BaseField attr_reader :always_show def initialize(id, **args, &block) super(id, **args, &block) @always_show = args[:always_show] || false @allow_non_colors = args[:allow_non_colors] end end ``` Next, in your fields `Show` component, you need to do a few things. 1. Wrap the field inside a controller tag 1. Add the trigger that will show the content. 1. Wrap the value in a div with the `hidden` class applied if the condition `@field.always_show` is `false`. 1. Add the `content` target (`data-hidden-input-target="content"`) to that div. ```erb{4-7,8} # app/components/avo/fields/color_picker_field/show_component.html.erb <%= show_field_wrapper field: @field, index: @index do %>
<% unless @field.always_show %> <%= link_to t('avo.show_content'), 'javascript:void(0);', class: 'font-bold inline-block', data: { action: 'click->hidden-input#showContent' } %> <% end %>
class="hidden" <% end %> data-hidden-input-target="content">
<%= @field.value %>
<% end %> ``` Hidden input controller --- # Resource tools Similar to adding custom fields to a resource, you can add custom tools. A custom tool is a partial added to your resource's `Show` and `Edit` views. ## Generate a resource tool Run `bin/rails generate avo:resource_tool post_info`. That will create two files. The configuration file `app/avo/resource_tools/post_info.rb` and the partial file `app/views/avo/resource_tools/_post_info.html.erb`. The configuration file holds the tool's name and the partial path if you want to override it. ```ruby class PostInfo < Avo::BaseResourceTool self.name = "Post info" # self.partial = "avo/resource_tools/post_info" end ``` The partial is ready for you to customize further. ```erb
<%= render Avo::PanelComponent.new title: "Post info" do |c| %> <% c.with_tools do %> <%= a_link('/avo', icon: 'heroicons/solid/academic-cap', style: :primary) do %> Dummy link <% end %> <% end %> <% c.with_body do %>

🪧 This partial is waiting to be updated

You can edit this file here app/views/avo/resource_tools/post_info.html.erb.

The resource tool configuration file should be here app/avo/resource_tools/post_info.rb.

<% # In this partial, you have access to the following variables: # tool # @resource # @resource.model # form (on create & edit pages. please check for presence first) # params # Avo::App.context # current_user %>
<% end %> <% end %>
``` Avo resource tool partial ## Partial context You might need access to a few things in the partial. You have access to the `tool`, which is an instance of your tool `PostInfo`, and the `@resource`, which holds all the information about that particular resource (`view`, `model`, `params`, and others), the `params` of the request, the `Avo::App.context` and the `current_user`. That should give you all the necessary data to scope out the partial content. ## Tool visibility The resource tool is default visible on the `Show` view of a resource. You can change that using the visibility options (`show_on`, `only_on`). ```ruby # app/avo/resources/post_resource.rb class PostResource < Avo::BaseResource tool PostInfo, show_on: :edit end ``` ### Using path helpers Because you're in a Rails engine, you will have to prepend the engine object to the path. #### For Avo paths Instead of writing `resources_posts_path(1)` you have to write `avo.resources_posts_path(1)`. #### For the main app paths When you want to reference paths from your main app, instead of writing `posts_path(1)`, you have to write `main_app.posts_path`. ## Add custom fields on forms **From Avo 2.12** You might want to add a few more fields or pieces of functionality besides the CRUD-generated fields on your forms. Of course, you can already create new custom fields to do it in a more structured way, but you can also use a resource tool to achieve more custom behavior. You have access to the `form` object that is available on the new/edit pages on which you can attach inputs of your choosing. You can even achieve nested form functionality. You have to follow three steps to enable this functionality: 1. Add the inputs in a resource tool and enable the tool on the form pages 2. Tell Avo which `params` it should permit to write to the model 3. Make sure the model is equipped to receive the params In the example below, we'll use the `FishResource`, add a few input fields (they will be a bit unstyled because this is not the scope of the exercise), and do some actions with some of them. We first need to generate the tool with `bin/rails g avo:resource_tool fish_information` and add the tool to the resource file. ```ruby{2} class FishResource < Avo::BaseResource tool FishInformation, show_on: :forms end ``` In the `_fish_information.html.erb` partial, we'll add a few input fields. Some are directly on the `form`, and some are nested with `form.fields_for`. The fields are: - `fish_type` as a text input - `properties` as a multiple text input which will produce an array in the back-end - `information` as nested inputs which will produce a `Hash` in the back-end ```erb{13-36}
<%= render Avo::PanelComponent.new(title: @resource.model.name) do |c| %> <% c.with_tools do %> <%= a_link('/admin', icon: 'heroicons/solid/academic-cap', style: :primary) do %> Primary <% end %> <% end %> <% c.with_body do %>
<% if form.present? %> <%= form.label :fish_type %> <%= form.text_field :fish_type, value: 'default type of fish', class: input_classes %>
<%= form.label :properties %> <%= form.text_field :properties, multiple: true, value: 'property 1', class: input_classes %> <%= form.text_field :properties, multiple: true, value: 'property 2', class: input_classes %>
<% form.fields_for :information do |information_form| %> <%= form.label :information_name %> <%= information_form.text_field :name, value: 'information name', class: input_classes %>
This is going to be passed to the model

<%= form.label :information_history %> <%= information_form.text_field :history, value: 'information history', class: input_classes %>
This is going to be passed to the model

<%= form.label :information_age %> <%= information_form.text_field :age, value: 'information age', class: input_classes %>
This is NOT going to be passed to the model
<% end %> <% end %>
<% end %> <% end %>
``` Next, we need to tell Avo and Rails which params are welcomed in the `create`/`update` request. We do that using the `extra_params` option on the `FishResource`. Avo's internal implementation is to assign the attributes you specify here to the underlying model (`model.assign_attributes params.permit(extra_params)`). ```ruby{2} class FishResource < Avo::BaseResource self.extra_params = [:fish_type, :something_else, properties: [], information: [:name, :history]] tool FishInformation, show_on: :forms end ``` The third step is optional. You must ensure your model responds to the params you're sending. Our example should have the `fish_type`, `properties`, and `information` attributes or setter methods on the model class. We chose to add setters to demonstrate the params are called to the model. ```ruby class Fish < ApplicationRecord self.inheritance_column = nil # required in order to use the type DB attribute def fish_type=(value) self.type = value end def properties=(value) # properties should be an array puts ["properties in the Fish model->", value].inspect end def information=(value) # properties should be a hash puts ["information in the Fish model->", value].inspect end end ``` If you run this code, you'll notice that the `information.information_age` param will not reach the `information=` method because we haven't allowed it in the `extra_params` option. --- # Stimulus JS & HTML attributes :::warning This feature is in the **beta** phase. The API might change while seeing how the community uses it to build their apps. This is not the **dependable fields** feature but a placeholder so we can observe and see what we need to ship to make it helpful to you. ::: _What we'll be able to do at the end of reading these docs_ Debug on input stimulus method :::info **Please note** that in order to have the JS code from your controllers loaded in Avo you'll need to add your asset pipeline using these instructions. It's really easier than it sounds. It's like you'd add a new JS file to your regular Rails app. :::
One of the most requested features is the ability to make the forms more dynamic. We want to bring the first iteration of this feature through Stimulus JS integration. This light layer will allow you to hook into the views and inject your functionality with Stimulus JS. You'll be able to add your Stimulus controllers to the resource views (`Index`, `Show`, `Edit`, and `New`), attach `classes`, `style`, and `data` attributes to the fields and inputs in different views. ## Assign Stimulus controllers to resource views To enable a stimulus controller to resource view, you can use the `stimulus_controllers` option on the resource file. ```ruby class CourseResource < Avo::BaseResource self.stimulus_controllers = "course-resource" end ``` You can add more and separate them by a space character. ```ruby class CourseResource < Avo::BaseResource self.stimulus_controllers = "course-resource select-field association-fields" end ``` Avo will add a `resource-[VIEW]` (`resource-edit`, `resource-show`, or `resource-index`) controller for each view. ### Field wrappers as targets By default, Avo will add stimulus target data attributes to all field wrappers. The notation scheme uses the name and field type `[FIELD_NAME][FIELD_TYPE]WrapperTarget`. ```ruby # Wrappers get the `data-[CONTROLLER]-target="nameTextWrapper"` attribute and can be targeted using nameTextWrapperTarget field :name, as: :text # Wrappers get the `data-[CONTROLLER]-target="createdAtDateTimeWrapper"` attribute and can be targeted using createdAtDateTimeWrapperTarget field :created_at, as: :date_time # Wrappers get the `data-[CONTROLLER]-target="hasSkillsTagsWrapper"` attribute and can be targeted using hasSkillsTagsWrapperTarget field :has_skills, as: :tags ``` For example for the following stimulus controllers `self.stimulus_controllers = "course-resource select-field association-fields"` Avo will generate the following markup for the `has_skills` field above on the `edit` view. ```html{4-7}
``` You can add those targets to your controllers and use them in your JS code. ### Field inputs as targets Similar to the wrapper element, inputs in the `Edit` and `New` views get the `[FIELD_NAME][FIELD_TYPE]InputTarget`. On more complex fields like the searchable, polymorphic `belongs_to` field, where there is more than one input, the target attributes are attached to all `input`, `select`, and `button` elements. ```ruby # Inputs get the `data-[CONTROLLER]-target="nameTextInput"` attribute and can be targeted using nameTextInputTarget field :name, as: :text # Inputs get the `data-[CONTROLLER]-target="createdAtDateTimeInput"` attribute and can be targeted using createdAtDateTimeInputTarget field :created_at, as: :date_time # Inputs get the `data-[CONTROLLER]-target="hasSkillsTagsInput"` attribute and can be targeted using hasSkillsTagsInputTarget field :has_skills, as: :tags ``` ### All controllers receive the `view` value All stimulus controllers receive the `view` attribute in the DOM. ```html{4-5}
``` Now you can use that inside your Stimulus JS controller like so: ```js{5,9} import { Controller } from '@hotwired/stimulus' export default class extends Controller { static values = { view: String, } async connect() { console.log('view ->', this.viewValue) } } ``` The possible values are `index`, `show`, `edit`, or `new` ## Assign Stimulus controllers to actions Similarly as to resource, you can assign stimulus controller to an action. To do that you can use the `stimulus_controllers` option on the action file. ```ruby class ShowCurrentTime < Avo::BaseAction self.stimulus_controllers = "city-in-country" end ``` You can add more and separate them by a space character. ```ruby class ShowCurrentTime < Avo::BaseAction self.stimulus_controllers = "course-resource select-field association-fields" end ``` The same way as for the resources, Avo will add stimulus target data attributes to [all field wrappers](#field-wrappers-as-targets) and [all input fields](#field-inputs-as-targets). Unlike with the resource, Avo will not add a specific default controller for each type of the view (`index`, `show`, `edit`). Same way, the controllers will not receive the `view` attribute in the DOM, [as in case of resources](#all-controllers-receive-the-view-value). ## Attach HTML attributes Using the `html` option you can attach `style`, `classes`, and `data` attributes. The `style` attribute adds the `style` tag to your element, `classes` adds the `class` tag, and the `data` attribute the `data` tag to the element you choose. Pass the `style` and `classes` attributes as strings, and the `data` attribute a Hash. ```ruby{4-11} field :name, as: :text, html: { edit: { wrapper: { style: "background: red; text: white;" # string classes: "absolute h-[41px] w-full" # string data: { action: "input->resource-edit#toggle", resource_edit_toggle_target_param: "skills_tags_wrapper", } # Hash } } } ``` ### Declare the fields from the outside in When you add these attributes, you need to think from the outside in. So first the `view` (`index`, `show`, or `edit`), next the element to which you add the attribute (`wrapper`, `label`, `content` or `input`), and then the attribute `style`, `classes`, or `data`. **The `edit` value will be used for both the `Edit` and `New` views.** There are two notations through which you can attach the attributes; `object` or `block` notation. ## The `object` notation This is the simplest way of attaching the attribute. You usually use this when you want to add _static_ content and params. ```ruby{3-9} field :has_skills, as: :boolean, html: { edit: { wrapper: { classes: "hidden" } } } ``` In this example, we're adding the `hidden` class to the field wrapper on the `Edit` and `New` views. ## The `block` notation You can use the' block' notation if you need to do a more complex transformation to add your attributes. You'll have access to the `params`, `current_user`, `record`, and `resource` variables. It's handy in multi-tenancy scenarios and when you need to scope out the information across accounts. ```ruby{3-18} field :has_skills, as: :boolean, html: -> do edit do wrapper do classes do "hidden" end data do if current_user.admin? { action: "click->admin#do_something_admin" } else { record: record, resource: resource, } end end end end end ``` For the `data`, `style`, and `classes` options, you may use the `method` notation alongside the block notation for simplicity. ```ruby{6,7} field :has_skills, as: :boolean, html: -> do edit do wrapper do classes("hidden") data({action: "click->admin#do_something_admin"}) end end end ``` ## Where are the attributes added? You can add attributes to the wrapper element for the `index`, `show`, or `edit` blocks. ## Index field wrapper ```ruby field :name, as: :text, html: { index: { wrapper: {} } } ``` Index field wrapper ## Show field wrapper ```ruby field :name, as: :text, html: { show: { wrapper: {} } } ``` Show field wrapper ## Show label target ```ruby field :name, as: :text, html: { show: { label: {} } } ``` Show label target ## Show content target ```ruby field :name, as: :text, html: { show: { content: {} } } ``` Show content target ## Edit field wrapper ```ruby field :name, as: :text, html: { edit: { wrapper: {} } } ``` Edit field wrapper ## Edit label target ```ruby field :name, as: :text, html: { edit: { label: {} } } ``` Edit label target ## Edit content target ```ruby field :name, as: :text, html: { edit: { content: {} } } ``` Edit content target ## Edit input target ```ruby field :name, as: :text, html: { edit: { input: {} } } ``` Index field wrapper ## Composing the attributes together You can use the attributes together to make your fields more dynamic. ```ruby{3-9} field :has_skills, as: :boolean, html: { edit: { input: { data: { # On click run the toggleSkills method on the toggle-fields controller action: "input->toggle-fields#toggleSkills", } } } } field :skills, as: :tags, html: { edit: { wrapper: { # hide this field by default classes: "hidden" } } } ``` ```js // toggle_fields_controller.js import { Controller } from "@hotwired/stimulus"; export default class extends Controller { static targets = ["skillsTagsWrapper"]; // use the target Avo prepared for you toggleSkills() { this.skillsTagsWrapperTarget.classList.toggle("hidden"); } } ``` ## Pre-made stimulus methods Avo ships with a few JS methods you may use on your resources. ### `resource-edit#toggle` On your `Edit` views, you can use the `resource-edit#toggle` method to toggle the field visibility from another field. ```ruby{5-7} field :has_country, as: :boolean, html: { edit: { input: { data: { action: "input->resource-edit#toggle", # use the pre-made stimulus method on input resource_edit_toggle_target_param: "countrySelectWrapper", # target to be toggled # resource_edit_toggle_targets_param: ["countrySelectWrapper"] # add more than one target } } } } field :country, as: :select, options: Course.countries.map { |country| [country, country] }.to_h ``` Toggle method ### `resource-edit#disable` Disable works similarly to toggle, with the difference that it disables the field instead of hiding it. ```ruby{5-7,16} field :has_skills, as: :boolean, html: { edit: { input: { data: { action: "input->resource-edit#disable", # use the pre-made stimulus method on input resource_edit_disable_target_param: "countrySelectInput", # target to be disabled # resource_edit_disable_targets_param: ["countrySelectWrapper"] # add more than one target to disable } } } } field :country, as: :select, options: Course.countries.map { |country| [country, country] }.to_h ``` Disable method You may also target the `wrapper` element for that field if the target field has more than one input like the searchable polymorphic `belongs_to` field. ```ruby{6} field :has_skills, as: :boolean, html: { edit: { input: { data: { action: "input->resource-edit#disable", # use the pre-made stimulus method on input resource_edit_disable_target_param: "countrySelectWrapper", # target the wrapper so all inputs are disabled # resource_edit_disable_targets_param: ["countrySelectWrapper"] # add more than one target to disable } } } } field :country, as: :select, options: Course.countries.map { |country| [country, country] }.to_h ``` ### `resource-edit#debugOnInput` For debugging purposes only, the `resource_edit` Stimulus JS controller provides the `debugOnInput` method that outputs the event and value for an action to the console. Use this just to make sure you targeted your fields correctly. It doesn't have any real use. Debug on input stimulus method ## Custom Stimulus controllers The bigger purpose of this feature is to create your own Stimulus JS controllers to bring the functionality you need to the CRUD interface. Below is an example of how you could implement a city & country select feature where the city select will have its options changed when the user selects a country: 1. Add an action to the country select to trigger a change. 1. The stimulus method `onCountryChange` will be triggered when the user changes the country. 1. That will trigger a fetch from the server where Rails will return an array of cities for the provided country. 1. The city field will have a `loading` state while we fetch the results. 1. The cities will be added to the `city` select field 1. If the initial value is present in the returned results, it will be selected. 1. All of this will happen only on the `New` and `Edit` views because of the condition we added to the `connect` method. ::: code-group ```ruby [app/avo/resources/course_resource.rb] # app/avo/resources/course_resource.rb class CourseResource < Avo::BaseResource self.stimulus_controllers = "course-resource" field :id, as: :id field :name, as: :text field :country, as: :select, options: Course.countries.map { |country| [country, country] }.to_h, html: { edit: { input: { data: { course_resource_target: "countryFieldInput", # Make the input a target action: "input->course-resource#onCountryChange" # Add an action on change } } } } field :city, as: :select, options: Course.cities.values.flatten.map { |city| [city, city] }.to_h, html: { edit: { input: { data: { course_resource_target: "cityFieldInput" # Make the input a target } } } } end ``` ```ruby{4-6} [config/routes.rb] Rails.application.routes.draw do if defined? ::Avo Avo::Engine.routes.draw do scope :resources do get "courses/cities", to: "courses#cities" end end end end ``` ```ruby{3} [app/controllers/avo/courses_controller.rb] class Avo::CoursesController < Avo::ResourcesController def cities render json: get_cities(params[:country]) # return an array of cities based on the country we received end private def get_cities(country) return [] unless Course.countries.include?(country) Course.cities[country.to_sym] end end ``` ```ruby [app/models/course.rb] class Course < ApplicationRecord def self.countries ["USA", "Japan", "Spain", "Thailand"] end def self.cities { USA: ["New York", "Los Angeles", "San Francisco", "Boston", "Philadelphia"], Japan: ["Tokyo", "Osaka", "Kyoto", "Hiroshima", "Yokohama", "Nagoya", "Kobe"], Spain: ["Madrid", "Valencia", "Barcelona"], Thailand: ["Chiang Mai", "Bangkok", "Phuket"] } end end ``` ```js [course_resource_controller.js] import { Controller } from "@hotwired/stimulus"; const LOADER_CLASSES = "absolute bg-gray-100 opacity-10 w-full h-full"; export default class extends Controller { static targets = ["countryFieldInput", "cityFieldInput", "citySelectWrapper"]; static values = { view: String, }; // Te fields initial value static initialValue; get placeholder() { return this.cityFieldInputTarget.ariaPlaceholder; } set loading(isLoading) { if (isLoading) { // create a loader overlay const loadingDiv = document.createElement("div"); loadingDiv.className = LOADER_CLASSES; loadingDiv.dataset.target = "city-loader"; // add the loader overlay this.citySelectWrapperTarget.prepend(loadingDiv); this.citySelectWrapperTarget.classList.add("opacity-50"); } else { // remove the loader overlay this.citySelectWrapperTarget .querySelector('[data-target="city-loader"]') .remove(); this.citySelectWrapperTarget.classList.remove("opacity-50"); } } async connect() { // Add the controller functionality only on forms if (["edit", "new"].includes(this.viewValue)) { this.captureTheInitialValue(); // Trigger the change on load await this.onCountryChange(); } } // Read the country select. // If there's any value selected show the cities and prefill them. async onCountryChange() { if (this.hasCountryFieldInputTarget && this.countryFieldInputTarget) { // Get the country const country = this.countryFieldInputTarget.value; // Dynamically fetch the cities for this country const cities = await this.fetchCitiesForCountry(country); // Clear the select of options Object.keys(this.cityFieldInputTarget.options).forEach(() => { this.cityFieldInputTarget.options.remove(0); }); // Add blank option this.cityFieldInputTarget.add(new Option(this.placeholder)); // Add the new cities cities.forEach((city) => { this.cityFieldInputTarget.add(new Option(city, city)); }); // Check if the initial value is present in the cities array and select it. // If not, select the first item const currentOptions = Array.from(this.cityFieldInputTarget.options).map( (item) => item.value ); if (currentOptions.includes(this.initialValue)) { this.cityFieldInputTarget.value = this.initialValue; } else { // Select the first item this.cityFieldInputTarget.value = this.cityFieldInputTarget.options[0].value; } } } // Private captureTheInitialValue() { this.initialValue = this.cityFieldInputTarget.value; } async fetchCitiesForCountry(country) { if (!country) { return []; } this.loading = true; const response = await fetch( `${window.Avo.configuration.root_path}/resources/courses/cities?country=${country}` ); const data = await response.json(); this.loading = false; return data; } } ``` ::: This is how the fields behave with this Stimulus JS controller. Debug on input stimulus method ## Use Stimulus JS in a tool There are a few steps you need to take in order to register the Stimulus JS controller in the current app context. First, you need to have a JS entrypoint (ex: `avo.custom.js`) and have that loaded in the `_head` partial. For instructions on that please follow these steps to add it to your app (`importmaps` or `esbuild`). ### Set up a controller ```js // app/javascript/controllers/sample_controller.js import { Controller } from "@hotwired/stimulus"; export default class extends Controller { connect() { console.log("Hey from sample controller 👋"); } } ``` ### Register that controller with the current Stimulus app ```js // app/javascript/avo.custom.js import SampleController from "controllers/sample_controller"; // Hook into the stimulus instance provided by Avo const application = window.Stimulus; application.register("course-resource", SampleController); // eslint-disable-next-line no-console console.log("Hi from Avo custom JS 👋"); ``` ### Use the controller in the Avo tool ```erb
``` Done 🙌 Now you have a controller connecting to a custom Resource tool or Avo tool (or Avo views). --- # Custom asset pipeline Avo plays well with most Rails asset pipelines. | Asset pipeline | Avo compatibility | |---------------|------------| | [importmap](https://github.com/rails/importmap-rails) | ✅ Fully supported | | [Propshaft](https://github.com/rails/propshaft) | ✅ Fully supported | | [Sprockets](https://github.com/rails/sprockets) | ✅ Fully supported | | [Webpacker](https://github.com/rails/webpacker) | 🛻 Only with Sprockets or Propshaft | There are two things we need to mention when communicating about assets. 1. Avo's assets 2. Your custom assets ## Avo's assets We chose to impact your app, and your deploy processes as little as possible. That's why we bundle up Avo's assets when we publish on [rubygems](https://rubygems.org/gems/avo), so you don't have to do anything else when you deploy your app. Avo doesn't require a NodeJS, or any kind of any other special environment in your deploy process. Under the hood Avo uses TailwindCSS 3.0 with the JIT engine and bundles the assets using [`jsbundling`](https://github.com/rails/jsbundling-rails) with `esbuild`. ## Your custom assets Avo makes it easy to use your own styles and javascript through your already set up asset pipeline. It just hooks on to it to inject the new assets to be used in Avo. ## Use TailwindCSS utility classes We use TailwindCSS 3.0 with the JIT engine to style Avo, so on release we only pack the used Tailwind classes in our final css file. That's why, when you want to style your custom content (tools, resource tools, fields, or ejected partials), you won't have access to all of Tailwind's utility classes. It's a performance optimization. But there's an easy way to overcome that limitation. You can add your own TailwindCSS process to watch for your the utility classes you use. ```bash bin/rails generate avo:tailwindcss:install ``` That command will: - install `tailwindcss-rails` gem if you haven't installed it yet; - create a custom `avo.tailwind.css` file where you can further customize your Avo space; - generate or enhance your `Procfile.dev` with the required compile `yarn avo:tailwindcss --watch` command, as per default `tailwindcss-rails` practices; - add the resulting file in your `_pre_head.html.erb` file; - prompt you to add the script your `package.json` file. **This is a manual step you need to do**. Now, instead of running `bin/rails server`, you can run that Procfile with `bin/dev` or `foreman start -f Procfile.dev`. :::info You mileage may vary when running these tasks depending with your setup. The gist is that you need to run `yarn avo:tailwindcss` on deploy0time to compile the css file and `yarn avo:tailwindcss --watch` to watch for changes in development. ::: :::warning Add rake task to compile the assets in production This setup works perfectly on your local environment and needs to be run on production too. Add this rake task to have them compiled and ready in production. ```ruby # lib/tasks/avo_assets.rake namespace :avo do desc "Build Avo tailwind assets" task build_avo_tailwind: [:environment] do puts "Building Avo tailwind assets..." `yarn` # this might be optional if you run it before `yarn avo:tailwindcss` end end Rake::Task["assets:precompile"].enhance(["avo:build_avo_tailwind"]) ``` ::: Inside `app/assets/stylesheets` you'll have a new `avo.tailwind.css` file that's waiting for you to customize. The default `tailwind.config.js` file should have the proper paths set up for purging and should be ready to go. ```css @tailwind base; @tailwind components; @tailwind utilities; /* @layer components { .btn-primary { @apply py-2 px-4 bg-blue-200; } } */ ``` ## Add custom JS code and Stimulus controllers There are more ways of dealing with JS assets, and Avo handles that well. ## Use Importmap to add your assets Importmap has become the default way of dealing with assets in Rails 7. For you to start using custom JS assets with Avo and importmap you should run this install command `bin/rails generate avo:js:install`. That will: - create your `avo.custom.js` file as your JS entrypoint; - add it to the `app/views/avo/partials/_head.html.erb` partial so Avo knows to load it; - pin it in your `importmap.rb` file so `importmap-rails` knows to pick it up. ## Use `js-bundling` with `esbuild` `js-bundling` gives you a bit more flexibility and power when it comes to assets. We use that under the hood and we'll use it to expose your custom JS assets. When you install `js-bundling` with `esbuild` you get this npm script `"build": esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets`. That script will take all your JS entrypoint files under `app/javascript` and bundle them under `assets/builds`. ```bash bin/rails generate avo:js:install --bundler esbuild ``` That command will: - eject the `_head.html.erb` file; - add the `avo.custom.js` asset to it; - create the `avo.custom.js` file under `app/javascript` which will be your entrypoint. That will be picked up by the `build` script and create it's own `assets/builds/avo.custom.js` file that will, in turn, be picked up by sprockets or propshaft and loaded into your app. :::info If your project has a `esbuild.config.mjs` file (Jumpstart Pro uses that), please add `avo.custom.js` to the `entryPoints` variable. ::: ## Use `js-bundling` with `rollup` or `webpack` Avo supports the other bundlers too but we just don't have a generator command to configure them for you. If you use the other bundlers and have configured them to use custom assets, then please [open up a PR](https://github.com/avo-hq/avo) and help the community get started faster. ## Manually add your CSS and JS assets In order to manually add your assets you have to eject the `_pre_head.html.erb` partial (`bin/rails generate avo:eject :pre_head`), create the asset files (examples below), and add the asset files from your pipeline to the `_pre_head` partial. Then, your asset pipeline will pick up those assets and use add them to your app. :::warning You should add your custom styles to `_pre_head.html.erb`, versus `_head.html.erb` to avoid overriding Avo's default styles. This The order in which Avo loads the partials and asset files is this one: 1. `_pre_head.html.erb` 2. Avo's CSS and JS assets 3. `_head.html.erb` ::: !Avo and the asset pipeline ### Sprockets and Propshaft Create `avo.custom.js` and `avo.custom.css` inside `app/javascripts` and `app/assets/stylesheets` with the desired scripts and styles. Then add them to Avo using the `_pre_head.html.erb` partial (`rails generate avo:eject :pre_head`). ```erb # app/views/avo/partials/_pre_head.html.erb <%= javascript_include_tag 'avo.custom', defer: true %> <%= stylesheet_link_tag 'avo.custom', media: 'all' %> ``` :::warning Please ensure that when using `javascript_include_tag` you add the `defer: true` option so the browser will use the same loading strategy as Avo's and the javascript files are loaded in the right order. ::: ### Webpacker :::warning We removed support for webpacker. In order to use Avo with your assets you must install Sprockets or Propshaft in order to serve assets like SVG, CSS, or JS files. ::: :::info Instructions below are for Webpacker version 6. Version 5 has different paths (`app/javascript/packs`). ::: Create `avo.custom.js` and `avo.custom.css` inside `app/packs/entrypoints` with the desired scripts and styles. Then add them to Avo using the `_pre_head.html.erb` partial (`rails generate avo:eject :pre_head`). ```erb # app/views/avo/partials/_pre_head.html.erb <%= javascript_pack_tag 'avo.custom', defer: true %> <%= stylesheet_pack_tag 'avo.custom', media: 'all' %> ``` --- # `Avo::PanelComponent` The panel component is one of the most used components in Avo. ```erb <%= render Avo::PanelComponent.new(title: @product.name, description: @product.description) do |c| %> <% c.with_tools do %> <%= a_link(@product.link, icon: 'heroicons/solid/academic-cap', style: :primary, color: :primary) do %> View product <% end %> <% end %> <% c.with_body do %>

Product information

Style: shiny

<% end %> <% end %> ``` ![](/assets/img/native-components/avo-panel-component/index.jpg) ## Options All options are optional. You may render a panel without options. ```erb <%= render Avo::PanelComponent.new do |c| %> <% c.with_body do %> Something here. <% end %> <% end %> ``` ## Slots The component has a few slots where you customize the content in certain areas. --- # Native field components One of the most important features of Avo is the ability to extend it pass the DSL. It's very important to us to enable you to add the features you need and create the best experience for your users. That's why you can so easily create custom fields, resource tools, and custom tools altogether. When you need to augment the UI even more you can use your custom CSS and JS assets too. When you start adding those custom views you might want to add your own fields, and you'd like to make them look like the rest of the app. That's why Avo provides a way to use those fields beyond the DSL, in your own custom Rails partials. ## Declaring fields When you generate a new resource tool you get access to the resource partial. :::details Sample resource tool ```erb
<%= render Avo::PanelComponent.new title: "Post info" do |c| %> <% c.with_tools do %> <%= a_link('/avo', icon: 'heroicons/solid/academic-cap', style: :primary) do %> Dummy link <% end %> <% end %> <% c.with_body do %>

🪧 This partial is waiting to be updated

You can edit this file here app/views/avo/resource_tools/post_info.html.erb.

The resource tool configuration file should be here app/avo/resource_tools/post_info.rb.

<% # In this partial, you have access to the following variables: # tool # @resource # @resource.model # form (on create & edit pages. please check for presence first) # params # Avo::App.context # current_user %>
<% end %> <% end %>
``` ::: You may add new fields using the `avo_show_field`, or `avo_edit_field` methods and use the arguments you are used to from resources. ```ruby # In your resource file field :name, as: :text ``` ```erb <%= avo_edit_field :name, as: :text %> ``` ## The `form` option If this is an or a view, you should pass it the `form` object that an Avo resource tool provides for you. ```erb <%= avo_edit_field :name, as: :text, form: form %> ``` ## The `value` option When you are building a show field and you want to give it a value to show, use the `value` options ```erb <%= avo_show_field(:photo, as: :external_image, value: record.cdn_image) %> ``` ## Other field options The fields take all the field options you are used to like, `help`, `required`, `readonly`, `placeholder`, and more. ```erb <%= avo_edit_field :name, as: :text, form: form, help: "The user's name", readonly: -> { !current_user.is_admin? }, placeholder: "John Doe", nullable: true %> ``` ## Component options The field taks a new `component_options` argument that will be passed to the view component for that field. Please check out the field wrapper documentation for more details on that. ## `avo_field` helper You may use the `avo_field` helper to conditionally switch from `avo_show_field` and `avo_edit_field`. ```erb <%= avo_field :name, as: :text, view: :show %> <%= avo_field :name, as: :text, view: :edit %> <%= avo_field :name, as: :text, view: ExampleHelper.view_conditional %> ``` --- # Field wrappers Each field display in your Avo resource has a field wrapper that helps display it in a cohesive way across the whole app. This not only helps with a unitary design, but also with styling in a future theming feature. :::info You'll probably never have to use these components and helpers by themselves, but we'd like to document how they work as a future reference for everyone. ::: # Index field wrapper ![](/assets/img/field-wrappers/index_field_wrapper.jpg) Each field displayed on the view is wrapped in this component that regulates the way content is displayed and makes it easy to control some options. You may use the component `Avo::Index::FieldWrapperComponent` or the helper `index_field_wrapper`. # Show & Edit field wrapper ![](/assets/img/field-wrappers/show_field_wrapper.jpg) ![](/assets/img/field-wrappers/edit_field_wrapper.jpg) The and field wrappers are actually the same component. You may use the component `Avo::Index::FieldWrapperComponent` or the helper `field_wrapper`. ## Field wrapper areas ![](/assets/img/field-wrappers/field_wrapper_areas.jpg) Each field wrapper is divided in three areas. ### Label This is where the field name is being displayed. This is also where the required asterisk is added for required fields. ### Value This area holds the actual value of the field or it's representation. The falue can be simple text or more advanced types like images, advanced pickers, and content editors. At the bottom the help text is going to be shown on the view and below it the validation error. ### Extra This space is rarely used and it's there just to fill some horizontal space so the content doesn't span to the whole width and maintain its readability. With the introduction of the sidebar, this space will be ignored ## Options ![](/assets/img/field-wrappers/stacked_field.jpg) --- # Testing :::info We know the testing guides aren't very detailed, and some testing helpers are needed. So please send your feedback [here](https://github.com/avo-hq/avo/discussions/1168). ::: Testing is an essential aspect of your app. Most Avo DSLs are Ruby classes, so regular testing methods should apply. ## Testing Actions Given this `ReleaseFish`, this is the `spec` that tests it. ```ruby class ReleaseFish < Avo::BaseAction self.name = "Release fish" self.message = "Are you sure you want to release this fish?" field :message, as: :textarea, help: "Tell the fish something before releasing." def handle(**args) args[:models].each do |model| model.release end succeed "#{args[:models].count} fish released with message '#{args[:fields][:message]}'." end end ``` ```ruby require 'rails_helper' RSpec.feature ReleaseFish, type: :feature do let(:fish) { create :fish } let(:current_user) { create :user } let(:resource) { UserResource.new.hydrate model: fish } it "tests the dummy action" do args = { fields: { message: "Bye fishy!" }, current_user: current_user, resource: resource, models: [fish] } action = described_class.new(model: fish, resource: resource, user: current_user, view: :edit) expect(action).to receive(:succeed).with "1 fish released with message 'Bye fishy!'." expect(fish).to receive(:release) action.handle **args end end ``` --- # `Avo::ApplicationController` ## On extending the `ApplicationController` You may sometimes want to add functionality to Avo's `ApplicationController`. That functionality may be setting attributes to `Current` or multi-tenancy scenarios. When you need to do that, you may feel the need to override it with your own version. That means you go into the source code, find `AVO_REPO/app/controllers/avo/application_controller.rb`, copy the whole thing into your own `YOUR_APP/app/controllers/avo/application_controller.rb` file inside your app, and add your own piece of functionality. ```ruby{10,14-16} # Copied from Avo to `app/controllers/avo/application_controller.rb` module Avo class ApplicationController < ::ActionController::Base include Pagy::Backend include Avo::ApplicationHelper include Avo::UrlHelpers protect_from_forgery with: :exception around_action :set_avo_locale before_action :multitenancy_detector # ... more Avo::ApplicationController methods def multitenancy_detector # your logic here end end end ``` That will work just fine until the next time we update it. After that, we might add a method, remove one, change the before/after actions, update the helpers and do much more to it. **That will definitely break your app the next time when you upgrade Avo**. Avo's private controllers are still considered private APIs that may change at any point. These changes will not appear in the changelog or the upgrade guide. ## Responsibly extending the `ApplicationController` There is a right way of approaching this scenario using Ruby modules or concerns. First, you create a concern with your business logic; then you include it in the parent `Avo::ApplicationController` like so: ```ruby{5-7,9-11,15-18} # app/controllers/concerns/multitenancy.rb module Multitenancy extend ActiveSupport::Concern included do before_action :multitenancy_detector end def multitenancy_detector # your logic here end end # configuration/initializers/avo.rb Rails.configuration.to_prepare do Avo::ApplicationController.include Multitenancy end ``` With this technique, the `multitenancy_detector` method and its `before_action` will be included safely in `Avo::ApplicationController`. --- # Evaluation hosts Avo is a package that does a lot of meta-programming. That means we have a lot of custom functionality passed from the host app to Avo to be executed at different points in time. That functionality can't always be performed in void but requires some pieces of state. We're going to talk all about them below. You'll probably never be going to implement the hosts yourself, but you'll want to know what they contain and how they work. Usually, this happens using lambdas. That's why we created the concept of a `Host`. ## What's a host? A `Host` is an object that holds some pieces of state on which we execute a lambda function. ```ruby require "dry-initializer" # This object holds some data that is usually needed to compute blocks around the app. module Avo module Hosts class BaseHost extend Dry::Initializer option :context, default: proc { Avo::App.context } option :params, default: proc { Avo::App.params } option :view_context, default: proc { Avo::App.view_context } option :current_user, default: proc { Avo::App.current_user } # This is optional because we might instantiate the `Host` first and later hydrate it with a block. option :block, optional: true delegate :authorize, to: Avo::Services::AuthorizationService def handle instance_exec(&block) end end end end # Use it like so. Avo::Hosts::BaseHost.new(block: &some_block).handle ``` ## `BaseHost` The `BaseHost` holds some of the most basic pieces of state like the request `params`, Avo's `context` object, the [`view_context`](https://apidock.com/rails/AbstractController/Rendering/view_context) object, and the `current_user`. As the name states, this is the base host. All other hosts are inherited from it. ### `params` The `params` object is the regular [`params`](https://guides.rubyonrails.org/action_controller_overview.html#parameters) object you are used to. ### Avo's `context` object As you progress throughout building your app, you'll probably configure a `context` object to hold some custom state you need. For example, in `BaseHost` you have access to this object. ### The `view_context` object The [`view_context`](https://apidock.com/rails/AbstractController/Rendering/view_context) object can be used to create the route helpers you are used to (EX: `posts_path`, `new_comment_path`, etc.). When dealing with the `view_context` you have to lean on the object to get those paths. Also, because we are operating under an engine (Avo), the paths must be prefixed with the engine name. Rails' is `main_app`. So if you'd like to output a route to your app `/comments/5/edit`, instead of writing `edit_comment_path 5`, you'd write `view_context.main_app.edit_comment_path 5`. ### The current user Provided that you set up the `:current_user_method`, you'll have access to that output using `current_user` in this block. ## Evaluating the block We talked about the host and the pieces of state it holds; now, let's talk about how we can use it. You're not going to use it when building with Avo. Instead, it's used internally when you pass a block to customize the behavior. For example, it's used when declaring the `visibility` block on `dashboards` or when you try pre-filling the suggestions for the `tags` field. Not all blocks you declare in Avo will be executed in a `Host`. We started implementing `Host`s since v2.0 after the experience gained with v1.0. We plan on refactoring the old block to hosts at some point, but that's going to be a breaking change, so probably in v3.0. You'll probably be prompted in the docs on each block if it's a `Host` or not. Different hosts have different pieces of state. ## `RecordHost` The `RecordHost` inherits from `BaseHost` and has the `record` available. The `record` is the model class instantiated with the DB information (like doing `User.find 1`) in that context. ## `ViewRecordHost` The `ViewRecordHost` inherits from `RecordHost` and has the `view` object available too. ## `ResourceViewRecordHost` The `ResourceViewRecordHost` inherits from `ViewRecordHost` and has the `resource` object available too. ## `AssociationScopeHost` The `AssociationScopeHost` inherits from `BaseHost` and has the `parent` and the `query` objects available. The `parent` is the instantiated model on which the block is given and the `query` is the actual query that is going to run. --- ## Generation Information - **Generated at:** 2025-07-18T08:51:35.972Z - **Total sections:** 42 ### Source Files - docs/2.0/index.md - docs/2.0/rails-and-hotwire.md - docs/2.0/licensing.md - docs/2.0/technical-support.md - docs/2.0/installation.md - docs/2.0/authentication.md - docs/2.0/authorization.md - docs/2.0/cache.md - docs/2.0/resources.md - docs/2.0/controllers.md - docs/2.0/field-options.md - docs/2.0/records-reordering.md - docs/2.0/tabs.md - docs/2.0/resource-sidebar.md - docs/2.0/customizable-controls.md - docs/2.0/associations.md - docs/2.0/associations/belongs_to.md - docs/2.0/associations/has_one.md - docs/2.0/associations/has_many.md - docs/2.0/associations/has_and_belongs_to_many.md - docs/2.0/dashboards.md - docs/2.0/cards.md - docs/2.0/customization.md - docs/2.0/grid-view.md - docs/2.0/map-view.md - docs/2.0/menu-editor.md - docs/2.0/search.md - docs/2.0/filters.md - docs/2.0/actions.md - docs/2.0/localization.md - docs/2.0/branding.md - docs/2.0/custom-tools.md - docs/2.0/custom-fields.md - docs/2.0/resource-tools.md - docs/2.0/stimulus-integration.md - docs/2.0/custom-asset-pipeline.md - docs/2.0/native-components/avo-panel-component.md - docs/2.0/native-field-components.md - docs/2.0/field-wrappers.md - docs/2.0/testing.md - docs/2.0/avo-application-controller.md - docs/2.0/evaluation-hosts.md