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