# Avo for Ruby on Rails Documentation - Version 4.0 Generated from Avo documentation v4.0 for LLM consumption # Getting Started Avo is a tool that helps developers and teams build apps 10x faster. It takes the things we always build for every app and abstracts them in familiar configuration files. It has three main parts: 1. [The CRUD UI](#_1-the-crud-ui) 2. [Dashboards](#_2-dashboards) 3. [The custom content](#_3-the-custom-content) ## 1. The CRUD UI If before, we built apps by creating layouts, adding controller methods to extract _data_ from the database, display it on the screen, worrying how we present it to the user, capture the users input as best we can and writing logic to send that data back to the database, Avo takes a different approach. It only needs to know what kind of data you need to expose and what type it is. After that, it takes care of the rest. You **tell it** you need to manage Users, Projects, Products, or any other types of data and what properties they have; `first_name` as `text`, `birthday` as `date`, `cover_photo` as `file` and so on. There are the basic fields like text, textarea, select and boolean, and the more complex ones like trix, markdown, gravatar, and boolean_group. There's even an amazing file field that's tightly integrated with `Active Storage`. **You've never added files integration as easy as this before.** ## 2. Dashboards Most apps need a way of displaying the stats in an aggregated form. Using the same configuration-based approach, Avo makes it so easy to display data in metric cards, charts, and even lets you take over using partial cards. ## 3. Custom content Avo is a shell in which you develop your app. It offers a familiar DSL to configure the app you're building, but sometimes you might have custom needs. That's where the custom content comes in. You can extend Avo in different layers. For example, in the CRUD UI, you may add Custom fields that slot in perfectly in the current panels and in each view. You can also add Resource tools to control the experience using standard Rails partials completely. You can even create Custom tools where you can add all the content you need using Rails partials or View Components. Most of the places where records are listed like Has many associations, attach modals, search, and more are scopable to meet your multi-tenancy scenarios. Most of the views you see are exportable using the `eject` command. StimulusJS is deeply baked into the CRUD UI and helps you extend the UI and make a complete experience for your users. ## Seamless upgrades Avo comes packaged as a [gem](https://rubygems.org/gems/avo). Therefore, it does not pollute your app with its internal files. Instead, everything is tucked away neatly in the package. That makes for a beautiful upgrade experience. You hit `bundle update avo` and get the newest and best of Avo without any file conflicts. ## Next up Please take your time and read the documentation pages to see how Avo interacts with your app and how one should use it. 1. Install Avo in your app 1. Set up the current user 1. Create a Resource 1. Set up authorization 1. Set up licensing 1. [Explore the live demo app](https://main.avodemo.com/) 1. Explore these docs 1. Enjoy building your app without ever worrying about the admin layer ever again 1. Explore the FAQ pages for guides on how to set up your Avo instance. ## Walkthrough videos ### Build a blog admin panel
### Build a booking app
--- # Technical support Avo is designed to be a self-serve product with [comprehensive documentation](https://docs.avohq.io) and [demo apps](#demo-apps) to be used as references. But, even the best of us get stuck at some point and you might need a nudge in the right direction. There are a few levels of how can get help. 1. [Open Source Software Support Policy](#open-source-software-support-policy) 1. [Self-help](#self-help) 1. [Help from the official team](#official-support) ## Open Source Software Support Avo's Open Source Software (OSS) support primarily revolves around assisting users with issues related to the Avo and other Avo libraries. This involves troubleshooting and providing solutions for problems originating from Avo or its related subcomponents. However, it is crucial to understand that the OSS support does not extend to application-specific issues that do not originate from Avo or its related parts. This includes but is not limited to: - Incorrect application configurations unrelated to Avo. - Conflicts with other libraries or frameworks within your application. - Deployment issues on specific infrastructure or platforms. - Application-specific runtime errors. - Problems caused by third-party plugins or extensions. - Data issues within your application. - Issues related to application performance optimization. - Integration problems with other services or databases. - Design and architecture questions about your specific application. - Language-specific issues are unrelated to Avo or other Avo libraries. We acknowledge that understanding your specific applications and their configuration is essential, but due to the time and resource demands, this goes beyond the scope of our OSS support. :::tip Enhanced support For users seeking assistance with application-specific issues, we offer a few paid technical support plans. These subscriptions provide comprehensive support, including help with application-specific problems. 1. Priority chat support 2. Advanced hands-on support For more information about our support plans, please visit [this](https://avohq.io/support) page. ::: ## Self help This is how you can help yourself. ## Help from the official team You sometimes need help from the authors. There are a few ways to do that. ## Reproduction repository The easiest way for us to troubleshoot and check on an issue is to send us a reproduction repository which we can install and run in our local environments. ```bash # run this command to get a new Rails app with Avo installed rails new -m https://avo.cool/new.rb APP_NAME # run to install avo-pro rails new -m https://avo.cool/new-pro.rb APP_NAME # run to install avo-advanced rails new -m https://avo.cool/new-advanced.rb APP_NAME ``` --- # Best practices Due of the dynamic nature of Ruby, Rails, and Avo, you might be tempted to do a few things differently than how we envisioned them to be done. It's ok if you want to keep doing them like that, but they might not be the most the optimum way of running Avo. Here's a collection of best practices that we'd like you to know about. ## Avoiding `n+1` using `self.includes` `n+1` issues happen, but they are pretty simple to mitigate using Avo. Each resource has the `self.includes` option that helps you eager-load associations. :::info Detailed documentation `self.includes` ::: ## Avoid using `if/else` statements in `def fields` You might be tempted to using `if/else` statements inside the `def fields` method. This practice is discouraged and we'll try to explain why here. Because of checks Avo makes during the request lifecycle, we need to know exactly which fields you have defined for your resource, no matter if they should be hidden or not to a user or in a certain scenario. The alternative is to use the `visible` field option which will add the field on the list, but keep it hidden from the user based on the computed value. ### Example: ```ruby # Scenario 1 def fields if params[:special_case].present? field :special_field, as: :text else field :regular_field, as: :text end end # Scenario 2 def fields field :special_field, as: :text, visible: -> { params[:special_case].present? } field :regular_field, as: :text, visible: -> { params[:special_case].present? } end ``` In the first scenario, where we use the `if/else` statements, depending on how the `params` are set, the fields list will be `[special_field]` or `[regular_field]`, but never both. This will lead to many issues like filters not being visible, params not being properly permitted, and more. In the second scenario, the field list will always be `[special_field, regular_field]` with different visibility rules. Now Avo will know they are both there and set up the request and UI properly. So, please use the `visibility` option and avoid `if/else` in `def fields` whenever possible. ## Add an index on the `created_at` column Avo, by default, sorts the the record on the view by the `created_at` attribute, so it's a good idea to add an index for that column. ```ruby # Example migration class AddIndexOnUsersCreatedAt < ActiveRecord::Migration[7.1] def change add_index :users, :created_at end end ``` --- # Getting Started Avo is a tool that helps developers and teams build apps 10x faster. It takes the things we always build for every app and abstracts them in familiar configuration files. It has three main parts: 1. [The CRUD UI](#_1-the-crud-ui) 2. [Dashboards](#_2-dashboards) 3. [The custom content](#_3-the-custom-content) ## 1. The CRUD UI If before, we built apps by creating layouts, adding controller methods to extract _data_ from the database, display it on the screen, worrying how we present it to the user, capture the users input as best we can and writing logic to send that data back to the database, Avo takes a different approach. It only needs to know what kind of data you need to expose and what type it is. After that, it takes care of the rest. You **tell it** you need to manage Users, Projects, Products, or any other types of data and what properties they have; `first_name` as `text`, `birthday` as `date`, `cover_photo` as `file` and so on. There are the basic fields like text, textarea, select and boolean, and the more complex ones like trix, markdown, gravatar, and boolean_group. There's even an amazing file field that's tightly integrated with `Active Storage`. **You've never added files integration as easy as this before.** ## 2. Dashboards Most apps need a way of displaying the stats in an aggregated form. Using the same configuration-based approach, Avo makes it so easy to display data in metric cards, charts, and even lets you take over using partial cards. ## 3. Custom content Avo is a shell in which you develop your app. It offers a familiar DSL to configure the app you're building, but sometimes you might have custom needs. That's where the custom content comes in. You can extend Avo in different layers. For example, in the CRUD UI, you may add Custom fields that slot in perfectly in the current panels and in each view. You can also add Resource tools to control the experience using standard Rails partials completely. You can even create Custom tools where you can add all the content you need using Rails partials or View Components. Most of the places where records are listed like Has many associations, attach modals, search, and more are scopable to meet your multi-tenancy scenarios. Most of the views you see are exportable using the `eject` command. StimulusJS is deeply baked into the CRUD UI and helps you extend the UI and make a complete experience for your users. ## Seamless upgrades Avo comes packaged as a [gem](https://rubygems.org/gems/avo). Therefore, it does not pollute your app with its internal files. Instead, everything is tucked away neatly in the package. That makes for a beautiful upgrade experience. You hit `bundle update avo` and get the newest and best of Avo without any file conflicts. ## Next up Please take your time and read the documentation pages to see how Avo interacts with your app and how one should use it. 1. Install Avo in your app 1. Set up the current user 1. Create a Resource 1. Set up authorization 1. Set up licensing 1. [Explore the live demo app](https://main.avodemo.com/) 1. Explore these docs 1. Enjoy building your app without ever worrying about the admin layer ever again 1. Explore the FAQ pages for guides on how to set up your Avo instance. ## Walkthrough videos ### Build a blog admin panel
### Build a booking app
--- # Avo ❀️ Rails & Hotwire In order to provide this all-in-one full-interface experience, we are using Rails' built-in [engines functionality](https://guides.rubyonrails.org/engines.html). ## Avo as a Rails engine Avo is a **Ruby on Rails engine** that runs isolated and side-by-side with your app. You configure it using a familiar DSL and sometimes regular Rails code through controller methods and partials. Avo's philosophy is to have as little business logic in your app as possible and give the developer the right tools to extend the functionality when needed. That means we use a few files to configure most of the interface. When that configuration is not enough, we enable the developer to export (eject) partials or even generate new ones for their total control. ### Prepend engine name in URL path helpers Because it's a **Rails engine** you'll have to follow a few engine rules. One of them is that [routes are isolated](https://guides.rubyonrails.org/engines.html#routes). That means that whenever you're using Rails' [path helpers](https://guides.rubyonrails.org/routing.html#generating-paths-and-urls-from-code) you'll need to prepend the name of the engine. For example, Avo's name is `avo,` and your app's engine name is `main_app`. ```ruby # When referencing an Avo route, use avo link_to 'Users', avo.resources_users_path link_to user.name, avo.resources_user_path(user) # When referencing a path for your app, use main_app link_to "Contact", main_app.contact_path link_to post.name, main_app.posts_path(post) ``` ### Use your helpers inside Avo This is something that we'd like to improve in the future, but the flow right now is to 1. include the helper module inside the controller you need it for and then 2. reference the methods from the `view_context.controller` object in resource files or any other place you'd need them. ```ruby{3-5,10,16} # app/helpers/application_helper.rb module ApplicationHelper def render_copyright_info "Copyright #{Date.today.year}" end end # app/controller/avo/products_controller.rb class Avo::ProductsController < Avo::ResourcesController include ApplicationHelper end # app/avo/resources/products_resource.rb class ProductsResource < Avo::BaseResource field :copyright, as: :text do view_context.controller.render_copyright_info end end ``` ## Hotwire Avo's built with Hotwire, so anytime you'd like to use Turbo Frames, that's supported out of the box. ## StimulusJS Avo comes loaded with Stimulus JS and has a quite deep integration with it by providing useful built-in helpers that improve the development experience. Please follow the Stimulus JS guide that takes an in-depth look at all the possible ways of extending the UI. --- # Installation ## Requirements - Ruby on Rails >= 6.1 - Ruby >= 3.1 - `api_only` set to `false`. More here. - `propshaft` or `sprockets` gem - Have the `secret_key_base` defined in any of the following `ENV["SECRET_KEY_BASE"]`, `Rails.application.credentials.secret_key_base`, or `Rails.application.secrets.secret_key_base` :::warning Zeitwerk autoloading is required. When adding Avo to a Rails app that was previously a Rails 5 app you must ensure that it uses zeitwerk for autoloading and Rails 6.1 or higher defaults. ```ruby # config/application.rb config.autoloader = :zeitwerk config.load_defaults 6.1 # 6.1 or higher, depending on your rails version ``` ::: ## Installing Avo ### 1. One-command install Use [this](https://railsbytes.com/public/templates/zyvsME) app template for a one-liner install process. Run this command which will run all the required steps to install Avo in your app. ``` bin/rails app:template LOCATION='https://avohq.io/app-template' ``` ### 2. Manual, step by step. 1. Add the appropiate Avo gem to the `Gemfile` ```ruby # Add one of the following in your Gemfile depending on the tier you are on. # Avo Community gem "avo", ">= 3.2.1" # Avo Pro gem "avo", ">= 3.2.1" gem "avo-pro", ">= 3.2.0", source: "https://packager.dev/avo-hq/" # Avo Advanced gem "avo", ">= 3.2.1" gem "avo-advanced", ">= 3.2.0", source: "https://packager.dev/avo-hq/" ``` :::info Please use this guide to find the best authentication strategy for your use-case. ::: 2. Run `bundle install`. 3. Run `bin/rails generate avo:install` to generate the initializer and add Avo to the `routes.rb` file. 4. Generate an Avo Resource :::info This will mount the app under `/avo` path. Visit the link to see the result. ::: ### 3. In popular Rails starter kits We have integrations with the most popular starter kits. #### Bullet Train Avo comes pre-installed in all new Bullet Train applications. I you have a Bullet Train app and you'd like to add Avo, please user [this template](https://avohq.io/templates/bullet-train). ```ruby bin/rails app:template LOCATION=https://v3.avohq.io/templates/bullet-train.template ``` #### Jumpstart Pro To install Avo in a Jumpstart Pro app use [this template](https://avohq.io/templates/jumpstart-pro). ```ruby bin/rails app:template LOCATION=https://v3.avohq.io/templates/jumpstart-pro.template ``` ## Install from GitHub You may also install Avo from GitHub but when you do that you must compile the assets yourself. You do that using the `rake avo:build-assets` command. When pushing to production, make sure you build the assets on deploy time using this task. ```ruby # Rakefile Rake::Task["assets:precompile"].enhance do Rake::Task["avo:build-assets"].execute end ``` :::info If you don't have the `assets:precompile` step in your deployment process, please adjust that with a different step you might have like `db:migrate`. ::: ## Mount Avo to a subdomain You can use the regular `host` constraint in the `routes.rb` file. ```ruby constraint host: 'avo' do mount_avo at: '/' end ``` ## Next steps Please follow the next steps to ensure your app is secured and you have access to all the features you need. 1. Set up authentication and tell Avo who is your `current_user`. This step is required for the authorization feature to work. 1. Set up authorization. Don't let your data be exposed. Give users access to the data they need to see. 1. Set up licensing. --- # Gem server authentication Avo comes in a few tiers. The Community tier which comes as a free gem available on rubygems.org and a few paid tiers which come in private gems hosted on our own private gems server (packager.dev). In order to have access to the paid gems you must authenticate using the **Gem Server Token** found on your [license page](https://v3.avohq.io/licenses). There are a few ways to do that, but we will focus on the most important and secure ones for [on the server and CI systems](#on-the-server-and-ci-systems) and [on your local development environment](#on-your-local-development-environment). :::info We'll use the `xxx` notiation instead of the actual gem server token. ::: ## On the server and CI systems :::info Recommendation This is the recommended way for most use cases. ::: The best way to do it is to register this environment variable so bundler knows to use it when pulling packages from [`packager.dev`](https://packager.dev). ```bash export BUNDLE_PACKAGER__DEV=xxx # or BUNDLE_PACKAGER__DEV=xxx bundle install ``` Each hosting service will have their own way to add environment variables. Check out how to do it on [Heroku](#Heroku), [Hatchbox](#Hatchbox), [Docker](#docker_and_docker_compose), [Kamal](#Kamal) or [GitHub Actions](#git_hub_actions). :::warning Warning about using the `.env` file You might be tempted to add the token to your `.env` file, as you might do with your Rails app. That will not work because `bundler` will not automatically load those environment variables. You should add the environment variable through the service dedicated page or by running the `export` command before `bundle install`. ::: ## On your local development environment For your local development environment you should add the token to the default bundler configuration. This way `bundler` is aware of it without having to specify it in the `Gemfile`. ```bash bundle config set --global https://packager.dev/avo-hq/ xxx ``` ## Add Avo to your `Gemfile` Now you are ready to add Avo to your `Gemfile`. ```ruby # Add one of the following in your Gemfile depending on the tier you are on. # Avo Community gem "avo", ">= 3.2.1" # Avo Pro gem "avo", ">= 3.2.1" gem "avo-pro", ">= 3.2.0", source: "https://packager.dev/avo-hq/" # Avo Advanced gem "avo", ">= 3.2.1" gem "avo-advanced", ">= 3.2.0", source: "https://packager.dev/avo-hq/" ``` Now you can run `bundle install` and `bundler` will pick it up and use it to authenticate on the server. ## Bundle without paid gems If you need to distribute your Rails app without the paid gems you can move them to an optional group. ```bash RAILS_GROUPS=avo BUNDLE_WITH=avo bundle install ``` ```ruby # Gemfile gem 'avo', group :avo, optional: true do source "https://packager.dev/avo-hq/" do gem "avo-advanced", "~> 3.17" end end ``` ## FAQ Frequently asked questions: --- # License troubleshooting There might be times when the configurations isn't up to date and you'd like to troubleshoot it. There are a couple of things you can do to perform a self-diagnostics session. ## Check the license status page Every Avo app has the license status page where you can see a few things about your license and the response from the license checking server. Go to `https://yourapp.com/avo/avo_private/status`. If you mounted Avo under a different path (like `admin`) it will be `https://yourapp.com/admin/avo_private/status`. In order to see that page your user has to be an an admin in Avo. Follow this guide to mark your user as an admin. This should tell you if the license authenticated correctly, what is your used license key and what was the response from our checking server. ## Frequent issues --- # Authentication With Avo, you have the flexibility to build apps either with or without authentication. While Avo has minimal assumptions about its users, a few guidelines still apply: 1. Users can be either authenticated or not. Avo apps can be developed without requiring user authentication. 2. If you choose to implement authentication, you need to [define the current_user](#customize-the-current-user-method). 3. You can assign [lightweight roles](#user-roles) to your users. 4. Any authentication strategy or gem of your choice can be utilized. :::info Rails 8 authentication scaffold In essence, the [authentication scaffold](https://github.com/rails/rails/pull/52328) that Rails 8 comes with is custom authentication so we need to do a few things to ensure it's working. Please follow this guide to enable it. ::: ## Customize the `current_user` method Avo will not assume your authentication provider (the `current_user` method returns `nil`). That means that you have to tell Avo who the `current_user` is. ## Customize the sign-out link If your app responds to `destroy_user_session_path`, a sign-out menu item will be added on the bottom sidebar (when you click the three dots). If your app does not respond to this method, the link will be hidden unless you provide a custom sign-out path. There are two ways to customize the sign-out path. ### Customize the current user resource name You can customize just the "user" part of the path name by setting `current_user_resource_name`. For example if you follow the `User` -> `current_user` convention, you might have a `destroy_current_user_session_path` that logs the user out. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.current_user_resource_name = :current_user end ``` Or if your app provides a `destroy_current_admin_session_path` then you would need to set `current_user_resource_name` to `current_admin`. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.current_user_resource_name = :current_admin end ``` ### Customize the entire sign-out path Alternatively, you can customize the sign-out path name completely by setting `sign_out_path_name`. For example, if your app provides `logout_path` then you would pass this name to `sign_out_path_name`. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.sign_out_path_name = :logout_path end ``` If both `current_user_resource_name` and `sign_out_path_name` are set, `sign_out_path_name` takes precedence. ## Filter out requests You probably do not want to allow Avo access to everybody. If you're using [devise](https://github.com/heartcombo/devise) in your app, use this block to filter out requests in your `routes.rb` file. ```ruby authenticate :user do mount_avo at: '/avo' end ``` You may also add custom user validation such as `user.admin?` to only permit a subset of users to your Avo instance. ```ruby authenticate :user, -> user { user.admin? } do mount_avo at: '/avo' end ``` Check out more examples of authentication on [sidekiq's authentication section](https://github.com/mperham/sidekiq/wiki/Monitoring#authentication). ## `authenticate_with` method Alternatively, you can use the `authenticate_with` config attribute. It takes a block and evaluates it in Avo's `ApplicationController` as a `before_action`. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.authenticate_with do authenticate_admin_user end end ``` Note that Avo's `ApplicationController` does not inherit from your app's `ApplicationController`, so any protected methods you defined would not work. Instead, you would need to explicitly write the authentication logic in the block. For example, if you store your `user_id` in the session hash, then you can do: ```ruby # config/initializers/avo.rb Avo.configure do |config| config.authenticate_with do redirect_to '/' unless session[:user_id] == 1 # hard code user ids here end end ``` ## User roles There might be cases where you want to signal to Avo that the current user has a role. Avo roles are very lightweight and that's for a reason. Building with roles in mind requires a bit more integration with the parent app. This is something that you can definitely take upon yourself to build if you need it. There are two extra roles that you can give to a user, besides the regular user. These roles can be assigned with a check on the `current_user` object. ### Customize the methods that check for roles You may customize the methods that Avo uses to assign roles in the initializer. ```ruby # config/avo.rb Avo.configure do |config| config.is_admin_method = :is_admin? config.is_developer_method = :is_developer? end ``` ## Authorization When you share access to Avo with your clients or large teams, you may want to restrict access to a resource or a subset of resources. You should set up your authorization rules (policies) to do that. Check out the authorization page for details on how to set that up. --- # Authorization When you share access to Avo with your clients or large teams, you may want to restrict access to a resource or a subset of resources. One example may be that only admin-level users may delete or update records. Avo provides a [Pundit](https://github.com/varvet/pundit) client out of the box for authorization that uses a policy system to manage access. :::info Pundit alternative Pundit is just the default client. You may plug in your own client using the instructions [here](#custom-authorization-clients). You can use [this](https://github.com/avo-hq/avo/issues/1922) `action_policy` client as well. ::: :::warning You must manually require `pundit` or your authorization library in your `Gemfile`. ```ruby # Minimal authorization through OO design and pure Ruby classes gem "pundit" ``` And update config/initializers/avo.rb with following configuration: ```ruby # Example of enabling authorization client in Avo configuration config.authorization_client = :pundit ``` ::: ## Ensure Avo knows who your current user is Before setting any policies up, please ensure Avo knows your current user. Usually, this πŸ‘‡ set up should be fine, but follow the authentication guide for more information. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.current_user_method = :current_user end ``` ## Policies Just run the regular pundit `bin/rails g pundit:policy Post` to generate a new policy. **If this is a new app you need to install pundit first bin/rails g pundit:install.** With this new policy, you may control what every type of user can do with Avo. The policy has the default methods for the regular controller actions: `index?`, `show?`, `create?`, `new?`, `update?`, `edit?` and `destroy?`. These methods control whether the resource appears on the sidebar, if the view/edit/destroy buttons are visible or if a user has access to those index/show/edit/create pages. ## Associations When using associations, you would like to set policies for `creating` new records on the association, allowing to `attach`, `detach`, `create` or `destroy` relevant records. Again, Avo makes this easy using a straightforward naming schema. :::warning Make sure you use the same pluralization as the association name. For a `has_many :users` association use the plural version method `view_users?`, `edit_users?`, `detach_users?`, etc., not the singular version `detach_user?`. ::: ### Example scenario We'll have this example of a `Post` resource with many `Comment`s through the `has_many :comments` association. :::info The `record` variable in policy methods In the `Post` `has_many` `Comments` example, when you want to authorize `show_comments?` in `PostPolicy` you will have a `Comment` instance as the `record` variable, but when you try to authorize the `attach_comments?`, you won't have that `Comment` instance because you want to create one, but we expose the parent `Post` instance so you have more information about that authorization action that you're trying to make. ::: ## Removing duplication :::info A note on duplication Let's take the following example: A `User` has many `Contract`s. And you represent that in your Avo resource. How do you handle authorization to the `ContractResource`? For one, you set the `ContractPolicy.index?` and `ContractPolicy.edit?` methods to `false` so regular users don't have access to all contracts (see and edit), and the `UserPolicy.view_contracts?` and `UserPolicy.edit_contracts?` set to `false`, because, when viewing a user you want to see all the contracts associated with that user and don't let them edit it. You might be thinking that there's code duplication here. "Why do I need to set a different rule for `UserPolicy.edit_contracts?` when I already set the `ContractPolicy.edit?` to `false`? Isn't that going to take precedence?" Now, let's imagine we have a user that is an admin in the application. The business need is that an admin has access to all contracts and can edit them. This is when we go back to the `ContractPolicy.edit?` and turn that to true for the admin user. And now we can separately control who and where a user can edit a contract. ::: You may remove duplication by applying the same policy rule from the original policy. ```ruby class CommentPolicy # ... more policy methods def edit record.user_id == current_user.id end end class PostPolicy # ... more policy methods def edit_comments? Pundit.policy!(user, record).edit? end end ``` Now, whatever action you take for one comment, it will be available for the `edit_comments?` method in `PostPolicy`. From version 2.31 we introduced a concern that removes the duplication and helps you apply the same rules to associations. You should include `Avo::Pro::Concerns::PolicyHelpers` in the `ApplicationPolicy` for it to be applied to all policy classes. `PolicyHelpers` allows you to use the method `inherit_association_from_policy`. This method takes two arguments; `association_name` and the policy file you want to be used as a template. ```ruby inherit_association_from_policy :comments, CommentPolicy ``` With just one line of code, it will define the following methods to policy your association: ```ruby def create_comments? CommentPolicy.new(user, record).create? end def edit_comments? CommentPolicy.new(user, record).edit? end def update_comments? CommentPolicy.new(user, record).update? end def destroy_comments? CommentPolicy.new(user, record).destroy? end def show_comments? CommentPolicy.new(user, record).show? end def reorder_comments? CommentPolicy.new(user, record).reorder? end def act_on_comments? CommentPolicy.new(user, record).act_on? end def view_comments? CommentPolicy.new(user, record).index? end # Since Version 3.10.0 def attach_comments? CommentPolicy.new(user, record).attach? end def detach_comments? CommentPolicy.new(user, record).detach? end ``` Although these methods won't be visible in your policy code, you can still override them. For instance, if you include the following code in your `CommentPolicy`, it will be executed in place of the one defined by the helper: ```ruby inherit_association_from_policy :comments, CommentPolicy def destroy_comments? false end ``` ## Attachments When working with files, it may be necessary to establish policies that determine whether users can `upload`, `download` or `delete` files. Fortunately, Avo simplifies this process by providing a straightforward naming schema for these policies. Both the `record` and the `user` will be available for you to access. :::info AUTHORIZE IN BULK If you want to allow or disallow these methods in bulk you can use a little meta-programming to assign all the same value. ```ruby [:cover_photo, :audio].each do |file| [:upload, :download, :delete].each do |action| define_method "#{action}_#{file}?" do true end end end ``` ::: ## Scopes You may specify a scope for the , , and views. ```ruby{3-9} class PostPolicy < ApplicationPolicy class Scope < Scope def resolve if user.admin? scope.all else scope.where(published: true) end end end end ``` :::warning This scope will be applied only to the view of Avo. It will not be applied to the association view. Example: A `Post` has_many `Comment`s. The `CommentPolicy::Scope` will not affect the `has_many` field. You need to add the `scope` option to the `has_many` field where you can modify the query. ```ruby # The `parent` is the Post instance that the user is seeing. ex: Post.find(1) # The `query` is the Active Record query being done on the comments. ex: post.comments field :comments, as: :has_many, scope: -> { Pundit.policy_scope(parent, query) } ``` ::: ## Using different policy methods By default Avo will use the generated Pundit methods (`index?`, `show?`, `create?`, `new?`, `update?`, `edit?` and `destroy?`). But maybe, in your app, you're already using these methods and would like to use different ones for Avo. You may want override these methods inside your configuration with a simple map using the `authorization_methods` key. ```ruby{6-14} Avo.configure do |config| config.root_path = '/avo' config.app_name = 'Avocadelicious' config.license_key = ENV['AVO_LICENSE_KEY'] config.authorization_methods = { index: 'avo_index?', show: 'avo_show?', edit: 'avo_edit?', new: 'avo_new?', update: 'avo_update?', create: 'avo_create?', destroy: 'avo_destroy?', search: 'avo_search?', } end ``` Now, Avo will use `avo_index?` instead of `index?` to manage the **Index** view authorization. ## Use Resource's Policy to authorize custom actions It may be necessary to authorize a specific field or custom action of a resource using a policy class rather than defining the authorization logic directly within the resource class. By doing so, we can delegate control to the policy class, ensuring a cleaner and more maintainable authorization structure. :::code-group ```ruby [app/resources/product.rb]{8} field :amount, as: :money, currencies: %w[USD], sortable: true, filterable: true, copyable: true, # define ability to change the amount in policy class instead of doing it here disabled: -> { !@resource.authorization.authorize_action(:amount?, raise_exception: false) } ``` ```ruby [app/policies/product_policy.rb]{2-4} # Define ability to change the amount in Product Policy def amount? user.admin? end ``` ::: ## Raise errors when policies are missing The default behavior of Avo is to allow missing policies for resources silently. So, if you have a `User` model and a `Avo::Resources::User` but don't have a `UserPolicy`, Avo will not raise errors regarding missing policies and authorize that resource. If, however, you need to be on the safe side of things and raise errors when a Resource is missing a Policy, you can toggle on the `raise_error_on_missing_policy` configuration. ```ruby{7} # config/initializers/avo.rb Avo.configure do |config| config.root_path = '/avo' config.app_name = 'Avocadelicious' config.license_key = ENV['AVO_LICENSE_KEY'] config.raise_error_on_missing_policy = true end ``` Now, you'll have to provide a policy for each resource you have in your app, thus making it a more secure app. ## Logs Developers have the ability to monitor any unauthorized actions. When a developer user makes a request that triggers an unauthorized action, a log entry similar to the following will be generated: In development each log entry provides details about the policy class, the action attempted, the global id of the user who made the request, and the global id of the record involved: ```bash web | [Avo->] Unauthorized action 'reorder?' for 'UserPolicy' web | user: gid://dummy/User/20 web | record: gid://dummy/User/31 ``` To find a record based on its global id you can use `GlobalID::Locator.locate` ```ruby gid = "gid://dummy/User/20" user = GlobalID::Locator.locate(gid) ``` In production each log entry provides details only about the policy class and the attempted action: ```bash web | [Avo->] Unauthorized action 'act_on?' for 'UserPolicy' ``` ## Custom policies By default, Avo will infer the policy from the model of the resource object. If you wish to use a different policy for a given resource, you can specify it directly in the resource using the `authorization_policy` option. ```ruby # app/avo/resources/photo_comment.rb class Avo::Resources::PhotoComment < Avo::BaseResource self.model_class = "Comment" self.authorization_policy = PhotoCommentPolicy # ... end ``` ## Custom authorization clients :::info Check out the [Pundit client](https://github.com/avo-hq/avo/blob/main/lib/avo/services/authorization_clients/pundit_client.rb) for reference. ::: ### Change the authorization client In order to use a different client change the `authorization_client` option in the initializer. The built-in possible values are `nil` and `:pundit`. When you create your own client, pass the class name. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.authorization_client = 'Services::AuthorizationClients::CustomClient' end ``` ### Client methods Each authorization client must expose a few methods. ## Explicit authorization ## Rolify integration Check out this guide to add rolify role management with Avo. --- # Resource options Avo effortlessly empowers you to build an entire customer-facing interface for your Ruby on Rails application. One of the most powerful features is how easy you can administer your database records using the CRUD UI. ## Overview Similar to how you configure your database layer using the Rails models and their DSL, Avo's CRUD UI is configured using `Resource` files. Each `Resource` maps out one of your models. There can be multiple `Resource`s associated to the same model if you need that. All resources are located in the `app/avo/resources` directory. ## Resources from model generation ```bash bin/rails generate model car make:string mileage:integer ``` Running this command will generate the standard Rails files (model, controller, etc.) and `Avo::Resources::Car` & `Avo::CarsController` for Avo. The auto-generated resource file will look like this: ```ruby # app/avo/resources/car.rb class Avo::Resources::Car < Avo::BaseResource self.includes = [] # self.search = { # query: -> { query.ransack(id_eq: params[:q], m: "or").result(distinct: false) } # } def fields field :id, as: :id field :make, as: :text field :mileage, as: :number end end ``` This behavior can be omitted by using the argument `--skip-avo-resource`. For example if we want to generate a `Car` model but no Avo counterpart we should use the following command: ```bash bin/rails generate model car make:string kms:integer --skip-avo-resource ``` ## Manually defining resources ```bash bin/rails generate avo:resource post ``` This command will generate the `Post` resource file in `app/avo/resources/post.rb` with the following code: ```ruby # app/avo/resources/post.rb class Avo::Resources::Post < Avo::BaseResource self.includes = [] # self.search = { # query: -> { query.ransack(id_eq: params[:q], m: "or").result(distinct: false) } # } def fields field :id, as: :id end end ``` From this config, Avo will infer a few things like the resource's model will be the `Post` model and the name of the resource is `Post`. But all of those inferred things are actually overridable. Now, let's say we already have a model `Post` well defined with attributes and associations. In that case, the Avo resource will be generated with the fields attributes and associations. ::: code-group ```ruby [app/models/post.rb] # == Schema Information # # Table name: posts # # id :bigint not null, primary key # name :string # body :text # is_featured :boolean # published_at :datetime # user_id :bigint # created_at :datetime not null # updated_at :datetime not null # status :integer default("draft") # class Post < ApplicationRecord enum status: [:draft, :published, :archived] validates :name, presence: true has_one_attached :cover_photo has_one_attached :audio has_many_attached :attachments belongs_to :user, optional: true has_many :comments, as: :commentable has_many :reviews, as: :reviewable acts_as_taggable_on :tags end ``` ```ruby [app/avo/resources/post.rb] class Avo::Resources::Post < Avo::BaseResource self.includes = [] # self.search = { # query: -> { query.ransack(id_eq: params[:q], m: "or").result(distinct: false) } # } def fields field :id, as: :id field :name, as: :text field :body, as: :textarea field :is_featured, as: :boolean field :published_at, as: :datetime field :user_id, as: :number field :status, as: :select, enum: ::Post.statuses field :cover_photo, as: :file field :audio, as: :file field :attachments, as: :files field :user, as: :belongs_to field :comments, as: :has_many field :reviews, as: :has_many field :tags, as: :tags end end ``` ::: It's also possible to specify the resource model class. For example, if we want to create a new resource named `MiniPost` resource using the `Post` model we can do that using the following command: ```bash bin/rails generate avo:resource mini-post --model-class post ``` That command will create a new resource with the same attributes as the post resource above with specifying the `model_class`: ```ruby class Avo::Resources::MiniPost < Avo::BaseResource self.model_class = "Post" end ``` :::info You can see the result in the admin panel using this URL `/avo`. The `Post` resource will be visible on the left sidebar. ::: ## Generating resources for all models To generate Avo resources for all models in your application, run: ```bash bin/rails generate avo:all_resources ``` ### What it does 1. Scans your `app/models` directory for all model files 2. Excludes `ApplicationRecord` from the generation process 3. For each model found, it: - Generates a corresponding Avo resource using the `avo:resource` generator - Handles errors gracefully, printing error messages if generation fails for any model This is particularly useful when: - Setting up Avo in an existing Rails application - Ensuring all your models have corresponding Avo resources ## Fields `Resource` files tell Avo what records should be displayed in the UI, but not what kinds of data they hold. You do that using the `fields` method. Read more about the fields here. ```ruby{5-17} class Avo::Resources::Post < Avo::BaseResource self.title = :id self.includes = [] def fields field :id, as: :id field :name, as: :text, required: true field :body, as: :trix, placeholder: "Add the post body here", always_show: false field :cover_photo, as: :file, is_image: true, link_to_record: true field :is_featured, as: :boolean field :is_published, as: :boolean do record.published_at.present? end field :user, as: :belongs_to, placeholder: "β€”" end end ``` ## Routing Avo will automatically generate routes based on the resource name when generating a resource. ``` Avo::Resources::Post -> /avo/resources/posts Avo::Resources::PhotoComment -> /avo/resources/photo_comments ``` If you change the resource name, you should change the generated controller name too. ## Use multiple resources for the same model Usually, an Avo Resource maps to one Rails model. So there will be a one-to-one relationship between them. But there will be scenarios where you'd like to create another resource for the same model. Let's take as an example the `User` model. You'll have an `User` resource associated with it. ```ruby # app/models/user.rb class User < ApplicationRecord end # app/avo/resources/user.rb class Avo::Resources::User < Avo::BaseResource self.title = :name def fields field :id, as: :id, link_to_record: true field :email, as: :gravatar, link_to_record: true, as_avatar: :circle field :first_name, as: :text, required: true, placeholder: "John" field :last_name, as: :text, required: true, placeholder: "Doe" end end ``` So when you click on the Users sidebar menu item, you get to the `Index` page where all the users will be displayed. The information displayed will be the gravatar image, the first and the last name. Let's say we have a `Team` model with many `User`s. You'll have a `Team` resource like so: ```ruby{12} # app/models/team.rb class Team < ApplicationRecord end # app/avo/resources/team.rb class Avo::Resources::Team < Avo::BaseResource self.title = :name def fields field :id, as: :id, link_to_record: true field :name, as: :text field :users, as: :has_many end end ``` From that configuration, Avo will figure out that the `users` field points to the `User` resource and will use that one to display the users. But, let's imagine that we don't want to display the gravatar on the `has_many` association, and we want to show the name on one column and the number of projects the user has on another column. We can create a different resource named `TeamUser` resource and add those fields. ```ruby # app/avo/resources/team_user.rb class Avo::Resources::TeamUser < Avo::BaseResource self.title = :name def fields field :id, as: :id, link_to_record: true field :name, as: :text field :projects_count, as: :number end end ``` We also need to update the `Team` resource to use the new `TeamUser` resource for reference. ```ruby # app/avo/resources/team.rb class Avo::Resources::Team < Avo::BaseResource self.title = :name def fields field :id, as: :id, link_to_record: true field :name, as: :text field :users, as: :has_many, use_resource: Avo::Resources::TeamUser end end ``` But now, if we visit the `Users` page, we will see the fields for the `TeamUser` resource instead of `User` resource, and that's because Avo fetches the resources in an alphabetical order, and `TeamUser` resource is before `User` resource. That's definitely not what we want. The same might happen if you reference the `User` in other associations throughout your resource files. To mitigate that, we are going to use the `model_resource_mapping` option to set the "default" resource for a model. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.model_resource_mapping = { 'User': 'Avo::Resources::User' } end ``` That will "shortcircuit" the regular alphabetical search and use the `User` resource every time we don't specify otherwise. We can still tell Avo which resource to use in other `has_many` or `has_and_belongs_to_many` associations with the `use_resource` option. ## Namespaced resources `Resource`s can't be namespaced yet, so they all need to be in the root level of that directory. If you have a model `Super::Dooper::Trooper::Model` you can use `Avo::Resources::SuperDooperTrooperModel` with the `model_class` option. ```ruby class Avo::Resources::SuperDooperTrooperModel < Avo::BaseResource self.model_class = "Super::Dooper::Trooper::Model" end ``` ## Views Please read the detailed views page. ## Extending `Avo::ResourcesController` You may need to execute additional actions on the `ResourcesController` before loading the Avo pages. You can create an `Avo::BaseResourcesController` and extend your resource controller from it. ```ruby # app/controllers/avo/base_resources_controller.rb class Avo::BaseResourcesController < Avo::ResourcesController include AuthenticationController::Authentication before_action :is_logged_in? end # app/controllers/avo/posts_controller.rb class Avo::PostsController < Avo::BaseResourcesController end ``` :::warning You can't use `Avo::BaseController` and `Avo::ResourcesController` as **your base controller**. They are defined inside Avo. ::: When you generate a new resource or controller in Avo, it won't automatically inherit from the `Avo::BaseResourcesController`. However, you have two approaches to ensure that the new generated controllers inherit from a custom controller: ### `--parent-controller` option on the generators Both the `avo:controller` and `avo:resource` generators accept the `--parent-controller` option, which allows you to specify the controller from which the new controller should inherit. Here are examples of how to use it: ```bash rails g avo:controller city --parent-controller Avo::BaseResourcesController rails g avo:resource city --parent-controller Avo::BaseResourcesController ``` ### `resource_parent_controller` configuration option You can configure the `resource_parent_controller` option in the `avo.rb` initializer. This option will be used to establish the inherited controller if the `--parent-controller` argument is not passed on the generators. Here's how you can do it: ```ruby Avo.configure do |config| # ... config.resource_parent_controller = "Avo::BaseResourcesController" # "Avo::ResourcesController" is default value # ... end ``` ### Attach concerns to `Avo::BaseController` Alternatively you can use [this guide](https://avohq.io/blog/safely-extend-a-ruby-on-rails-controller) to attach methods, actions, and hooks to the main `Avo::BaseController` or `Avo::ApplicationController`. ## Manually registering resources In order to have a more straightforward experience when getting started with Avo, we are eager-loading the `app/avo/resources` directory. That makes all those resources available to your app without you doing anything else. If you want to manually load them use the `config.resources` option. ```ruby # config/initializers/avo.rb Avo.configure do |config| config.resources = [ "Avo::Resources::User", "Avo::Resources::Fish", ] end ``` This tells Avo which resources you use and stops the eager-loading process on boot-time. This means that other resources that are not declared in this array will not show up in your app. ## Extending `Avo::BaseResource` we have restructured the `Avo::BaseResource` to enhance user customization capabilities. The existing functionality has been moved to a new base class `Avo::Resources::Base`, and `Avo::BaseResource` is now left empty for user overrides. This allows users to easily add custom methods that all of their resources will inherit, without having to modify the internal base class. ### How to Customize `Avo::BaseResource` You can customize `Avo::BaseResource` by creating your own version in your application. This custom resource can include methods and logic that you want all your resources to inherit. Here's an example to illustrate how you can do this: ```ruby # app/avo/base_resource.rb module Avo class BaseResource < Avo::Resources::Base # Example custom method: make all number fields cast their values to float def field(id, **args, &block) if args[:as] == :number args[:format_using] = -> { value.to_f } end super(id, **args, &block) end end end ``` All your resources will now inherit from your custom `Avo::BaseResource`, allowing you to add common functionality across your admin interface. For instance, the above example ensures that all number fields in your resources will have their values cast to floats. You can add any other shared methods or customizations here, making it easier to maintain consistent behavior across all resources. ### Your resource files Your resource file will still look the same as it did before. ```ruby # app/avo/resources/post_resource.rb module Avo::Resources::Post < Avo::BaseResource # Your existing configuration for the Post resource end ``` ## Resource Options Resources have a few options available for customization. ## Cards Use the `def cards` method to add some cards to your resource. Check cards documentation for more details. ```ruby{9-19} class Avo::Resources::User < Avo::BaseResource def fields field :id, as: :id field :name, as: :text field :email, as: :text field :roles, as: :boolean_group, options: {admin: "Administrator", manager: "Manager", writer: "Writer"} end def cards card Avo::Cards::ExampleAreaChart, cols: 3 card Avo::Cards::ExampleMetric, cols: 2 card Avo::Cards::ExampleMetric, label: "Active users metric", description: "Count of the active users.", arguments: { active_users: true }, visible: -> { !resource.view.form? } end end ``` Cards on resources - Avo for Rails --- # Array Resources ## Overview An **Array Resource** is a flexible resource that can be backed by an **array of hashes** or an **array of Active Record objects**. It is not constrained to an Active Record model and allows dynamic data handling. :::info Related field The Array Resource can be used in conjunction with the `Array` field to manage structured array data in your resources. For more details on using the `Array` field, including examples and hierarchy of data fetching, check out the Array Field documentation. This integration allows for seamless configuration of dynamic or predefined array-based data within your application. ::: :::warning ⚠️ Limitations #### Sorting - The array resource does **not support sorting**. #### Performance Considerations - When dealing with large datasets, you might experience suboptimal performance due to inherent architectural constraints. - **Caching Recommendation:** - It is advisable to implement caching mechanisms as a viable solution to ameliorate these performance bottlenecks. - **Note:** These caching mechanisms should ideally be integrated into the methods that fetch data, such as the `def records` method. **Please note that these caveats are based on the current implementation and may be subject to revisions in future releases.** :::
## Creating an Array Resource Generate an **Array Resource** using the `--array` flag: ```bash bin/rails generate avo:resource Movie --array ``` This sets up a resource designed to work with an array of data. ## Defining the `records` Method The `records` method serves as the fallback source for data in the resource. It returns an array of hashes or Active Record objects. ### Example ```ruby def records [ { id: 1, name: "The Shawshank Redemption", release_date: "1994-09-23" }, { id: 2, name: "The Godfather", release_date: "1972-03-24", fun_fact: "The iconic cat in the opening scene was a stray found by director Francis Ford Coppola on the studio lot." }, { id: 3, name: "Pulp Fiction", release_date: "1994-10-14" } ] end ``` ## Defining Fields Array Resources use fields like any other Avo resource. Here’s an example for a `Movie` resource: ```ruby class Avo::Resources::Movie < Avo::Resources::ArrayResource def records [ { id: 1, name: "The Shawshank Redemption", release_date: "1994-09-23" }, { id: 2, name: "The Godfather", release_date: "1972-03-24", fun_fact: "The iconic cat in the opening scene was a stray found by director Francis Ford Coppola on the studio lot." }, { id: 3, name: "Pulp Fiction", release_date: "1994-10-14" } ] end def fields main_panel do field :id, as: :id field :name, as: :text field :release_date, as: :date field :fun_fact, only_on: :index, visible: -> { resource.record.fun_fact.present? } do record.fun_fact.truncate_words(10) end sidebar do field :fun_fact do record.fun_fact || "There is no register of a fun fact for #{record.name}" end end end end end ``` --- # HTTP Resources ## Overview An **HTTP Resource** is a flexible resource that can be backed by an **endpoint** request. Unlike traditional resources tied to Active Record models, HTTP Resources allow dynamic interaction with external APIs and non-persistent data sources. :::warning ⚠️ Limitations - The HTTP Resource does **not support sorting** at this time. **Please note that these limitations stem from the current implementation and may evolve in future releases.** ::: ## Installing the gem To enable HTTP Resource functionality in your Avo project, you need to include the `avo-http_resource` gem. Add it to your Gemfile: ```ruby gem "avo-http_resource", source: "https://packager.dev/avo-hq/" ``` Then install it: ```bash bundle install ``` Once the gem is installed, HTTP Resources will be available as a new type of resource, enabling you to connect seamlessly with external APIs and custom data endpoints, no Active Record necessary. ## Creating an HTTP Resource You can generate an HTTP Resource using the `--http` flag in the generator: ```bash bin/rails generate avo:resource Author --http ``` ## Parsing Data from an Endpoint To wire an HTTP Resource to a data source, you must configure several attributes. Below is a breakdown of the supported options, each with an illustrative example. ```ruby # app/avo/resources/author.rb class Avo::Resources::Author < Avo::Core::Resources::Http # The base URL for your external API self.endpoint = "https://api.openalex.org/authors" # How to extract the list of records from the API response self.parse_collection = -> { raise Avo::HttpError.new response["message"] if response["error"].present? response["results"] } # How to extract a single record from the API response self.parse_record = -> { raise Avo::HttpError.new response["message"] if response["error"].present? response } # How to extract the total count of records (useful for pagination) self.parse_count = -> { response["meta"]["count"] } # Optional: custom method to find a record if the ID is encoded or non-standard self.find_record_method = -> { query.find Base64.decode64(id) } # Optional: redefines model behavior to obfuscate the ID via Base64 self.model_class_eval = -> { define_method :to_param do Base64.encode64(id) end } # Optional: custom headers to send with the request self.headers = { "Authorization" => "Bearer #{ENV.fetch("API_KEY")}" } def fields field :id, as: :id field :display_name field :cited_by_count, name: "Total citations" field :works_count, name: "Total works" end end ``` ### Response Option Reference Here's a brief reference for the main configuration options: | Option | Description | |---------------------|-----------------------------------------------------------------------------| | `parse_collection` | Proc that returns the array of records | | `parse_record` | Proc that returns a single record | | `parse_count` | Proc that returns the total number of records | | `model_class_eval` | Optional: proc to define extra model behavior, often used for `to_param` | All HTTP Resource response options accept a **proc** (i.e., a lambda or block). These procs are executed in a rich runtime context that gives you full access to the HTTP response and metadata around the request. Within this block, you gain access to all attributes of `Avo::ExecutionContext`, including: - `raw_response` β€” the raw `HTTParty::Response` response object - `response` β€” the parsed body from `raw_response` (`raw_response.parsed_response`) - `headers` β€” the headers from the response, available via `raw_response.headers` This contextual access empowers you to define your resource’s behavior with a high degree of precision. Whether you're extracting deeply nested structures or implementing nuanced error handling, the execution context provides all the necessary components to **structure your parsing logic with clarity and control**. ### Request Option Reference Here's a brief reference for the main request options: | Option | Description | |---------------------|-----------------------------------------------------------------------------| | `endpoint` | Base URL for the external API | | `headers` | Optional: custom headers to send with the request | All HTTP Resource request options accept a **proc** (i.e., a lambda or block). These procs are executed in a rich runtime context that gives you full access to all attributes of `Avo::ExecutionContext` ## Handling API Errors Gracefully When interacting with external APIs, it's important to handle error responses gracefully. Avo provides a custom exception, `Avo::HttpError`, for this exact purpose. You can raise this error within your parsing procs like so: ```ruby raise Avo::HttpError.new response["message"] if response["error"].present? ``` This signals to Avo that the API returned an error, and the HTTP controller will automatically **rescue** the exception and **display the message as a flash error in the UI**. This allows you to surface meaningful error feedback to users without breaking the experience or having to manually handle exceptions across the interface. This pattern ensures your integration remains **resilient** and **intuitive**, providing a seamless user experience even when interacting with unreliable or unpredictable external data sources. ## Controlling Create, Update, and Destroy Behavior By default, the HTTP Resource controller provides built-in methods to handle **creation**, **updates**, and **deletion** of records through your API client. These methods are designed to be flexible and easy to override when you need custom behavior. ### Default Implementation ```ruby class Avo::Core::Controllers::Http def save_record # Perform either a create or update request based on the current controller action response = @resource.client.send(action_name, @record) # Should return true if the operation succeeded, false otherwise response.success? end def destroy_model # Perform a DELETE request to remove the record via the external API @resource.client.delete(@record.id) end end ``` ### Customizing the Behavior If your external API requires additional parameters, or conditional logic, you can override these methods in your custom controller. - `save_record` should return a **boolean**, indicating whether the create or update operation was successful. - You can determine if the operation is a **create** or an **update** by inspecting the `action_name`, which will be `"create"` or `"update"` respectively. ### Example Override ```ruby # app/controllers/avo/authors_controller.rb class Avo::AuthorsController < Avo::Core::Controllers::Http def save_record auth_headers = { "Authorization" => "Bearer #{ENV.fetch("API_KEY")}" } if action_name == "create" response = MyCustomApi.post("/authors", body: @record.as_json, headers: auth_headers) else response = MyCustomApi.put("/authors/#{@record.id}", body: @record.as_json, headers: auth_headers) end response.status == 200 end end ``` This approach grants you complete control over how HTTP Resources interact with your external services, allowing seamless integration, even with APIs that have unconventional or highly specific requirements. --- # Fields Fields are the backbone of a `Resource`. Through fields you tell Avo what to fetch from the database and how to display it on the , , and views. Avo ships with various simple fields like `text`, `textarea`, `number`, `password`, `boolean`, `select`, and more complex ones like `markdown`, `key_value`, `trix`, `tags`, and `code`. ## Declaring fields You add fields to a resource through the `fields` method using the `field DATABASE_COLUMN, as: FIELD_TYPE, **FIELD_OPTIONS` notation. ```ruby def fields field :name, as: :text end ``` The `name` property is the column in the database where Avo looks for information or a property on your model. That will add a few fields in your new Avo app. On the and views, we'll get a new text column of that record's database value. Finally, on the and views, we will get a text input field that will display & update the `name` field on that model. ### Specific methods for each view The `fields` method in your resource is invoked whenever non-specific view methods are present. To specify fields for each view or a group of views, you can use the following methods: `index` view -> `index_fields`
`show` view -> `show_fields`
`edit` / `update` views -> `edit_fields`
`new` / `create` views -> `new_fields` You can also register fields for a specific group of views as follows: `index` / `show` views -> `display_fields`
`edit` / `update` / `new` / `create` views -> `form_fields` When specific view fields are defined, they take precedence over view group fields. If neither specific view fields nor view group fields are defined, the fields will be retrieved from the `fields` method. The below example use two custom helpers methods to organize the fields through `display_fields` and `form_fields` :::code-group ```ruby [display_fields] def display_fields base_fields tool_fields end ``` ```ruby [form_fields] def form_fields base_fields tool_fields tool Avo::ResourceTools::CityEditor, only_on: :forms end ``` ```ruby [tool_fields (helper method)] # Notice that even if those fields are hidden on the form, we still include them on `form_fields`. # This is because we want to be able to edit them using the tool. # When submitting the form, we need this fields declared on the resource in order to know how to process them and fill the record. def tool_fields with_options hide_on: :forms do field :name, as: :text, help: "The name of your city", filterable: true field :population, as: :number, filterable: true field :is_capital, as: :boolean, filterable: true field :features, as: :key_value field :image_url, as: :external_image field :tiny_description, as: :markdown field :status, as: :badge, enum: ::City.statuses end end ``` ```ruby [base_fields (helper method)] def base_fields field :id, as: :id field :coordinates, as: :location, stored_as: [:latitude, :longitude] field :city_center_area, as: :area, geometry: :polygon, mapkick_options: { style: "mapbox://styles/mapbox/satellite-v9", controls: true }, datapoint_options: { label: "Paris City Center", tooltip: "Bonjour mes amis!", color: "#009099" } field :description, as: :trix, attachment_key: :description_file, visible: -> { resource.params[:show_native_fields].blank? } field :metadata, as: :code, format_using: -> { if view.edit? JSON.generate(value) else value end }, update_using: -> do ActiveSupport::JSON.decode(value) end field :created_at, as: :date_time, filterable: true end ``` ::: :::warning In some scenarios fields require presence even if not visible In certain situations, fields must be present in your resource configuration, even if they are hidden from view. Consider the following example where `tool_fields` are included within `form_fields` despite being wrapped in a `with_options hide_on: :forms do ... end` block. For instance, when using `tool Avo::ResourceTools::CityEditor, only_on: :forms`, it will render the `features` field, which is of type `key_value`. When the form is submitted, Avo relies on the presence of the `features` field to determine its type and properly parse the submitted value. If you omit the declaration of `field :features, as: :key_value, hide_on: :forms`, Avo will be unable to update that specific database column. ::: ## Field conventions When we declare a field, we pinpoint the specific database row for that field. Usually, that's a snake case value. Each field has a label. Avo will convert the snake case name to a humanized version. In the following example, the `is_available` field will render the label as *Is available*. ```ruby field :is_available, as: :boolean ``` Field naming convention :::info If having the fields stacked one on top of another is not the right layout, try the resource-sidebar. ::: ### A more complex example ```ruby class Avo::Resources::User < Avo::BaseResource def fields field :id, as: :id field :first_name, as: :text field :last_name, as: :text field :email, as: :text field :active, as: :boolean field :cv, as: :file field :is_admin?, as: :boolean end end ``` The `fields` method is already hydrated with the `current_user`, `params`, `request`, `view_context`, and `context` variables so you can use them to conditionally show/hide fields ```ruby class Avo::Resources::User < Avo::BaseResource def fields field :id, as: :id field :first_name, as: :text field :last_name, as: :text field :email, as: :text field :is_admin?, as: :boolean field :active, as: :boolean if current_user.is_admin? field :cv, as: :file end end end ``` ## Field Types --- # Field options Avo fields are dynamic and can be configured using field options. There are quite a few **common field options** described on this page that will work with most fields (but some might not support them), and some **custom field options** that only some fields respond to that are described on each field page. ### Common field option example ```ruby # disabled will disable the field on the `Edit` view field :name, as: :text, disabled: true field :status, as: :select, disabled: true ``` ### Custom field option example ```ruby # options will set the dropdown options for a select field field :status, as: :select, options: %w[first second third] ``` ## Change field name To customize the label, you can use the `name` property to pick a different label. ```ruby field :is_available, as: :boolean, name: "Availability" ``` 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`). Version 3 introduces the `:display` option that is the opposite of `:forms`, referring to both, `:index` and `:show` Be aware that a few fields are designed to override those options (ex: the `id` field is hidden in and ). ```ruby field :body, as: :text, hide_on: [:index, :show] ``` Please read the detailed views page for more info. ## Field Visibility You might want to restrict some fields to be accessible only if a specific condition applies. For example, hide fields if the user is not an admin. You can use the `visible` block to do that. It can be a `boolean` or a lambda. Inside the lambda, we have access to the `context` object and the current `resource`. The `resource` has the current `record` object, too (`resource.record`). ```ruby field :is_featured, as: :boolean, visible: -> { context[:user].is_admin? } # show field based on the context object field :is_featured, as: :boolean, visible: -> { resource.name.include? 'user' } # show field based on the resource name field :is_featured, as: :boolean, visible: -> { resource.record.published_at.present? } # show field based on a record attribute ``` :::warning On form submissions, the `visible` block is evaluated in the `create` and `update` controller actions. That's why you have to check if the `resource.record` object is present before trying to use it. ::: ```ruby # `resource.record` is nil when submitting the form on resource creation field :name, as: :text, visible -> { resource.record.enabled? } # Do this instead field :name, as: :text, visible -> { resource.record&.enabled? } ``` ## Computed Fields You might need to show a field with a value you don't have in a database row. In that case, you may compute the value using a block that receives the `record` (the actual database record), the `resource` (the configured Avo resource), and the current `view`. With that information, you can compute what to show on the field in the and views. ```ruby field 'Has posts', as: :boolean do record.posts.present? rescue false end ``` :::info Computed fields are displayed only on the and views. ::: This example will display a boolean field with the value computed from your custom block. ## Fields Formatter Sometimes you will want to process the database value before showing it to the user. There are several ways to format fields. ### Common formatting options In all cases, you have access to a bunch of variables inside this block, all the defaults that `Avo::ExecutionContext` provides plus `value`, `record`, `resource`, `view` and `field`. ## Formatting with Rails helpers You can also format using Rails helpers like `number_to_currency` (note that `view_context` is used to access the helper): ```ruby field :price, as: :number, format_using: -> { view_context.number_to_currency(value) } ``` ## Parse value before update When it's necessary to parse information before storing it in the database, the `update_using` option proves to be useful. Inside the block you can access the raw `value` from the form, and the returned value will be saved in the database. ```ruby field :metadata, as: :code, update_using: -> do ActiveSupport::JSON.decode(value) end ``` ## Sortable fields One of the most common operations with database records is sorting the records by one of your fields. For that, Avo makes it easy using the `sortable` option. Add it to any field to make that column sortable in the view. ```ruby field :name, as: :text, sortable: true ``` Sortable fields **Related:** - Add an index on the `created_at` column ## Custom sortable block When using computed fields or `belongs_to` associations, you can't set `sortable: true` to that field because Avo doesn't know what to sort by. However, you can use a block to specify how the records should be sorted in those scenarios. ```ruby{4-7} class Avo::Resources::User < Avo::BaseResource field :is_writer, as: :text, sortable: -> { # Order by something else completely, just to make a test case that clearly and reliably does what we want. query.order(id: direction) }, hide_on: :edit do record.posts.to_a.size > 0 ? "yes" : "no" end end ``` The block receives the `query` and the `direction` in which the sorting should be made and must return back a `query`. In the example of a `Post` that `has_many` `Comment`s, you might want to order the posts by which one received a comment the latest. You can do that using this query. ::: code-group ```ruby{5} [app/avo/resources/post.rb] class Avo::Resources::Post < Avo::BaseResource field :last_commented_at, as: :date, sortable: -> { query.includes(:comments).order("comments.created_at #{direction}") } end ``` ```ruby{4-6} [app/models/post.rb] class Post < ApplicationRecord has_many :comments def last_commented_at comments.last&.created_at end end ``` ::: ## Placeholder Some fields support the `placeholder` option, which will be passed to the inputs on and views when they are empty. ```ruby field :name, as: :text, placeholder: 'John Doe' ``` Placeholder option ## Required To indicate that a field is mandatory, you can utilize the `required` option, which adds an asterisk to the field as a visual cue. Avo automatically examines each field to determine if the associated attribute requires a mandatory presence. If it does, Avo appends the asterisk to signify its mandatory status. It's important to note that this option is purely cosmetic and does not incorporate any validation logic into your model. You will need to manually include the validation logic yourself, such as (`validates :name, presence: true`). ```ruby field :name, as: :text, required: true ``` Required option You may use a block as well. It will be executed in the `Avo::ExecutionContext` and you will have access to the `view`, `record`, `params`, `context`, `view_context`, and `current_user`. ```ruby field :name, as: :text, required: -> { view == :new } # make the field required only on the new view and not on edit ``` ## Disabled When you need to prevent the user from editing a field, the `disabled` option will render it as `disabled` on and views and the value will not be passed to that record in the database. This prevents a bad actor to go into the DOM, enable that field, update it, and then submit it, updating the record. ```ruby field :name, as: :text, disabled: true ``` Disabled option ### Disabled as a block You may use a block as well. It will be executed in the `Avo::ExecutionContext` and you will have access to the `view`, `record`, `params`, `context`, `view_context`, and `current_user`. ```ruby field :id, as: :number, disabled: -> { view == :edit } # make the field disabled only on the new edit view ``` ## Readonly When you need to prevent the user from editing a field, the `readonly` option will render it as `disabled` on and views. This does not, however, prevent the user from enabling the field in the DOM and send an arbitrary value to the database. ```ruby field :name, as: :text, readonly: true ``` Readonly 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 ## Nullable When a user uses the **Save** button, Avo stores the value for each field in the database. However, there are cases where you may prefer to explicitly instruct Avo to store a `NULL` value in the database row when the field is empty. You do that by using the `nullable` option, which converts `nil` and empty values to `NULL`. You may also define which values should be interpreted as `NULL` using the `null_values` method. ```ruby # using default options field :updated_status, as: :status, failed_when: [:closed, :rejected, :failed], loading_when: [:loading, :running, :waiting], nullable: true # using custom null values field :body, as: :textarea, nullable: true, null_values: ['0', '', 'null', 'nil', nil] ``` ## Link to record Sometimes, on the view, you may want a field in the table to be a link to that resource so that you don't have to scroll to the right to click on the icon. You can use `link_to_record` to change a table cell to be a link to that record. ```ruby # for id field field :id, as: :id, link_to_record: true # for text field field :name, as: :text, link_to_record: true # for gravatar field field :email, as: :gravatar, link_to_record: true ``` 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 `html` option. ```ruby{2} class Avo::Resources::Project < Avo::BaseResource field :users_required, as: :number, html: {index: {wrapper: {classes: "text-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) #### `stacked` layout ## Global `stacked` layout You may also set all the fields to follow the `stacked` layout by changing the `field_wrapper_layout` initializer option from `:inline` (default) to `:stacked`. ```ruby Avo.configure do |config| config.field_wrapper_layout = :stacked end ``` Now, all fields will have the stacked layout throughout your app. ## Field options --- # Field Discovery `discover_columns` and `discover_associations` automatically detect and configure fields for your Avo resources based on your model's database structure. ```rb{6-7} # app/avo/resources/user.rb class Avo::Resources::User < Avo::BaseResource # ... def fields discover_columns discover_associations end end ```
## Options ## Examples ### Basic Discovery ```rb{6-7} # app/avo/resources/user.rb class Avo::Resources::User < Avo::BaseResource # ... def fields discover_columns discover_associations end end ``` ### Custom Field Options This will add the provided options to every discovered field or association. This is particularly useful when having duplicative configurations across many fields. ```rb{6-7} # app/avo/resources/post.rb class Avo::Resources::Post < Avo::BaseResource # ... def fields discover_columns help: "Automatically discovered fields" discover_associations searchable: false end end ``` ### Combining Manual and Discovered Fields ```rb{6,8-9,11} # app/avo/resources/project.rb class Avo::Resources::Project < Avo::BaseResource # ... def fields field :custom_field, as: :text discover_columns except: [:custom_field] discover_associations field :another_custom_field, as: :boolean end end ``` ## Automatic Type Mapping Field discovery maps database column types to Avo field types automatically. e.g. - `string` β†’ `:text` - `integer` β†’ `:number` - `float` β†’ `:number` - `datetime` β†’ `:datetime` - `boolean` β†’ `:boolean` - `json/jsonb` β†’ `:code` The full, up-to-date list can be found [here](https://github.com/avo-hq/avo/blob/main/lib/avo/mappings.rb) ## Association Discovery The following associations are automatically configured: - `belongs_to` β†’ `:belongs_to` - `has_one` β†’ `:has_one` - `has_many` β†’ `:has_many` - `has_one_attached` β†’ `:file` - `has_many_attached` β†’ `:files` - `has_rich_text` β†’ `:trix` - `acts-as-taggable-on :tags` β†’ `:tags` The full, up-to-date list can be found [here](https://github.com/avo-hq/avo/blob/main/lib/avo/mappings.rb) --- # Resource controllers In order to benefit from Rails' amazing REST architecture, Avo generates a controller alongside every resource. Generally speaking you don't need to touch those controllers. Everything just works out of the box with configurations added to the resource file. However, sometimes you might need more granular control about what is happening in the controller actions or their callbacks. In that scenario you may take over and override that behavior. ## Request-Response lifecycle Each interaction with the CRUD UI results in a request - response cycle. That cycle passes through the `BaseController`. Each auto-generated controller for your resource inherits from `ResourcesController`, which inherits from `BaseController`. ```ruby class Avo::CoursesController < Avo::ResourcesController end ``` In order to make your controllers more flexible, there are several overridable methods similar to how [devise](https://github.com/heartcombo/devise#controller-filters-and-helpers:~:text=You%20can%20also%20override%20after_sign_in_path_for%20and%20after_sign_out_path_for%20to%20customize%20your%20redirect%20hooks) overrides `after_sign_in_path_for` and `after_sign_out_path_for`. ## Create methods For the `create` method, you can modify the `after_create_path`, the messages, and the actions both on success or failure. ## Update methods For the `update` method, you can modify the `after_update_path`, the messages, and the actions both on success or failure. ## Destroy methods For the `destroy` method, you can modify the `after_destroy_path`, the messages, and the actions both on success or failure. --- # Record previews :::warning This section is a work in progress. ::: To use record previews add the `preview` field on your resource and add `show_on: :preview` to the fields you'd like to have visible on the preview popover. ```ruby{3,7,11,14} class Avo::Resources::Team < Avo::BaseResource def fields field :preview, as: :preview field :name, as: :text, sortable: true, show_on: :preview field :color, as: Avo::Fields::ColorPickerField, hide_on: :index, show_on: :preview field :description, as: :textarea, show_on: :preview end end ``` --- # Scopes :::warning This section is a work in progress. ::: Sometimes you might need to segment your data beyond just a few filters. You might have an `User` resource but you frequently need to see all the **Active users** or **Admin users**. You can use a filter for that or add a scope. ## Generating scopes ```bash bin/rails generate avo:scope admins ``` ```ruby # app/avo/scopes/admins.rb class Avo::Scopes::Admins < Avo::Advanced::Scopes::BaseScope self.name = "Admins" # Name displayed on the scopes bar self.description = "Admins only" # This is the tooltip value self.scope = :admins # valid scope on the model you're using it self.visible = -> { true } # control the visibility end # app/models/user.rb class User < ApplicationRecord scope :admins, -> { where role: :admin } # This is used in the scope file above end ``` ## Registering scopes Because scopes are re-utilizable, you must manually add that scope to a resource using the `scope` method inside the `scopes` method. ```ruby{4} # app/avo/resources/user.rb class Avo::Resources::User < Avo::BaseResource def scopes scope Avo::Scopes::Admins end end ``` ## Options ### Execution Context All options can be configured using static values or procs. The procs are executed using the Avo::ExecutionContext, which provides access to all default methods and attributes available in Avo's execution context. Each option has access to: - `query` - `resource` - `scope` - `scoped_query` (check below Performance Note) :::warning Performance Note Inside each proc, you can call `scoped_query`, but use it with caution as it executes the scope. If the scope takes a while to execute, this could impact performance. ::: --- --- --- --- ## Full example ```ruby # app/avo/scopes/even_id.rb class Avo::Scopes::EvenId < Avo::Advanced::Scopes::BaseScope # Please see the performance note above if you're using `scoped_query` self.name = -> { "Even (#{scoped_query.count})" } # This will compute the description based on the resource name self.description = -> { "Only #{resource.name.downcase.pluralize} that have an even ID" } # This will scope the query to only even IDs self.scope = -> { query.where("#{resource.model_key}.id % 2 = ?", "0") } # Only show this scope to admins self.visible = -> { current_user.admin? } end ``` --- # Records ordering A typical scenario is when you need to set your records into a specific order. Like re-ordering `Slide`s inside a `Carousel` or `MenuItem`s inside a `Menu`. The `ordering` class attribute is your friend for this. You can set four actions `higher`, `lower`, `to_top` or `to_bottom`, and the `display_inline` and `visible_on` options. The actions are simple lambda functions but coupled with your logic or an ordering gem, and they can be pretty powerful. ## Configuration I'll demonstrate the ordering feature using the `acts_as_list` gem. Install and configure the gem as instructed in the [tutorials](https://github.com/brendon/acts_as_list#example). Please ensure you [give all records position attribute values](https://github.com/brendon/acts_as_list#adding-acts_as_list-to-an-existing-model), so the gem works fine. Next, add the order actions like below. ```ruby class Avo::Resources::CourseLink < Avo::BaseResource self.ordering = { visible_on: :index, actions: { higher: -> { record.move_higher }, lower: -> { record.move_lower }, to_top: -> { record.move_to_top }, to_bottom: -> { record.move_to_bottom }, } } end ``` The `record` is the actual instantiated model. The `move_higher`, `move_lower`, `move_to_top`, and `move_to_bottom` methods are provided by `acts_as_list`. If you're not using that gem, you can add your logic inside to change the position of the record. The actions have access to `record`, `resource`, `options` (the `ordering` class attribute) and `params` (the `request` params). That configuration will generate a button with a popover containing the ordering buttons. 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 Avo::Resources::CourseLink < Avo::BaseResource self.ordering = { display_inline: true, visible_on: :index, actions: { higher: -> { record.move_higher }, lower: -> { record.move_lower }, to_top: -> { record.move_to_top }, to_bottom: -> { record.move_to_bottom }, } } end ``` Avo ordering ## Display the buttons in the `Index` view or association view A typical scenario is to have the order buttons on the view or a resource. That's the default value for the `visible_on` option. ```ruby{3} class Avo::Resources::CourseLink < Avo::BaseResource self.ordering = { visible_on: :index, } end ``` ## Display the button on a `has_many` association Another scenario is to order the records only in the scope of a parent record, like order the `MenuItems` for a `Menu`, or `Slides` for a `Slider`. So you wouldn't need to have the order buttons on the view but only in the association section (in a has many association). To control that, you can use the `visible_on` option and set it to `:association`. ```ruby{3} class Avo::Resources::CourseLink < Avo::BaseResource self.ordering = { visible_on: :association, } end ``` ### Possible values The possible values for the `visible_on` option are `:index`, `:association` or `[:index, :association]` for both views. ## Change the scope on the `Index` view Naturally, you'll want to apply the `order(position: :asc)` condition to your query. You may do that in two ways. 1. Add a `default_scope` to your model. If you're using this ordering scheme only in Avo, then, this is not the recommended way, because it will add that scope to all queries for that model and you probably don't want that. 2. Use the [`index_query`](https://docs.avohq.io/3.0/customization.html#custom-query-scopes) to alter the query in Avo. ```ruby{2-4} class Avo::Resources::CourseLink < Avo::BaseResource self.index_query = -> { query.order(position: :asc) } self.ordering = { display_inline: true, visible_on: :index, # :index or :association actions: { higher: -> { record.move_higher }, # has access to record, resource, options, params lower: -> { record.move_lower }, to_top: -> { record.move_to_top }, to_bottom: -> { record.move_to_bottom } } } end ``` ## Reorder using drag and drop Sometimes just picking up a record and dropping it in the position that you'd like it to be. That's exactly what this feature does. It's disabled by default but you can enable it by adding `drag_and_drop: true` and `insert_at` options to the `self.ordering` hash. ```ruby{5,11} self.ordering = { display_inline: true, visible_on: %i[index association], # :index or :association or both # position: -> { record.position }, drag_and_drop: true, actions: { higher: -> { record.move_higher }, # has access to record, resource, options, params lower: -> { record.move_lower }, to_top: -> { record.move_to_top }, to_bottom: -> { record.move_to_bottom }, insert_at: -> { record.insert_at position } } } ``` ### Custom `position` attribute Using the `position` option you can specify the record's `position` attribute. The default is `record.position`. ```ruby{4} self.ordering = { display_inline: true, visible_on: %i[index association], # :index or :association or both position: -> { record.position_in_list }, drag_and_drop: true, actions: { higher: -> { record.move_higher }, # has access to record, resource, options, params lower: -> { record.move_lower }, to_top: -> { record.move_to_top }, to_bottom: -> { record.move_to_bottom }, insert_at: -> { record.insert_at position } } } ``` ## Authorization If you're using the authorization feature please ensure you give the proper permissions using the `reorder?` method. ```ruby class CourseLinkPolicy < ApplicationPolicy def reorder? = edit? # or a custom permission def reorder? user.can_reorder_items? end # other policy methods end ``` --- # Discreet Information Sometimes you need to have some information available on the record page, but not necesarily front-and-center. This is where the Discreet Information option is handy. ```ruby # app/avo/resources/post.rb class Avo::Resources::Post < Avo::BaseResource self.discreet_information = [ :timestamps, { tooltip: -> { sanitize("Product is #{record.published_at ? "published" : "draft"}", tags: %w[strong]) }, icon: -> { "heroicons/outline/#{record.published_at ? "eye" : "eye-slash"}" } }, { label: -> { record.published_at ? "πŸš€" : "😬" }, url: -> { "https://avohq.io" }, url_target: :_blank } ] end ``` ## Display the `id` To save field space, you can use the discreet information area to display the id of the current record. Set the option to the `:id` value and the id will be added next to the title. ```ruby # app/avo/resources/post.rb class Avo::Resources::Post < Avo::BaseResource self.discreet_information = :id # fields and other resource configuration end ``` You can alternatively use `:id_badge` to display the id as a badge. ## Display the `created_at` and `updated_at` timestamps The reason why we built this feature was that we wanted a place to display the created and updated at timestamps but didn't want to use up a whole field for it. That's why this is the most simple thing to add. Set the option to the `:timestamps` value and a new icon will be added next to the title. When the user hovers over the icon, they will see the record's default timestamps. ```ruby # app/avo/resources/post.rb class Avo::Resources::Post < Avo::BaseResource self.discreet_information = :timestamps # fields and other resource configuration end ``` If the record doesn't have the `created_at` or `updated_at` attributes, they will be ommited. You can alternatively use `:timestamps_badge` to display the timestamps as a badge. ## Options You may fully customize the discreet information item by taking control of different options. To do that, you can set it to a `Hash` with various keys. ```ruby # app/avo/resources/post.rb class Avo::Resources::Post < Avo::BaseResource self.discreet_information = { tooltip: -> { "Product is #{record.published_at ? "published" : "draft"}" }, icon: -> { "heroicons/outline/#{record.published_at ? "eye" : "eye-slash"}" } url: -> { main_app.post_path record } } end ``` ## Display multiple pieces of information You can use it to display one or more pieces of information. ## Information properties Each piece of information has a fe ## Full configuration ```ruby # app/avo/resources/post.rb class Avo::Resources::Post < Avo::BaseResource self.discreet_information = [ :timestamps, { tooltip: -> { sanitize("Product is #{record.published_at ? "published" : "draft"}", tags: %w[strong]) }, icon: -> { "heroicons/outline/#{record.published_at ? "eye" : "eye-slash"}" } }, { label: -> { record.published_at ? "βœ…" : "πŸ™„" }, url: -> { "https://avohq.io" }, url_target: :_blank } ] # fields and other resource configuration end ``` --- # Customizable controls One of the things that we wanted to support from day one is customizable controls on resource pages, and now, Avo supports customizable controls on , , and views and for the table row. ## Default controls By default, Avo displays a few buttons (controls) for the user to use on the , , and views which you can override using the appropriate resource options. ## Customize the controls You can take over and customize them all using the `index_controls`, `show_controls`, `edit_controls`, and `row_controls` class attributes. ## Controls A control is an item that you can place in a designated area. They can be one of the default ones like `back_button`, `delete_button`, or `edit_button` to custom ones like `link_to` or `action`. You may use the following controls: :::warning WARNING (**NOT** applicable for versions greater than ) When you use the `action` helper in any customizable block it will act only as a shortcut to display the action button, it will not also register it to the resource. You must manually register it with the `action` declaration. ```ruby{6-8,13-15} class Avo::Resources::Fish < Avo::BaseResource self.title = :name self.show_controls = -> do # In order to use it here action Avo::Actions::ReleaseFish, style: :primary, color: :fuchsia, arguments: { action_on_show_controls: "Will use this arguments" } end # πŸ‘‡ Also declare it here πŸ‘‡ def actions action Avo::Actions::ReleaseFish, arguments: { action_from_list: "Will use this arguments" } end end ``` ::: ## Control Options Some controls take options. Not all controls take all options. Example: The `link_to` control is the only one that will take the `target` option, but most other controls use the `class` option. ## Default values If you're curious what are the default controls Avo adds for each block, here they are: ```ruby # show controls back_button delete_button detach_button actions_list edit_button # form (edit & new) controls back_button delete_button actions_list save_button # index controls attach_button actions_list create_button # row controls order_controls show_button edit_button detach_button delete_button ``` ## Conditionally hiding/showing actions Actions have the `visible` block where you can control the visibility of an action. In the context of `show_controls` that block is not taken into account, but you can use regular `if`/`else` statements because the action declaration is wrapped in a block. ```ruby{6-8} class Avo::Resources::Fish < Avo::BaseResource self.show_controls = -> do back_button label: "", title: "Go back now" # visibility conditional if record.something? action Avo::Actions::ReleaseFish, style: :primary, color: :fuchsia, icon: "heroicons/outline/globe" end edit_button label: "" end end ``` --- # Cover and Profile photos Cover and Profile Photos It's common to want to display the information in different ways than just "key" and "value". That's why Avo has rich fields like `key_value`, `trix`, `tip_tap`, `files`, and more. Avo now also has the Cover and Profile photo areas where you can customize the experience even more. The APIs used are pretty similar and easy to use. ## Profile photo The `profile_photo` option takes two arguments: `visible_on` and `source`. ```ruby self.profile_photo = { source: -> { if view.index? # We're on the index page and don't have a record to reference DEFAULT_IMAGE else # We have a record so we can reference it's profile_photo record.profile_photo end } } ``` ## Cover photo The `cover_photo` option takes three arguments: `size`, `visible_on`, and `source`. ```ruby self.cover_photo = { size: :md, # :sm, :md, :lg visible_on: [:show, :forms], # can be :show, :index, :edit, or a combination [:show, :index] source: -> { if view.index? # We're on the index page and don't have a record to reference DEFAULT_IMAGE else # We have a record so we can reference it's cover_photo record.cover_photo end } } ``` --- # Array The `Array` field in allows you to display and manage structured array data. This field supports flexibility in fetching and rendering data, making it suitable for various use cases. :::tip Important To use the `Array` field, you must create a resource specifically for it. Refer to the Array Resource documentation for detailed instructions. For example, to use `field :attendees, as: :array`, you can generate an array resource by running the following command: ```bash rails generate avo:resource Attendee --array ``` This step ensures the proper setup of your array field within the Avo framework. ::: ### Example 1: Array field with a block You can define array data directly within a block. This is useful for static or pre-configured data: ```ruby{3-8} class Avo::Resources::Course < Avo::BaseResource def fields field :attendees, as: :array do [ { id: 1, name: "John Doe", role: "Software Developer", organization: "TechCorp" }, { id: 2, name: "Jane Smith", role: "Data Scientist", organization: "DataPros" } ] end end end ``` :::warning Authorization The `array` field internally inherits many behaviors from `has_many`, including authorization. If you are using authorization and the array field is not rendering, it is most likely not authorized. To explicitly authorize it, define the following method in the resource's policy: ```ruby{3} # app/policies/course_policy.rb class CoursePolicy < ApplicationPolicy def view_attendees? = true end ``` For more details, refer to the view_{association}? documentation. ::: ### Example 2: Array field fetching data from the model's method If no block is defined, Avo will attempt to fetch data by calling the corresponding method on the model: ```ruby class Course < ApplicationRecord def attendees User.all.first(6) # Example fetching first 6 users end end ``` Here, the `attendees` field will use the `attendees` method from the `Course` model to render its data dynamically. ### Example 3: Fallback to the `records` method If neither the block nor the model's method exists, Avo will fall back to the `records` method defined in the resource used to render the array field. This is useful for providing a default dataset. When neither a block nor a model's method is defined, Avo will fall back to the `records` method in the resource used to render the field. This is a handy fallback for providing default datasets: ```ruby class Avo::Resources::Attendee < Avo::Resources::ArrayResource def records [ { id: 1, name: "Default Attendee", role: "Guest", organization: "DefaultOrg" } ] end end ``` ## Summary of Data Fetching Hierarchy When using `has_many` with `array: true`, Avo will fetch data in the following order: 1. Use data returned by the **block** provided in the field. 2. Fetch data from the **associated model method** (e.g., `Course#attendees`). 3. Fall back to the **`records` method** defined in the resource. This hierarchy provides maximum flexibility and ensures seamless integration with both dynamic and predefined datasets. --- # Badge The `Badge` field is used to display an easily recognizable status of a record. 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. ### DB payload example An example of a boolean group object stored in the database: ```ruby { "admin": true, "manager": true, "writer": true, } ``` ### Field declaration example Below is an example of declaring a `boolean_group` field for roles that matches the DB value from the example above: ```ruby field :roles, as: :boolean_group, name: "User roles", options: { admin: "Administrator", manager: "Manager", writer: "Writer" } ``` ## Updates Before version Avo would override the whole attribute with only the payload sent from the client. ```json // Before update. { "feature_enabled": true, "another_feature_enabled": false, "something_else": "some_value" // this will disappear } // After update. { "feature_enabled": true, "another_feature_enabled": false, } ``` will only update the keys that you send from the client. ```json // Before update. { "feature_enabled": true, "another_feature_enabled": false, "something_else": "some_value" // this will be kept } // After update. { "feature_enabled": true, "another_feature_enabled": false, "something_else": "some_value" } ``` --- # Code 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`. ::: --- # EasyMDE :::info Before Avo 3.17 this field was called `markdown`. It was renamed to `easy_mde` so we can add our own implementation with `markdown`. ::: Trix field The `easy_mde` field renders a [EasyMDE Markdown Editor](https://github.com/Ionaru/easy-markdown-editor) and is associated with a text or textarea column in the database. `easy_mde` field converts text within the editor into raw Markdown text and stores it back in the database. ```ruby field :description, as: :easy_mde ``` :::info The `easy_mde` field is hidden from the **Index** view. ::: ## Options --- # External image You may have a field in the database that has the URL to an image, and you want to display that in Avo. That is where the `ExternalImage` field comes in to help. It will take that value, insert it into an `image_tag`, and display it on the `Index` and `Show` views. ```ruby field :logo, as: :external_image ``` ## Options All options can be static values or procs that are executed within Avo's execution context. When using procs, you have access to all the defaults that `Avo::ExecutionContext` provides plus: - `record` - `resource` - `view` - `field` ## Conditional sizing based on view You can use procs to set different image dimensions and styling based on the current view: ```ruby field :logo, as: :external_image, width: -> { view.index? ? 40 : 150 }, height: -> { view.index? ? 40 : 150 }, radius: -> { view.index? ? 4 : 12 } ``` This example will display smaller, slightly rounded images on the index view (40x40px with 4px radius) and larger, more rounded images on the show view (150x150px with 12px radius). ## Use computed values Another common scenario is to use a value from your database and create a new URL using a computed value. ```ruby field :logo, as: :external_image do "//logo.clearbit.com/#{URI.parse(record.url).host}?size=180" rescue nil end ``` ## Use in the Grid `cover` position Another common place you could use it is in the grid `:cover` position. ```ruby cover :logo, as: :external_image, link_to_record: true do "//logo.clearbit.com/#{URI.parse(record.url).host}?size=180" rescue nil end ``` --- # File :::warning You must manually require `activestorage` and `image_processing` gems in your `Gemfile`. ```ruby # Active Storage makes it simple to upload and reference files gem "activestorage" # High-level image processing wrapper for libvips and ImageMagick/GraphicsMagick gem "image_processing" ``` ::: The `File` field is the fastest way to implement file uploads in a Ruby on Rails app using [Active Storage](https://edgeguides.rubyonrails.org/active_storage_overview.html). Avo will use your application's Active Storage settings with any supported [disk services](https://edgeguides.rubyonrails.org/active_storage_overview.html#disk-service). ```ruby field :avatar, as: :file, is_image: true ``` ## Authorization :::info Please ensure you have the `upload_{FIELD_ID}?`, `delete_{FIELD_ID}?`, and `download_{FIELD_ID}?` methods set on your model's **Pundit** policy. Otherwise, the input and download/delete buttons will be hidden. ::: **Related:** - Attachment pundit policies ## Variants When using the `file` field to display an image, you can opt to show a processed variant of that image. This can be achieved using the `format_using` option. ### Example: ```ruby{3-5} field :photo, as: :file, format_using: -> { value.variant(resize_to_limit: [150, 150]).processed.image } ``` ## Options --- # Files :::warning You must manually require `activestorage` and `image_processing` gems in your `Gemfile`. ```ruby # Active Storage makes it simple to upload and reference files gem "activestorage" # High-level image processing wrapper for libvips and ImageMagick/GraphicsMagick gem "image_processing" ``` ::: The `Files` field is similar to `File` and enables you to upload multiple files at once using the same easy-to-use [Active Storage](https://edgeguides.rubyonrails.org/active_storage_overview.html) implementation. ```ruby field :documents, as: :files ``` ## Options ## Authorization :::info Please ensure you have the `upload_{FIELD_ID}?`, `delete_{FIELD_ID}?`, and `download_{FIELD_ID}?` methods set on your model's **Pundit** policy. Otherwise, the input and download/delete buttons will be hidden. ::: **Related:** - Attachment pundit policies --- # Gravatar The `Gravatar` field turns an email field from the database into an avatar image if it's found in the [Gravatar](https://en.gravatar.com/site/implement/images/) database. ```ruby field :email, as: :gravatar, rounded: false, size: 60, default_url: 'some image url' ``` ## Options ## Using computed values You may also pass in a computed value. ```ruby field :email, as: :gravatar do "#{record.google_username}@gmail.com" end ``` --- # Heading :::code-group ```ruby [Field id] field :user_information, as: :heading ``` ```ruby [Label] field :some_id, as: :heading, label: "user information" ``` ```ruby [Computed] field :some_id, as: :heading do "user information" end ``` ::: 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. :::warning Computed heading The computed fields are not rendered on form views, same with heading field, if computed syntax is used it will not be rendered on the form views. Use `label` in order to render it on **all** views. ::: ## Options --- # Hidden There are scenarios where in order to be able to submit a form, an input should be present but inaccessible to the user. An example of this might be where you want to set a field by default without the option to change, or see it. `Hidden` will render a `` element on the `Edit` and `New` page. > Hidden will only render on the `Edit` and `New` views. ### Example usage: ```ruby # Basic field :group_id, as: :hidden # With default field :user_id, as: :hidden, default: -> { current_user.id } # If the current_user is a admin # 1. Allow them to see and select a user. # 2. Remove the user_id field to prevent user_id it from overriding the user selection. # Otherwise set the user_id to the current user and hide the field. field :user, as: :belongs_to, visible: -> { context[:current_user].admin? } field :user_id, as: :hidden, default: -> { current_user.id }, visible: -> { !context[:current_user].admin? } ``` --- # ID The `id` field is used to show the record's id. By default, it's visible only on the `Index` and `Show` views. That is a good field to add the `link_to_record` option to make it a shortcut to the record `Show` page. ```ruby field :id, as: :id ``` ## Options --- # KeyValue KeyValue field The `KeyValue` field makes it easy to edit flat key-value pairs stored in `JSON` format in the database. ```ruby field :meta, as: :key_value ``` ## Options ## Customizing the labels You can easily customize the labels displayed in the UI by mentioning custom values in `key_label`, `value_label`, `action_text`, and `delete_text` properties when defining the field. ```ruby field :meta, # The database field ID as: :key_value, # The field type. key_label: "Meta key", # Custom value for key header. Defaults to 'Key'. value_label: "Meta value", # Custom value for value header. Defaults to 'Value'. action_text: "New item", # Custom value for button to add a row. Defaults to 'Add'. delete_text: "Remove item" # Custom value for button to delete a row. Defaults to 'Delete'. ``` ## Enforce restrictions You can enforce some restrictions by removing the ability to edit the field's key or value by setting `disable_editing_keys` or `disable_editing_values` to `true` respectively. If `disable_editing_keys` is set to `true`, be aware that this option will also disable adding rows as well. You can separately remove the ability to add a new row by setting `disable_adding_rows` to `true`. Deletion of rows can be enforced by setting `disable_deleting_rows` to `true`. ```ruby field :meta, # The database field ID as: :key_value, # The field type. disable_editing_keys: false, # Option to disable the ability to edit keys. Implies disabling to add rows. Defaults to false. disable_editing_values: false, # Option to disable the ability to edit values. Defaults to false. disable_adding_rows: false, # Option to disable the ability to add rows. Defaults to false. disable_deleting_rows: false # Option to disable the ability to delete rows. Defaults to false. ``` Setting `disabled: true` enforces all restrictions by disabling editing keys, editing values, adding rows, and deleting rows collectively. ```ruby field :meta, # The database field ID as: :key_value, # The field type. disabled: true, # Option to disable editing keys, editing values, adding rows, and deleting rows. Defaults to false. ``` `KeyValue` is hidden on the `Index` view. --- # Location The `Location` field is used to display a point on a map. ```ruby field :coordinates, as: :location ``` Location 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 location field is attached to one database column that has the coordinates in plain text with a comma `,` joining them (`latitude,longitude`). Ex: `44.427946,26.102451` Avo will take that value, split it by the comma and use the first element as the `latitude` and the second one as the `longitude`. On the view you'll get in interactive map and on the edit you'll get one field where you can edit the coordinates. ## Options --- # Markdown Markdown field :::info In Avo 3.17 we renamed the `markdown` field `easy_mde` and introduced this custom one based on the [Marksmith editor](https://github.com/avo-hq/marksmith). Please read the docs on the repo for more information on how it works. ::: This field is inspired by the wonderful GitHub editor we all love and use. It supports applying styles to the markup, dropping files in the editor, and using the Media Library. The uploaded files will be taken over by Rails and persisted using Active Storage. ```ruby field :body, as: :markdown ``` :::warning Please ensure you have these gems in your `Gemfile`. ```ruby gem "marksmith" gem "commonmarker" ``` :::
## Supported features - [x] ActiveStorage file attachments - [x] Media Library integration - [x] Preview panel - [x] [Ready-to-use renderer](https://github.com/avo-hq/marksmith#built-in-preview-renderer) - [x] Text formatting - [x] Lists - [x] Links - [x] Images - [x] Tables - [x] Code blocks - [x] Headings ## Customize the renderer There are two places where we parse the markdown into the HTML you see. 1. In the controller 2. In the field component You may customize the renderer by overriding the model. ```ruby # app/models/marksmith/renderer.rb module Marksmith class Renderer def initialize(body:) @body = body end def render if Marksmith.configuration.parser == "commonmarker" render_commonmarker elsif Marksmith.configuration.parser == "kramdown" render_kramdown else render_redcarpet end end def render_commonmarker # commonmarker expects an utf-8 encoded string body = @body.to_s.dup.force_encoding("utf-8") Commonmarker.to_html(body) end def render_redcarpet ::Redcarpet::Markdown.new( ::Redcarpet::Render::HTML, tables: true, lax_spacing: true, fenced_code_blocks: true, space_after_headers: true, hard_wrap: true, autolink: true, strikethrough: true, underline: true, highlight: true, quote: true, with_toc_data: true ).render(@body) end def render_kramdown body = @body.to_s.dup.force_encoding("utf-8") Kramdown::Document.new(body).to_html end end end ``` --- # Money The `Money` field is used to display a monetary value. ```ruby field :price, as: :money, currencies: %w[EUR USD RON PEN] ``` ## Money Field Example You can explore the implementation of the money field in [avodemo](https://main.avodemo.com/avo/resources/products/new) and it's corresponding code on GitHub [here](https://github.com/avo-hq/main.avodemo.com/blob/main/app/avo/resources/product.rb) ### Example on new ### Example on show with currencies USD ### Example on show with currencies RON ### Example on index ## Installation This field is a standalone gem. You have to add it to your `Gemfile` alongside the `money-rails` gem. :::info Add this field to the `Gemfile` ```ruby # Gemfile gem "avo-money_field" gem "money-rails", "~> 1.12" ``` ::: :::warning Important: Monetization Requirement In order to fully utilize the money field's features, you must monetize the associated attribute at the model level using the `monetize` method from the `money-rails` gem. ([Usage example](https://github.com/RubyMoney/money-rails?tab=readme-ov-file#usage-example)) For example: ```ruby monetize :price_cents ``` Without this step, the money field may not behave as expected, and the field might not render. ::: ## Options --- # Number The `number` field renders a `input[type="number"]` element. ```ruby field :age, as: :number ``` ## Options ## Examples ```ruby field :age, as: :number, min: 0, max: 120, step: 5 ``` --- # Password The `Password` field renders a `input[type="password"]` element for that field. By default, it's visible only on the `Edit` and `New` views. ```ruby field :password, as: :password ``` #### Revealable You can set the `revealable` to true to show an "eye" icon that toggles the password between hidden or visible. **Related:** - Devise password optional --- # Preview The `Preview` field adds a tiny icon to each row on the view that, when hovered, it will display a preview popup with more information regarding that record. ```ruby field :preview, as: :preview ``` ## Define the fields The fields shown in the preview popup are configured similarly to how you configure the visibility in the different views. When you want to display a field in the preview popup simply call the `show_on :preview` option on the field. ```ruby field :name, as: :text, show_on :preview ``` ## Authorization Since version the preview request authorization is controller with the `preview?` policy method. --- # Progress bar The `ProgressBar` field renders a `progress` element on `Index` and `Show` views and and a `input[type=range]` element on `Edit` and `New` views. ```ruby field :progress, as: :progress_bar ``` Progress bar custom field on index ## Options ## Examples ```ruby field :progress, as: :progress_bar, max: 150, step: 10, display_value: true, value_suffix: "%" ``` Progress bar custom field edit --- # Radio Radio field The `Radio` field is used to render radio buttons. It's useful when only one value can be selected in a given options group. ### Field declaration example Below is an example of declaring a `radio` field for a role: ```ruby field :role, as: :radio, name: "User role", options: { admin: "Administrator", manager: "Manager", writer: "Writer" } ``` --- # Record link Sometimes you just need to link to a field. That's it! This is what this field does. You give it a record and it will link to it. That record can come off an association a method or any kind of property on the record instance. :::info Add this field to the `Gemfile` ```ruby # Gemfile gem "avo-record_link_field" ``` ::: :::warning That record you're pointing to should have a resource configured. ::: ```ruby{14,19} class Comment < ApplicationRecord # Your model must return an instance of a record has_one :post # or belongs_to :post # or def post # trivially find a post Post.find 42 end end # Calling the method like so will give us an instance of a Post Comment.first.post => # class Avo::Resources::Comment < Avo::BaseResource def fields # This will run `record.post` and try to display whatever is returned. field :post, as: :record_link end end ``` Record link field ## Options Besides some of the default options, there are a few custom ones. ## Using computed values Of course you can take full control of this field and use your computed values too. In order to do that, open a block and run some ruby query to return an instance of a record. #### Example ```ruby field :post, as: :record_link do # This will generate a link similar to this # https://example.com/avo/resources/posts/42 Post.find 42 end # or field :creator, as: :record_link, add_via_params: false do user_id = SomeService.new(comment: record).fetch_user_id # returns 31 # This will generate a link similar to this # https://example.com/avo/resources/users/31 User.find user_id end # or field :creator, as: :record_link, use_resource: "AdminUser", add_via_params: false do user_id = SomeService.new(comment: record).fetch_user_id # returns 31 # This will generate a link similar to this # https://example.com/avo/resources/admin_users/31 User.find user_id end ``` --- # Rhino Rhino field The wonderful [Rhino Editor](https://rhino-editor.vercel.app/) built by [Konnor Rogers](https://www.konnorrogers.com/) is available and fully integrated with Avo. ```ruby field :body, as: :rhino ``` :::info Add this line to your application's `Gemfile`: ```ruby gem "avo-rhino_field" ``` ::: Rhino is based on [TipTap](https://tiptap.dev/) which is a powerful and flexible WYSIWYG editor. It supports [ActiveStorage](https://guides.rubyonrails.org/active_storage_overview.html) file attachments, [ActionText](https://guides.rubyonrails.org/action_text_overview.html), and seamlessly integrates with the Media Library. ## Options ## Contributing You can contribute to the Rhino editor by visiting the [GitHub repository](https://github.com/avo-hq/avo-rhino_field). --- # Select The `Select` field renders a `select` field. ```ruby field :type, as: :select, options: { 'Large container': :large, 'Medium container': :medium, 'Tiny container': :tiny }, display_value: true, placeholder: 'Choose the type of the container.' ``` ## Customization You may customize the `Text` field with as many options as you need. ```ruby field :title, # The database field ID as: :text, # The field type name: 'Post title', # The label you want displayed required: true, # Display it as required readonly: true, # Display it disabled as_html: true # Should the output be parsed as html placeholder: 'My shiny new post', # Update the placeholder text format_using: -> { value.truncate 3 } # Format the output ``` --- # Textarea The `textarea` field renders a `