From f3d0b1b811b8e430caf5e22926d34e1a5cdaea7c Mon Sep 17 00:00:00 2001 From: Adrian Marin Date: Tue, 1 Apr 2025 16:42:58 +0300 Subject: [PATCH 1/2] enhancement: initial LLM rules --- .cursor/rules/avo.mdc | 37 +++++++ .cursor/rules/execution-context.mdc | 123 ++++++++++++++++++++++ .cursor/rules/ruby-general.mdc | 66 ++++++++++++ lib/generators/avo/rules_generator.rb | 15 +++ lib/generators/avo/templates/rules/avo.tt | 26 +++++ 5 files changed, 267 insertions(+) create mode 100644 .cursor/rules/avo.mdc create mode 100644 .cursor/rules/execution-context.mdc create mode 100644 .cursor/rules/ruby-general.mdc create mode 100644 lib/generators/avo/rules_generator.rb create mode 100644 lib/generators/avo/templates/rules/avo.tt diff --git a/.cursor/rules/avo.mdc b/.cursor/rules/avo.mdc new file mode 100644 index 0000000000..972e294703 --- /dev/null +++ b/.cursor/rules/avo.mdc @@ -0,0 +1,37 @@ +--- +description: Descriptor +globs: +alwaysApply: true +--- +# Resources + +The Avo CRUD functionality uses the concept of a resource. A resource belongs to a model and a model may have multiple resources. +The model is how Rails talks to the database and the resource is how Avo talks to Rails and knows how to fetch and manipulate the records. + +Each resource is a class defined in a file. They inherit the `Avo::BaseResource` class which inherits `Avo::Resources::Base`. `Avo::BaseResource` is empty so the user can override anything they want on a global level in theyr own app. + +A resource has a multitude of options which are usually declared using the `self.OPTION_NAME = ` format. They can take a simple value like a string, boolean, symbol, hash or array or they can take an `ExecutionContext` which will give the developer much more control over what they can return from it. + +## Fields + +Fields are declared using the `field` method inside a `fields` method in a resource like so: + +```ruby +class Avo::Resources::Team < Avo::BaseResource + def fields + field :id, as: :id + field :name, as: :text + field :description, as: :textarea + end +end +``` + +All fields have a name which is the first argument and the type of the field, as the `as:` argument. + +Most fields support optons that are universal for all fields. Some examples are: readonly, disabled, help, format_using, update using, hide_on, show_on, and others. + +Some fields have their own proprietary options, like `rows` for `textarea` fields and `options` for `select` fields. + +All field options: + + diff --git a/.cursor/rules/execution-context.mdc b/.cursor/rules/execution-context.mdc new file mode 100644 index 0000000000..d813032bd9 --- /dev/null +++ b/.cursor/rules/execution-context.mdc @@ -0,0 +1,123 @@ +--- +description: Descriptor +globs: +alwaysApply: true +--- +# Execution context + +Avo enables developers to hook into different points of the application lifecycle using blocks. +That functionality can't always be performed in void but requires some pieces of state to set up some context. + +Computed fields are one example. + +```ruby +field :full_name, as: :text do + "#{record.first_name} #{record.last_name}" +end +``` + +In that block we need to pass the `record` so you can compile that value. We send more information than just the `record`, we pass on the `resource`, `view`, `view_context`, `request`, `current_user` and more depending on the block that's being run. + +## How does the `ExecutionContext` work? + +The `ExecutionContext` is an object that holds some pieces of state on which we execute a lambda function. + +```ruby +module Avo + class ExecutionContext + + attr_accessor :target, :context, :params, :view_context, :current_user, :request + + def initialize(**args) + # If target don't respond to call, handle will return target + # In that case we don't need to initialize the others attr_accessors + return unless (@target = args[:target]).respond_to? :call + + args.except(:target).each do |key,value| + singleton_class.class_eval { attr_accessor "#{key}" } + instance_variable_set("@#{key}", value) + end + + # Set defaults on not initialized accessors + @context ||= Avo::Current.context + @params ||= Avo::Current.params + @view_context ||= Avo::Current.view_context + @current_user ||= Avo::Current.current_user + @request ||= Avo::Current.request + end + + delegate :authorize, to: Avo::Services::AuthorizationService + + # Return target if target is not callable, otherwise, execute target on this instance context + def handle + target.respond_to?(:call) ? instance_exec(&target) : target + end + end +end + +# Use it like so. +SOME_BLOCK = -> { + "#{record.first_name} #{record.last_name}" +} + +Avo::ExecutionContext.new(target: &SOME_BLOCK, record: User.first).handle +``` + +This means you could throw any type of object at it and it it responds to a `call` method wil will be called with all those objects. + + + + + + + + + + + + + + + + diff --git a/.cursor/rules/ruby-general.mdc b/.cursor/rules/ruby-general.mdc new file mode 100644 index 0000000000..30ba566c8a --- /dev/null +++ b/.cursor/rules/ruby-general.mdc @@ -0,0 +1,66 @@ +--- +description: +globs: +alwaysApply: true +--- + + You are an expert in Ruby on Rails, PostgreSQL, Hotwire (Turbo and Stimulus), and Tailwind CSS. + + Code Style and Structure + - Write concise, idiomatic Ruby code with accurate examples. + - Follow Rails conventions and best practices. + - Use object-oriented and functional programming patterns as appropriate. + - Prefer iteration and modularization over code duplication. + - Use descriptive variable and method names (e.g., user_signed_in?, calculate_total). + - Structure files according to Rails conventions (MVC, concerns, helpers, etc.). + + Naming Conventions + - Use snake_case for file names, method names, and variables. + - Use CamelCase for class and module names. + - Follow Rails naming conventions for models, controllers, and views. + + Ruby and Rails Usage + - Use Ruby 3.x features when appropriate (e.g., pattern matching, endless methods). + - Leverage Rails' built-in helpers and methods. + - Use ActiveRecord effectively for database operations. + + Syntax and Formatting + - Follow the Ruby Style Guide (https://rubystyle.guide/) + - Use Ruby's expressive syntax (e.g., unless, ||=, &.) + - Prefer single quotes for strings unless interpolation is needed. + + Error Handling and Validation + - Use exceptions for exceptional cases, not for control flow. + - Implement proper error logging and user-friendly messages. + - Use ActiveModel validations in models. + - Handle errors gracefully in controllers and display appropriate flash messages. + + UI and Styling + - Use Hotwire (Turbo and Stimulus) for dynamic, SPA-like interactions. + - Implement responsive design with Tailwind CSS. + - Use Rails view helpers and partials to keep views DRY. + + Performance Optimization + - Use database indexing effectively. + - Implement caching strategies (fragment caching, Russian Doll caching). + - Use eager loading to avoid N+1 queries. + - Optimize database queries using includes, joins, or select. + + Key Conventions + - Follow RESTful routing conventions. + - Use concerns for shared behavior across models or controllers. + - Implement service objects for complex business logic. + - Use background jobs (e.g., Sidekiq) for time-consuming tasks. + + Testing + - Write comprehensive tests using RSpec or Minitest. + - Follow TDD/BDD practices. + - Use factories (FactoryBot) for test data generation. + + Security + - Implement proper authentication and authorization (e.g., Devise, Pundit). + - Use strong parameters in controllers. + - Protect against common web vulnerabilities (XSS, CSRF, SQL injection). + + Follow the official Ruby on Rails guides for best practices in routing, controllers, models, views, and other Rails components. + \ No newline at end of file diff --git a/lib/generators/avo/rules_generator.rb b/lib/generators/avo/rules_generator.rb new file mode 100644 index 0000000000..f7d09bc20b --- /dev/null +++ b/lib/generators/avo/rules_generator.rb @@ -0,0 +1,15 @@ +require_relative "base_generator" + +module Generators + module Avo + class RulesGenerator < BaseGenerator + source_root File.expand_path("templates", __dir__) + + namespace "avo:rules" + + def create_resource_file + template "rules/avo.tt", ".cursor/rules/avo.mdc" + end + end + end +end diff --git a/lib/generators/avo/templates/rules/avo.tt b/lib/generators/avo/templates/rules/avo.tt new file mode 100644 index 0000000000..cbe008c0c5 --- /dev/null +++ b/lib/generators/avo/templates/rules/avo.tt @@ -0,0 +1,26 @@ +# Resources + +Each resource is defined in a file. + +## Fields + +Fields are declared using the `field` method inside a `fields` method in a resource like so: + +```ruby +class Avo::Resources::Team < Avo::BaseResource + def fields + field :id, as: :id + field :name, as: :text + field :description, as: :textarea + end +end +``` + +All fields have a name which is the first argument and the type of the field, as the `as:` argument. + +Most fields support optons that are universal for all fields. Some examples are: readonly, disabled, help, format_using, update using, hide_on, show_on, and others. +Some options are just for some fields. + +All field options: + + From cc666aa50a6c2302f8d67aab584d1cc0d2ffdc43 Mon Sep 17 00:00:00 2001 From: Paul Bob Date: Tue, 6 May 2025 14:39:56 +0300 Subject: [PATCH 2/2] add improved llms.txt --- .cursor/rules/avo.mdc | 23743 +++++++++++++++++++- .cursor/rules/execution-context.mdc | 123 - .cursor/rules/ruby-general.mdc | 66 - lib/generators/avo/rules_generator.rb | 15 - lib/generators/avo/templates/rules/avo.tt | 26 - 5 files changed, 23728 insertions(+), 245 deletions(-) delete mode 100644 .cursor/rules/execution-context.mdc delete mode 100644 .cursor/rules/ruby-general.mdc delete mode 100644 lib/generators/avo/rules_generator.rb delete mode 100644 lib/generators/avo/templates/rules/avo.tt diff --git a/.cursor/rules/avo.mdc b/.cursor/rules/avo.mdc index 972e294703..b9a85310c8 100644 --- a/.cursor/rules/avo.mdc +++ b/.cursor/rules/avo.mdc @@ -1,37 +1,23750 @@ ---- -description: Descriptor -globs: -alwaysApply: true ---- +# Avo + +Lean teams use Avo to build exceptional internal tools while it handles the technical heavy lifting, so they can focus on what matters. + +Avo offers a few big features to get that done: + +- CRUD +- Dashboards +- Advanced filters +- Kanban boards +- Collaboration tools +- Audit logging + +CRUD is probably the most important feature of Avo. It's how you create, read, update and delete records (manage records). + # Resources The Avo CRUD functionality uses the concept of a resource. A resource belongs to a model and a model may have multiple resources. The model is how Rails talks to the database and the resource is how Avo talks to Rails and knows how to fetch and manipulate the records. -Each resource is a class defined in a file. They inherit the `Avo::BaseResource` class which inherits `Avo::Resources::Base`. `Avo::BaseResource` is empty so the user can override anything they want on a global level in theyr own app. +Each resource is a ruby class in this configuration `Avo::Resources::RESOURCE_NAME` and inherits the `Avo::BaseResource` class which inherits `Avo::Resources::Base`. `Avo::BaseResource` is empty so the user can override anything they want on a global level in theyr own app. A resource has a multitude of options which are usually declared using the `self.OPTION_NAME = ` format. They can take a simple value like a string, boolean, symbol, hash or array or they can take an `ExecutionContext` which will give the developer much more control over what they can return from it. -## Fields -Fields are declared using the `field` method inside a `fields` method in a resource like so: + +# Customization + +Actions can be customized in several ways to enhance the user experience. You can modify the action's display name, confirmation message, button labels, and confirmation behavior between other things. + +There are 2 types of customization, visual and behavioral. + +## Visual customization + +Visual customization is the process of modifying the action's appearance. This includes changing the action's name, message and button labels. + +All visual customization options can be set as a string or a block. + +The blocks are executed using [`Avo::ExecutionContext`](#execution-context). Within these blocks, you gain access to: + +- All attributes of [`Avo::ExecutionContext`](#execution-context) +- `resource` - The current resource instance +- `record` - The current record +- `view` - The current view +- `arguments` - Any passed arguments +- `query` - The current query parameters + + + + + + + + + + +## Behavioral customization + +Behavioral customization is the process of modifying the action's behavior. This includes changing the action's confirmation behavior and authorization. + + + + + + + + + + + + + + + + + +# Execution flow + +When a user triggers an action in Avo, the following flow occurs: + +1. Record selection phase: + - This phase can be bypassed by setting `self.standalone = true` + - For bulk actions on the index page, Avo collects all the records selected by the user + - For actions on the show page [or row controls](#customizable-controls), Avo uses that record as the target of the action + +2. The action is initiated by the user through the index page (bulk actions), show page (single record actions), [or resource controls (custom action buttons)](#customizable-controls) + +3. Form display phase (optional): + - This phase can be bypassed by setting `self.no_confirmation = true` + - By default, a modal is displayed where the user can confirm or cancel the action + - If the action has defined fields, they will be shown in the modal for the user to fill out + - The user can then choose to run the action or cancel it + - If the user cancels, the execution stops here + +4. Action execution: + - The `handle` method processes selected records, form values, current user, and resource details + - Your custom business logic is executed within the `handle` method + - User feedback is configured ([`succeed`](#succeed), [`warn`](#warn), [`inform`](#inform), [`error`](#error), or [`silent`](#silent)) + - Response type is configured ([`redirect_to`](#redirect_to), [`reload`](#reload), [`keep_modal_open`](#keep_modal_open), and [more](#response-types)) + + +## The `handle` method + +The `handle` method is where you define what happens when your action is executed. This is the core of your action's business logic and receives the following arguments: + +- `query` Contains the selected record(s). Single records are automatically wrapped in an array for consistency +- `fields` Contains the values submitted through the action's form fields +- `current_user` The currently authenticated user +- `resource` The Avo resource instance that triggered the action + +```ruby{10-23} +# app/avo/actions/toggle_inactive.rb +class Avo::Actions::ToggleInactive < Avo::BaseAction + self.name = "Toggle Inactive" + + def fields + field :notify_user, as: :boolean + field :message, as: :textarea + end + + def handle(query:, fields:, current_user:, resource:, **args) + query.each do |record| + # Toggle the inactive status + record.update!(inactive: !record.inactive) + + # Send notification if requested + if fields[:notify_user] + # Assuming there's a notify method + record.notify(fields[:message]) + end + end + + succeed "Successfully toggled status for #{query.count}" + end +end +``` + +## Feedback notifications + +After an action runs, you can respond to the user with different types of notifications or no feedback at all. The default feedback is an `Action ran successfully` message of type `inform`. + + + + + + + + + + + + +:::info +You can show multiple notifications at once by calling multiple feedback methods (`succeed`, `warn`, `inform`, `error`) in your action's `handle` method. Each notification will be displayed in sequence. +::: + +```ruby{4-7} +# app/avo/actions/toggle_inactive.rb +class Avo::Actions::ToggleInactive < Avo::BaseAction + def handle(**args) + succeed "Success response ✌️" + warn "Warning response ✌️" + inform "Info response ✌️" + error "Error response ✌️" + end +end +``` + +Avo notification types + +## Response types + +After an action completes, you can control how the UI responds through various response types. These powerful responses give you fine-grained control over the user experience by allowing you to: + +- **Navigate**: Reload pages or redirect users to different parts of your application +- **Manipulate UI**: Control modals, update specific page elements, or refresh table rows +- **Handle Files**: Trigger file downloads and handle data exports +- **Show Feedback**: Combine with notification messages for clear user communication + +You can use these responses individually or combine them to create sophisticated interaction flows. Here are all the available action responses: + + + + + + + + + + + + + + + + + + + + + +# Action Generator + +Avo provides a powerful Rails generator to create action files quickly and efficiently. + +## Basic Generator Usage + +Generate a new action file using the Rails generator: + +```bash +bin/rails generate avo:action toggle_inactive +``` + +This command creates a new action file at `app/avo/actions/toggle_inactive.rb` with the following structure: + +```ruby +# app/avo/actions/toggle_inactive.rb +class Avo::Actions::ToggleInactive < Avo::BaseAction + self.name = "Toggle Inactive" + # self.visible = -> do + # true + # end + + # def fields + # # Add Action fields here + # end + + def handle(query:, fields:, current_user:, resource:, **args) + query.each do |record| + # Do something with your records. + end + end +end +``` + +## Generator Options + +### `--standalone` + +By default, actions require at least one record to be selected before they can be triggered, unless specifically configured as standalone actions. + +The `--standalone` option creates an action that doesn't require record selection. This is particularly useful for: +- Generating reports +- Exporting all records +- Running global operations + +```bash +bin/rails generate avo:action export_users --standalone +``` + +You can also make an existing action standalone by manually setting `self.standalone = true` in the action class: + +```ruby{5} +# app/avo/actions/export_users.rb + +class Avo::Actions::ExportUsers < Avo::BaseAction + self.name = "Export Users" + self.standalone = true + + # ... rest of the action code +end +``` + +## Best Practices + +When generating actions, consider the following: + +1. Use descriptive names that reflect the action's purpose (e.g., `toggle_published`, `send_newsletter`, `archive_records`) +2. Follow Ruby naming conventions (snake_case for file names) +3. Group related actions in namespaces using subdirectories +4. Use the `--standalone` flag when the action doesn't operate on specific records + +## Examples + +```bash +# Generate a regular action +bin/rails generate avo:action mark_as_featured + +# Generate a standalone action +bin/rails generate avo:action generate_monthly_report --standalone + +# Generate an action in a namespace +bin/rails generate avo:action admin/approve_user +``` + + + +# Action Generator + +Avo provides a powerful Rails generator to create action files quickly and efficiently. + +## Basic Generator Usage + +Generate a new action file using the Rails generator: + +```bash +bin/rails generate avo:action toggle_inactive +``` + +This command creates a new action file at `app/avo/actions/toggle_inactive.rb` with the following structure: + +```ruby +# app/avo/actions/toggle_inactive.rb +class Avo::Actions::ToggleInactive < Avo::BaseAction + self.name = "Toggle Inactive" + # self.visible = -> do + # true + # end + + # def fields + # # Add Action fields here + # end + + def handle(query:, fields:, current_user:, resource:, **args) + query.each do |record| + # Do something with your records. + end + end +end +``` + +## Generator Options + +### `--standalone` + +By default, actions require at least one record to be selected before they can be triggered, unless specifically configured as standalone actions. + +The `--standalone` option creates an action that doesn't require record selection. This is particularly useful for: +- Generating reports +- Exporting all records +- Running global operations + +```bash +bin/rails generate avo:action export_users --standalone +``` + +You can also make an existing action standalone by manually setting `self.standalone = true` in the action class: + +```ruby{5} +# app/avo/actions/export_users.rb + +class Avo::Actions::ExportUsers < Avo::BaseAction + self.name = "Export Users" + self.standalone = true + + # ... rest of the action code +end +``` + +## Best Practices + +When generating actions, consider the following: + +1. Use descriptive names that reflect the action's purpose (e.g., `toggle_published`, `send_newsletter`, `archive_records`) +2. Follow Ruby naming conventions (snake_case for file names) +3. Group related actions in namespaces using subdirectories +4. Use the `--standalone` flag when the action doesn't operate on specific records + +## Examples + +```bash +# Generate a regular action +bin/rails generate avo:action mark_as_featured + +# Generate a standalone action +bin/rails generate avo:action generate_monthly_report --standalone + +# Generate an action in a namespace +bin/rails generate avo:action admin/approve_user +``` + + +# WIP +this section is under construction +## Helpers + +### `link_arguments` + +The `link_arguments` method is used to generate the arguments for an action link. + +You may want to dynamically generate an action link. For that you need the action class and a resource instance (with or without record hydrated). Call the action's class method `link_arguments` with the resource instance as argument and it will return the `[path, data]` that are necessary to create a proper link to a resource. + +Let's see an example use case: + +```ruby{4-,16} [Current Version] +# app/avo/resources/city.rb +class Avo::Resources::City < Avo::BaseResource + field :name, as: :text, name: "Name (click to edit)", only_on: :index do + path, data = Avo::Actions::City::Update.link_arguments( + resource: resource, + arguments: { + cities: Array[resource.record.id], + render_name: true + } + ) + + link_to resource.record.name, path, data: data + end +end +``` + + +actions link demo + +:::tip +#### Generate an Action Link Without a Resource Instance +Sometimes, you may need to generate an action link without having access to an instantiated resource. + +#### Scenario +Imagine you want to trigger an action from a custom partial card on a dashboard, but there is no resource instance available. + +#### Solution +In this case, you can create a new resource instance (with or without record) and use it as follows: + +```ruby +path, data = Avo::Actions::City::Update.link_arguments( + resource: Avo::Resources::City.new(record: city) +) + +link_to "Update city", path, data: data +``` +::: + +## Guides + +### StimulusJS + +Please follow our extended [StimulusJS guides](#stimulus-integration) for more information. + +### Passing Params to the Action Show Page +When navigation to an action from a resource or views, it's sometimes useful to pass parameters to an action. + +One particular example is when you'd like to populate a field in that action with some particular value based on that param. + +```ruby +class Action + def fields + field :some_field, as: :hidden, default: -> { if previous_param == yes ? :yes : :no} + end +end +``` +Consider the following scenario: + +1. Navigate to `https://main.avodemo.com/avo/resources/users`. +2. Add the parameter `hey=ya` to the URL: `https://main.avodemo.com/avo/resources/users?hey=ya` +3. Attempt to run the dummy action. +4. After triggering the action, verify that you can access the `hey` parameter. +5. Ensure that the retrieved value of the `hey` parameter is `ya`. + +**Implementation** + +To achieve this, we'll reference the `request.referer` object and extract parameters from the URL. Here is how to do it: + +```ruby +class Action + def fields + # Accessing the parameters passed from the parent view + field :some_field, as: :hidden, default: -> { + # Parsing the request referer to extract parameters + parent_params = URI.parse(request.referer).query.split("&").map { |param| param.split("=")}.to_h.with_indifferent_access + # Checking if the `hei` parameter equals `ya` + if parent_params[:hey] == 'ya' + :yes + else + :no + end + } + end +end +``` +Parse the `request.referer` to extract parameters using `URI.parse`. +Split the query string into key-value pairs and convert it into a hash. +Check if the `hey` parameter equals `ya`, and set the default value of `some_field` accordingly. + + + +# Actions Overview + + + +Actions in Avo are powerful tools that transform the way you interact with your data. They enable you to perform operations on one or multiple records simultaneously, extending your interface with custom functionality that goes beyond basic CRUD operations. + +## What Are Actions? + +Think of Actions as custom operations you can trigger from your admin interface. They're like specialized commands that can: +- Process single records or work in batch mode +- Collect additional information through customizable forms +- Trigger background jobs +- Generate reports or export data +- Modify record states +- Send notifications +- And much more... + +## Key Benefits + +### 1. Streamlined Workflows +Instead of building custom interfaces for common operations, Actions provide a standardized way to perform complex tasks right from your admin panel. + +### 2. Flexibility +Actions can be as simple or as complex as you need: +- Simple toggles for changing record states +- Multi-step processes with user input on each step +- Background job triggers for heavy operations +- API integrations with external services + +### 3. Batch Operations +Save time by performing operations on multiple records at once. Whether you're updating statuses, sending notifications, or processing data, batch actions have you covered. + +### 4. User Input Forms +When additional information is needed, Actions can present custom forms to collect data before execution. These forms are fully customizable and support various field types. + +## Common Use Cases + +- **User Management**: Activate/deactivate accounts, reset passwords, or send welcome emails +- **Content Moderation**: Approve/reject content, flag items for review +- **Data Processing**: Generate reports, export data, or trigger data transformations +- **Communication**: Send notifications, emails, or SMS messages +- **State Management**: Change status, toggle features, or update permissions +- **Batch Updates**: Modify multiple records with consistent changes +- **Integration Triggers**: Connect with external APIs or services + +Common use cases include managing user states, sending notifications, and automating data processing. Their flexibility makes them essential for building robust interfaces, streamlining workflows, and managing data efficiently. + + + +# Registration + +Actions are registered within a resource by using the resource's `actions` method. This method defines which actions are available for that specific resource. + +## `action` + +The `action` method is used to register an action within the `actions` block. It accepts the action class as its first argument and optional configuration parameters like `arguments` and `icon` + +```ruby{5} +# app/avo/resources/user.rb +class Avo::Resources::User < Avo::BaseResource + def actions + # Basic registration + action Avo::Actions::ToggleInactive + end +end +``` + +:::warning +Using the Pundit policies, you can restrict access to actions using the `act_on?` method. If you think you should see an action on a resource and you don't, please check the policy method. + +More info [here](#authorization) +::: + +Once attached, the action will appear in the **Actions** dropdown menu. By default, actions are available on all views. + +:::info +You may use the [customizable controls](#customizable-controls) feature to show the actions outside the dropdown. +::: + + + + + +--- + +## `divider` + + + +Action dividers allow you to organize and separate actions into logical groups, improving the overall layout and usability. +This will create a visual separator in the actions dropdown menu, helping you group related actions together. + +```ruby{8} +# app/avo/resources/user.rb +class Avo::Resources::User < Avo::BaseResource + def actions + # User status actions + action Avo::Actions::ActivateUser + action Avo::Actions::DeactivateUser + + divider + + # Communication actions + action Avo::Actions::SendWelcomeEmail + action Avo::Actions::SendPasswordReset + end +end +``` + + + + + +# Belongs to + +```ruby +field :user, as: :belongs_to +``` + +You will see three field types when you add a `BelongsTo` association to a model. + + +## Options + + + + + + + + +:::warning +The `attach_scope` will not filter the records in the listing from `has_many` or `has_and_belongs_to_many` associations. +Use [`scope`](#scope) or a [Pundit policy `Scope`](#authorization) for that. +::: + +```ruby-vue{3} +field :members, + as: :{{ $frontmatter.field_type }}, + attach_scope: -> { query.where.not(team_id: parent.id) } + ``` +In this example, in the `attach_scope`, we ensure that when attaching members to a team, only those who are not already members will appear in the list of options. + + + + + + + + + + + + + +## Overview + +On the `Index` and `Show` views, Avo will generate a link to the associated record containing the [`self.title`](#resources) value of the target resource. + +Belongs to index + +Belongs to show + +On the `Edit` and `New` views, Avo will generate a dropdown element with the available records where the user can change the associated model. + +Belongs to edit + +## Polymorphic `belongs_to` + +To use a polymorphic relation, you must add the `polymorphic_as` and `types` properties. + +```ruby{13} +class Avo::Resources::Comment < Avo::BaseResource + self.title = :id + + def fields + field :id, as: :id + field :body, as: :textarea + field :excerpt, as: :text, show_on: :index do + ActionView::Base.full_sanitizer.sanitize(record.body).truncate 60 + rescue + "" + end + + field :commentable, as: :belongs_to, polymorphic_as: :commentable, types: [::Post, ::Project] + end +end +``` + +## Polymorphic help + +When displaying a polymorphic association, you will see two dropdowns. One selects the polymorphic type (`Post` or `Project`), and one for choosing the actual record. You may want to give the user explicit information about those dropdowns using the `polymorphic_help` option for the first dropdown and `help` for the second. + +```ruby{17-18} +class Avo::Resources::Comment < Avo::BaseResource + self.title = :id + + def fields + field :id, as: :id + field :body, as: :textarea + field :excerpt, as: :text, show_on: :index do + ActionView::Base.full_sanitizer.sanitize(record.body).truncate 60 + rescue + "" + end + + field :reviewable, + as: :belongs_to, + polymorphic_as: :reviewable, + types: [::Post, ::Project, ::Team], + polymorphic_help: "Choose the type of record to review", + help: "Choose the record you need." + end +end +``` + +Belongs to ploymorphic help + +## Searchable `belongs_to` + + + +There might be the case that you have a lot of records for the parent resource, and a simple dropdown won't cut it. This is where you can use the `searchable` option to get a better search experience for that resource. + +```ruby{8} +class Avo::Resources::Comment < Avo::BaseResource + self.title = :id + + def fields + field :id, as: :id + field :body, as: :textarea + + field :user, as: :belongs_to, searchable: true + end +end +``` + +Belongs to searchable +Belongs to searchable + +`searchable` works with `polymorphic` `belongs_to` associations too. + +```ruby{8} +class Avo::Resources::Comment < Avo::BaseResource + self.title = :id + + def fields + field :id, as: :id + field :body, as: :textarea + + field :commentable, as: :belongs_to, polymorphic_as: :commentable, types: [::Post, ::Project], searchable: true + end +end +``` + +:::info +Avo uses the [search feature](#search) behind the scenes, so **make sure the target resource has the `query` option configured inside the `search` block**. +::: + + +```ruby +# app/avo/resources/post.rb +class Avo::Resources::Post < Avo::BaseResource + self.search = { + query: -> { + query.ransack(id_eq: params[:q], name_cont: params[:q], body_cont: params[:q], m: "or").result(distinct: false) + } + } +end + +# app/avo/resources/project.rb +class Avo::Resources::Project < Avo::BaseResource + self.search = { + query: -> { + query.ransack(id_eq: params[:q], name_cont: params[:q], country_cont: params[:q], m: "or").result(distinct: false) + } + } +end +``` + +## Belongs to attach scope + + + +When you edit a record that has a `belongs_to` association, on the edit screen, you will have a list of records from which you can choose a record to associate with. + +For example, a `Post` belongs to a `User`. So on the post edit screen, you will have a dropdown (or a search field if it's [searchable](#searchable-belongs-to)) with all the available users. But that's not ideal. For example, maybe you don't want to show all the users in your app but only those who are not admins. + +You can use the `attach_scope` option to keep only the users you need in the `belongs_to` dropdown field. + +You have access to the `query` that you can alter and return it and the `parent` object, which is the actual record where you want to assign the association (the true `Post` in the below example). + +```ruby +# app/models/user.rb +class User < ApplicationRecord + scope :non_admins, -> { where "(roles->>'admin')::boolean != true" } +end + +# app/avo/resources/post.rb +class Avo::Resources::Post < Avo::BaseResource + def fields + field :user, as: :belongs_to, attach_scope: -> { query.non_admins } + end +end +``` + +For scenarios where you need to add a record associated with that resource (you create a `Post` through a `Category`), the `parent` is unavailable (the `Post` is not persisted in the database). Therefore, Avo makes the `parent` an instantiated object with its parent populated (a `Post` with the `category_id` populated with the parent `Category` from which you started the creation process) so you can better scope out the data (you know from which `Category` it was initiated). + +## Allow detaching via the association + +When you visit a record through an association, that `belongs_to` field is disabled. There might be cases where you'd like that field not to be disabled and allow your users to change that association. + +You can instruct Avo to keep that field enabled in this scenario using `allow_via_detaching`. + +```ruby{12} +class Avo::Resources::Comment < Avo::BaseResource + self.title = :id + + def fields + field :id, as: :id + field :body, as: :textarea + + field :commentable, + as: :belongs_to, + polymorphic_as: :commentable, + types: [::Post, ::Project], + allow_via_detaching: true + end +end +``` + + + +# Has And Belongs To Many + +The `HasAndBelongsToMany` association works similarly to [`HasMany`](#has-many). + +```ruby +field :users, as: :has_and_belongs_to_many +``` + +## Options + + + + +:::warning +The `attach_scope` will not filter the records in the listing from `has_many` or `has_and_belongs_to_many` associations. +Use [`scope`](#scope) or a [Pundit policy `Scope`](#authorization) for that. +::: + +```ruby-vue{3} +field :members, + as: :{{ $frontmatter.field_type }}, + attach_scope: -> { query.where.not(team_id: parent.id) } + ``` +In this example, in the `attach_scope`, we ensure that when attaching members to a team, only those who are not already members will appear in the list of options. + + + + + + + + + + + + + + +## Search query scope + + + +If the resource used for the `has_many` association has the `search` block configured with a `query`, Avo will use that to scope out the search query to that association. + +For example, if you have a `Team` model that `has_many` `User`s, now you'll be able to search through that team's users instead of all of them. + +You can target that search using `params[:via_association]`. When the value of `params[:via_association]` is `has_many`, the search has been mad inside a has_many association. + +For example, if you want to show the records in a different order, you can do this: + +```ruby +self.search = { + query: -> { + if params[:via_association] == 'has_many' + query.ransack(id_eq: params[:q], m: "or").result(distinct: false).order(name: :asc) + else + query.ransack(id_eq: params[:q], m: "or").result(distinct: false) + end + } +} +``` + +## Show on edit screens + +By default, the `{{ $frontmatter.field_type }}` field is only visible in the [show](#views) view. To make it available in the [edit](#views) view as well, include the `show_on: :edit` option. This ensures that the `{{ $frontmatter.field_type }}` [show](#views) view component is also rendered within the [edit](#views) view. + +## Nested in Forms +
+ + + +
+ + +You can use ["Show on edit screens"](#show-on-edit-screens) to make the `{{ $frontmatter.field_type }}` field available in the [edit](#views) view. However, this will render it using the [show](#views) view component. + +To enable nested creation for the `{{ $frontmatter.field_type }}` field, allowing it to be created and / or edited alongside its parent record within the same form, use the `nested` option which is a hash with configurable option. + + +Keep in mind that this will display the field’s resource as it appears in the edit view. + + + + +### Searchable `has_and_belongs_to_many` + +
+ + +
+ + +Similar to [`belongs_to`](#belongs_to), the `has_many` associations support the `searchable` option. + +## Add scopes to associations + + + +When displaying `has_many` associations, you might want to scope out some associated records. For example, a user might have multiple comments, but on the user's `Show` page, you don't want to display all the comments, but only the approved ones. + +```ruby{5,16,22} +# app/models/comment.rb +class Comment < ApplicationRecord + belongs_to :user, optional: true + + scope :approved, -> { where(approved: true) } +end + +# app/models/user.rb +class User < ApplicationRecord + has_many :comments +end + +# app/avo/resources/user.rb +class Avo::Resources::User < Avo::BaseResource + def fields + field :comments, as: :has_many, scope: -> { query.approved } + end +end +``` + +The `comments` query on the user `Index` page will have the `approved` scope attached. + +Association scope + +With version 2.5.0, you'll also have access to the `parent` record so that you can use that to scope your associated models even better. + +Starting with version 3.12, access to `resource` and `parent_resource` was additionally provided. + +All the `has_many` associations have the [`attach_scope`](#belongs_to) option available too. + +## Show/hide buttons + +You will want to control the visibility of the attach/detach/create/destroy/actions buttons visible throughout your app. You can use the policy methods to do that. + +Find out more on the [authorization](#authorization) page. + +Associations authorization + + + + + + + + +# Has Many + +By default, the `HasMany` field is visible only on the `Show` view. You will see a new panel with the model's associated records below the regular fields panel. + +```ruby +field :projects, as: :has_many +``` + +## Options + + + + + +:::warning +The `attach_scope` will not filter the records in the listing from `has_many` or `has_and_belongs_to_many` associations. +Use [`scope`](#scope) or a [Pundit policy `Scope`](#authorization) for that. +::: + +```ruby-vue{3} +field :members, + as: :{{ $frontmatter.field_type }}, + attach_scope: -> { query.where.not(team_id: parent.id) } + ``` +In this example, in the `attach_scope`, we ensure that when attaching members to a team, only those who are not already members will appear in the list of options. + + + + + + + + + + + + + + + + +## Search query scope + + + +If the resource used for the `has_many` association has the `search` block configured with a `query`, Avo will use that to scope out the search query to that association. + +For example, if you have a `Team` model that `has_many` `User`s, now you'll be able to search through that team's users instead of all of them. + +You can target that search using `params[:via_association]`. When the value of `params[:via_association]` is `has_many`, the search has been mad inside a has_many association. + +For example, if you want to show the records in a different order, you can do this: + +```ruby +self.search = { + query: -> { + if params[:via_association] == 'has_many' + query.ransack(id_eq: params[:q], m: "or").result(distinct: false).order(name: :asc) + else + query.ransack(id_eq: params[:q], m: "or").result(distinct: false) + end + } +} +``` + + + + + +## Has Many Through + +The `HasMany` association also supports the `:through` option. + +```ruby{3} +field :members, + as: :has_many, + through: :memberships +``` + + +## Show on edit screens + +By default, the `{{ $frontmatter.field_type }}` field is only visible in the [show](#views) view. To make it available in the [edit](#views) view as well, include the `show_on: :edit` option. This ensures that the `{{ $frontmatter.field_type }}` [show](#views) view component is also rendered within the [edit](#views) view. + +## Nested in Forms +
+ + + +
+ + +You can use ["Show on edit screens"](#show-on-edit-screens) to make the `{{ $frontmatter.field_type }}` field available in the [edit](#views) view. However, this will render it using the [show](#views) view component. + +To enable nested creation for the `{{ $frontmatter.field_type }}` field, allowing it to be created and / or edited alongside its parent record within the same form, use the `nested` option which is a hash with configurable option. + + +Keep in mind that this will display the field’s resource as it appears in the edit view. + + + +## Add scopes to associations + + + +When displaying `has_many` associations, you might want to scope out some associated records. For example, a user might have multiple comments, but on the user's `Show` page, you don't want to display all the comments, but only the approved ones. + +```ruby{5,16,22} +# app/models/comment.rb +class Comment < ApplicationRecord + belongs_to :user, optional: true + + scope :approved, -> { where(approved: true) } +end + +# app/models/user.rb +class User < ApplicationRecord + has_many :comments +end + +# app/avo/resources/user.rb +class Avo::Resources::User < Avo::BaseResource + def fields + field :comments, as: :has_many, scope: -> { query.approved } + end +end +``` + +The `comments` query on the user `Index` page will have the `approved` scope attached. + +Association scope + +With version 2.5.0, you'll also have access to the `parent` record so that you can use that to scope your associated models even better. + +Starting with version 3.12, access to `resource` and `parent_resource` was additionally provided. + +All the `has_many` associations have the [`attach_scope`](#belongs_to) option available too. + +## Show/hide buttons + +You will want to control the visibility of the attach/detach/create/destroy/actions buttons visible throughout your app. You can use the policy methods to do that. + +Find out more on the [authorization](#authorization) page. + +Associations authorization + + + + + + + + +:::warning +It's important to set the `inverse_of` as often as possible to your model's association attribute. +::: + +# Has One + +The `HasOne` association shows the unfolded view of your `has_one` association. It's like peaking on the `Show` view of that associated record. The user can also access the `Attach` and `Detach` buttons. + +```ruby +field :admin, as: :has_one +``` + +Has one + +## Options + + + + + +:::warning +The `attach_scope` will not filter the records in the listing from `has_many` or `has_and_belongs_to_many` associations. +Use [`scope`](#scope) or a [Pundit policy `Scope`](#authorization) for that. +::: + +```ruby-vue{3} +field :members, + as: :{{ $frontmatter.field_type }}, + attach_scope: -> { query.where.not(team_id: parent.id) } + ``` +In this example, in the `attach_scope`, we ensure that when attaching members to a team, only those who are not already members will appear in the list of options. + + +## Show on edit screens + +By default, the `{{ $frontmatter.field_type }}` field is only visible in the [show](#views) view. To make it available in the [edit](#views) view as well, include the `show_on: :edit` option. This ensures that the `{{ $frontmatter.field_type }}` [show](#views) view component is also rendered within the [edit](#views) view. + +## Nested in Forms +
+ + + +
+ + +You can use ["Show on edit screens"](#show-on-edit-screens) to make the `{{ $frontmatter.field_type }}` field available in the [edit](#views) view. However, this will render it using the [show](#views) view component. + +To enable nested creation for the `{{ $frontmatter.field_type }}` field, allowing it to be created and / or edited alongside its parent record within the same form, use the `nested` option which is a hash with configurable option. + + +Keep in mind that this will display the field’s resource as it appears in the edit view. + + + + + + +# Audit Logging + +Avo's Audit Logging feature provides a seamless way to track and visualize user activity and changes within your applications. It seamlessly integrates with [`paper_trail`](https://github.com/paper-trail-gem/paper_trail), offering flexible installation and customization options. + +Captures user activities on Avo resources and actions, recording details such as the author and the performed event. + +The installation process will automatically generate the necessary migrations, resources, and controllers that power activity tracking. Additionally [`paper_trail`](https://github.com/paper-trail-gem/paper_trail) will be installed if it is not already present in your project. + +## Requirements + +- `avo-advanced` + +## Installation + +:::info +When installing `avo-audit_logging` on an application, we strongly recommend following this documentation page step-by-step without skipping sections, as it was designed with that approach in mind. +::: + +### 1. Install the gem + +Start by adding the following to your `Gemfile`: + +```bash +gem "avo-audit_logging", source: "https://packager.dev/avo-hq/" +``` + +Then +```bash +bundle install +``` + +### 2. Run the installer + +```bash +bin/rails generate avo:audit_logging install +``` + +### 3. Migrate + +At this stage, all migrations, resources, and controllers required for the audit logging feature are set up and ready, it's time to migrate: + +```bash +bin/rails db:migrate +``` + +## Enable and configure audit logging + +### Global enable + +After installation, audit logging is disabled by default. To enable it, navigate to your `avo.rb` initializer file and update the configuration for the `Avo::AuditLogging` module. + +Set `config.enabled` to `true` within this configuration. + +```ruby +# config/initializers/avo.rb # [!code focus] + +Avo.configure do |config| + # ... +end + +Avo::AuditLogging.configure do |config| # [!code focus] + # config.enabled = false # [!code --] # [!code focus] + config.enabled = true # [!code ++] # [!code focus] + # config.author_model = "User" +end # [!code focus] +``` + +:::info +Setting this configuration to `false` will disable the audit logging feature entirely, overriding any other specific settings. We'll cover those specific settings in the next steps. +::: + +:::warning +Setting this configuration to `false` will not prevent previously registered activity from being displayed. + +To control the display behavior when this configuration is set to `false`, +you can wrap the relevant fields or tools within an `Avo::AuditLogging.configuration.enabled?` condition, like this: + +```ruby{6-8} +class Avo::Resources::User < Avo::BaseResource + def fields + field :id, as: :id, link_to_record: true + field :email, as: :text, link_to_record: true + field :products, as: :has_many + if Avo::AuditLogging.configuration.enabled? + field :avo_authored, as: :has_many, name: "Activity" + end + end +end +``` +::: + +### Configure author models + +:::info +If `User` is your only author model, you can skip this step as it will be automatically set by default. +::: + +Avo must determine the potential author models to correctly establish associations in the background. This setup enables the retrieval of all activities associated with a specific author via the `avo_authored` association. To designate a model as an author, use `config.author_model`, for multiple models, utilize `config.author_models`. + +```ruby +# config/initializers/avo.rb # [!code focus] + +Avo.configure do |config| + # ... +end + +Avo::AuditLogging.configure do |config| # [!code focus] + config.enabled = true + + # config.author_model = "User" # [!code --] # [!code focus] + config.author_model = "Account" # [!code ++] # [!code focus] + + # Or for multiples models # [!code focus] + config.author_models = ["User", "Account"] # [!code ++] # [!code focus] +end # [!code focus] +``` + +### Enable specific resources and actions + +At this stage, the audit logging feature should be enabled, but activities are not yet being saved. By default, only resources and actions that are explicitly enabled for auditing will be tracked. + +To enable audit logging for specific resources or actions, use the `self.audit_logging` class attribute. + +:::code-group +```ruby [Resource]{2-4} +class Avo::Resources::Product < Avo::BaseResource # [!code focus] + self.audit_logging = { # [!code ++] # [!code focus] + activity: true # [!code ++] # [!code focus] + } # [!code ++] # [!code focus] + + def fields + field :id, as: :id, link_to_record: true + field :name, as: :text, link_to_record: true + field :price, as: :number, step: 1 + # ... + end + + def actions + action Avo::Actions::ChangePrice + end +end # [!code focus] +``` + +```ruby [Action]{4-6} +class Avo::Actions::ChangePrice < Avo::BaseAction # [!code focus] + self.name = "Change Price" + + self.audit_logging = { # [!code ++] # [!code focus] + activity: true # [!code ++] # [!code focus] + } # [!code ++] # [!code focus] + + def fields + field :price, as: :number, default: -> { resource.record.price rescue nil } + end + + def handle(query:, fields:, current_user:, resource:, **args) + query.each do |record| + record.update!(price: fields[:price]) + end + end +end # [!code focus] +``` +::: + +All resources and actions with audit logging activity enabled are being tracked now. + +But these activities aren't visible yet, right? Let's look at how to display them in the next step. + +## Display logged activities + +### Resource-Specific Activities + +The `Avo::ResourceTools::Timeline` tool, provided by the `avo-audit_logging` gem, is designed for use in the sidebar. It offers a compact view of activities that have occurred on a specific resource, presenting them in a streamlined format: + +Avo compact activities on sidebar image + +### Configuring the Sidebar for Activity Tracking + +To enable this feature, configure the resource to include the resource tool in the main menu sidebar: + +```ruby{7,12-15} +class Avo::Resources::Product < Avo::BaseResource # [!code focus] + self.audit_logging = { + activity: true + } + + def fields # [!code focus] + main_panel do # [!code ++] # [!code focus] + field :id, as: :id, link_to_record: true + field :name, as: :text, link_to_record: true + field :price, as: :number, step: 1 + + sidebar do # [!code ++] # [!code focus] + tool Avo::ResourceTools::Timeline # [!code ++] # [!code focus] + end # [!code ++] # [!code focus] + end # [!code ++] # [!code focus] + + field :avo_activities, as: :has_many # [!code focus] + end # [!code focus] + + def actions + action Avo::Actions::ChangePrice + end +end # [!code focus] +``` + +### Viewing and Navigating Activity Logs + +Hovering over an entry reveals the precise timestamp in UTC. Clicking on an entry navigates to a detailed page displaying the full payload. + +Hover on activity + +### Enabling Change Logs and Reverting Changes + +By default, update activities do not display a change log, and there is no way to revert changes. This is because PaperTrail has not yet been enabled on the model. To enable it, simply add `has_paper_trail` to the model: + +```ruby +# app/models/product.rb # [!code focus] + +class Product < ApplicationRecord # [!code focus] + has_paper_trail # [!code ++] # [!code focus] + + belongs_to :user, optional: true + + validates_presence_of :price +end # [!code focus] +``` + +Once enabled, the changelog will be visible, along with an action to revert changes. + +Activity details page + +### Troubleshooting: Missing `changeset` Field + +:::warning +If the `changeset` field in the versions table consistently appears as `nil`, ensure you add the following configuration in your `application.rb` file: + +```ruby +config.active_record.yaml_column_permitted_classes = [Symbol, Date, Time, ActiveSupport::TimeWithZone, ActiveSupport::TimeZone] +``` +::: + +### Display author logged activities + +We’ve already covered how to view all activity on a specific record. Now, let’s display a table within `Avo::Resources::User` to view all tracked activity for a particular user. + +Authored table image + +:::warning +If you're using a model other than `User`, make sure you have already [configured the author models](#configure-author-models). +::: + +```ruby +class Avo::Resources::User < Avo::BaseResource # [!code focus] + def fields # [!code focus] + field :id, as: :id, link_to_record: true + field :email, as: :text, link_to_record: true + field :products, as: :has_many + field :avo_authored, as: :has_many, name: "Activity" # [!code ++] # [!code focus] + end # [!code focus] +end # [!code focus] +``` + +### Overview of all activities + +We've covered how to view activities for specific records and how to view all actions made by a particular author. However, having an overview of all the activities in one place can also be useful. This can be achieved by configuring the menu to include a section with an entry for all activities. + +```ruby +# config/initializers/avo.rb + +Avo.configure do |config| + config.main_menu = -> { + section "AuditLogging", icon: "presentation-chart-bar" do # [!code ++] + resource :avo_activity # [!code ++] + end # [!code ++] + } +end +``` + +## Disable specific actions logging + +By default, when audit logging is enabled for a resource or action, all actions, such as `index` visits, `show` visits, `edit`, `update`, etc. are logged. + +If you prefer not to log all of these actions, configure the `actions` key within the `self.audit_logging` class attribute. + +Let's turn off `edit` and `show` logging for the `Avo::Resources::Product`: + +```ruby +class Avo::Resources::Product < Avo::BaseResource # [!code focus] + self.audit_logging = { # [!code focus] + activity: true, # [!code focus] + actions: { # [!code ++] # [!code focus] + edit: false, # [!code ++] # [!code focus] + show: false # [!code ++] # [!code focus] + } # [!code ++] # [!code focus] + } # [!code focus] + + def fields + main_menu do + field :id, as: :id, link_to_record: true + field :name, as: :text, link_to_record: true + field :price, as: :number, step: 1 + + sidebar do + tool Avo::ResourceTools::Timeline + end + end + # ... + field :avo_activities, as: :has_many + end + + def actions + action Avo::Actions::ChangePrice + end +end # [!code focus] +``` + +The default value for `actions` is: + +```ruby +{ + index: true, + new: true, + create: true, + edit: true, + update: true, + show: true, + destroy: true, + attach: true, + detach: true, + handle: true +} +``` + +## Conclusion + +With Avo's Audit Logging, you gain a powerful tool to track and visualize user actions and record changes seamlessly across your application. By carefully following the setup steps and configuring logging to fit your needs, you can establish a robust and transparent audit system, enhancing accountability and preserving data integrity. + +Happy auditing! + + + +# 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](#array-resources) 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}?](#authorization) 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 + + + + + + + + + + +## 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](#authorization) + + + + +## 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`](#field-options) 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`](#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](#authorization) + + + + + + + + + + +# 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](#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](#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](#resources) + + + + +# 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](#resources). + +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](#authorization). + + + +# 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](#resources). +::: + +```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](#field-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 +``` + +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](#media-library). + +## Options + + + + + + +# Select + +The `Select` field renders a `select` field. + +```ruby +field :type, as: :select, options: { 'Large container': :large, 'Medium container': :medium, 'Tiny container': :tiny }, display_with_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 `