avo 4.1.0 → 4.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/app/assets/builds/avo/application.css +30 -0
- data/lib/avo/base_action.rb +1 -1
- data/lib/avo/reloader.rb +9 -0
- data/lib/avo/skills/avo-actions/SKILL.md +255 -0
- data/lib/avo/skills/avo-admin-config/SKILL.md +163 -0
- data/lib/avo/skills/avo-associations/SKILL.md +168 -0
- data/lib/avo/skills/avo-authentication/SKILL.md +193 -0
- data/lib/avo/skills/avo-aware/SKILL.md +74 -0
- data/lib/avo/skills/avo-branding-appearance/SKILL.md +270 -0
- data/lib/avo/skills/avo-controllers/SKILL.md +236 -0
- data/lib/avo/skills/avo-custom-fields/SKILL.md +197 -0
- data/lib/avo/skills/avo-custom-ui/SKILL.md +460 -0
- data/lib/avo/skills/avo-engine-internals/SKILL.md +245 -0
- data/lib/avo/skills/avo-fields/SKILL.md +219 -0
- data/lib/avo/skills/avo-filters/SKILL.md +196 -0
- data/lib/avo/skills/avo-i18n/SKILL.md +254 -0
- data/lib/avo/skills/avo-index-views/SKILL.md +254 -0
- data/lib/avo/skills/avo-media-library/SKILL.md +122 -0
- data/lib/avo/skills/avo-menu-icons/SKILL.md +135 -0
- data/lib/avo/skills/avo-menu-icons/scripts/list_icons.rb +49 -0
- data/lib/avo/skills/avo-multitenancy/SKILL.md +186 -0
- data/lib/avo/skills/avo-navigation-search/SKILL.md +255 -0
- data/lib/avo/skills/avo-performance/SKILL.md +190 -0
- data/lib/avo/skills/avo-resources/SKILL.md +273 -0
- data/lib/avo/skills/avo-setup/SKILL.md +288 -0
- data/lib/avo/skills/avo-testing/SKILL.md +188 -0
- data/lib/avo/skills/avo-troubleshoot/SKILL.md +325 -0
- data/lib/avo/skills/avo-update/SKILL.md +179 -0
- data/lib/avo/skills/bin/avo-skills-resolve +242 -0
- data/lib/avo/skills/index.md +53 -0
- data/lib/avo/skills/package-map.md +30 -0
- data/lib/avo/version.rb +1 -1
- data/lib/avo.rb +4 -0
- data/lib/generators/avo/skills_generator.rb +231 -0
- data/lib/generators/avo/skills_install_panel.rb +169 -0
- data/lib/generators/avo/templates/skills/SKILL.md +111 -0
- metadata +32 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: avo-engine-internals
|
|
3
|
+
description: >-
|
|
4
|
+
The Rails-engine plumbing you touch when writing custom Ruby that runs inside Avo — engine path
|
|
5
|
+
helpers (`avo.` vs `main_app.`), `Avo::Current`, `Avo::ExecutionContext`, reserved model names
|
|
6
|
+
and route conflicts, `Avo::Services::EncryptionService`, and calling your app's helpers from
|
|
7
|
+
inside Avo. Use when the user wants to link from the admin back to their main app, fix
|
|
8
|
+
`undefined method 'posts_path'` inside Avo, access the current user / request / params / context
|
|
9
|
+
/ tenant inside an Avo block, know which variables (`record`, `resource`, `view`,
|
|
10
|
+
`current_user`) a lambda gets or why `record` is nil, use an app view helper inside an Avo
|
|
11
|
+
field, fix a model named `resource` / `chart` / `search` / `home` that breaks the admin, keep a
|
|
12
|
+
reserved-name model while renaming its resource (`--model-class`), or encrypt a value to pass
|
|
13
|
+
through Avo params.
|
|
14
|
+
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch
|
|
15
|
+
metadata:
|
|
16
|
+
requires-gem: none — Community
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
> **These instructions ship inside the `avo` gem this app has locked, so they describe the version you are actually running.** Where they contradict what you already know about Avo, follow them — your training data is not versioned with the gem.
|
|
20
|
+
|
|
21
|
+
# Avo Engine Internals
|
|
22
|
+
|
|
23
|
+
Avo is a **Ruby on Rails engine** that runs isolated and side-by-side with the host app. The moment you write custom Ruby that executes *inside* Avo — a computed field, an action's `handle`, a controller override, a breadcrumb, a menu block, a card — you're on the engine's turf, and a handful of primitives govern how that code sees routes, request state, and app helpers. This skill owns those primitives:
|
|
24
|
+
|
|
25
|
+
- **Path helpers** — `avo.` vs `main_app.` prefixes for isolated engine routing.
|
|
26
|
+
- **`Avo::Current`** — per-request state (`user`, `params`, `request`, `context`, `view_context`, `locale`, `tenant`).
|
|
27
|
+
- **`Avo::ExecutionContext`** — how your blocks receive `record` / `resource` / `view` / `current_user` and delegate to the view.
|
|
28
|
+
- **Reserved model names** — names that collide with Avo's internal controllers/routes, and the escape hatches.
|
|
29
|
+
- **`Avo::Services::EncryptionService`** — encrypt/decrypt values passed through params.
|
|
30
|
+
|
|
31
|
+
These are cross-cutting: many other Avo features lean on them, but this is where they're documented. When another concern (a controller, a multitenant setup, a custom card) trips over one of these, it's this skill's territory. License: **Community** — none of this needs a paid gem.
|
|
32
|
+
|
|
33
|
+
**Docs** — fetch on demand with WebFetch; prefer the raw `.md` (clean, no HTML):
|
|
34
|
+
|
|
35
|
+
- Overview: https://docs.avohq.io/4.0/internals.md
|
|
36
|
+
- Rails engines & path helpers: https://docs.avohq.io/4.0/rails-engines-paths.md
|
|
37
|
+
- Avo ❤️ Rails & Hotwire (path helpers + using your helpers): https://docs.avohq.io/4.0/rails-and-hotwire.md
|
|
38
|
+
- `Avo::Current`: https://docs.avohq.io/4.0/avo-current.md
|
|
39
|
+
- `Avo::ExecutionContext`: https://docs.avohq.io/4.0/execution-context.md
|
|
40
|
+
- Reserved model names & routes: https://docs.avohq.io/4.0/internal-model-names.md
|
|
41
|
+
- `Avo::Services::EncryptionService`: https://docs.avohq.io/4.0/encryption-service.md
|
|
42
|
+
- Docs map (find any other Avo page): https://docs.avohq.io/4.0/docs-map.md
|
|
43
|
+
|
|
44
|
+
## When this applies
|
|
45
|
+
|
|
46
|
+
Reach for this skill when a symptom or request points at the engine plumbing rather than a specific feature:
|
|
47
|
+
|
|
48
|
+
| Symptom / request | Section |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| "Link from the admin back to my main app", `undefined method 'posts_path'` inside Avo, a link resolves to the wrong page | [Path helpers](#path-helpers-main_app-vs-avo) |
|
|
51
|
+
| "Access the current user / request / params / context / tenant inside an Avo block", "set the tenant for this request" | [`Avo::Current`](#avocurrent) |
|
|
52
|
+
| "What variables are available in this block?", "why is `record` nil in my lambda?", "use `record` / `resource` / `view` in a proc" | [`Avo::ExecutionContext`](#avoexecutioncontext) |
|
|
53
|
+
| "Use my app's view helper (`link_to`, a custom formatter) inside an Avo field" | [`ExecutionContext`](#avoexecutioncontext) + [Using your app's helpers](#using-your-apps-helpers-inside-avo) |
|
|
54
|
+
| Model named `resource` / `chart` / `search` / `home` / `attachment` breaks the admin, routing conflicts, `resources :resources` clash | [Reserved names](#reserved-names) |
|
|
55
|
+
| "Encrypt a value to pass through Avo params", "decrypt what select-all sent" | [`EncryptionService`](#encryptionservice) |
|
|
56
|
+
|
|
57
|
+
## Path helpers (`main_app` vs `avo`)
|
|
58
|
+
|
|
59
|
+
Rails engines have **isolated routes**. Code running inside Avo resolves path helpers in Avo's route set by default, so a bare `posts_path` either raises `undefined method` or — worse, silently — resolves to a different route than you meant. Always prefix:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
# Avo's own routes (resources, tools, dashboards, media library, ...)
|
|
63
|
+
avo.root_path
|
|
64
|
+
avo.resources_users_path
|
|
65
|
+
avo.resources_user_path(user)
|
|
66
|
+
avo.resource_path(resource: UserResource, record: @user)
|
|
67
|
+
|
|
68
|
+
# Your host application's routes
|
|
69
|
+
main_app.root_path
|
|
70
|
+
main_app.posts_path
|
|
71
|
+
main_app.post_path(record)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This applies anywhere your code runs inside the engine: `Avo::ResourcesController` / `Avo::ToolsController` overrides, breadcrumbs configured in `config/initializers/avo.rb`, fields, actions, cards, custom tools, ejected partials.
|
|
75
|
+
|
|
76
|
+
| You want to link to… | Prefix |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| Avo pages (resources, tools, dashboards) | `avo.` |
|
|
79
|
+
| Your main application routes | `main_app.` |
|
|
80
|
+
|
|
81
|
+
Inside an `ExecutionContext` block (fields, actions, most DSL lambdas) `main_app` and `avo` are already in scope as accessors — you can write `main_app.post_path(record)` directly without a receiver. See below.
|
|
82
|
+
|
|
83
|
+
## `Avo::Current`
|
|
84
|
+
|
|
85
|
+
`Avo::Current` is Avo's request-scoped state, built on [`ActiveSupport::CurrentAttributes`](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html). Avo populates it at the start of each request; read it from anywhere in your custom Ruby.
|
|
86
|
+
|
|
87
|
+
| Attribute | What it is |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `Avo::Current.user` | The authenticated user — whatever your `current_user_method` returns. |
|
|
90
|
+
| `Avo::Current.params` | Equivalent to `request.params` (falls back to `{}` when there's no rack input). |
|
|
91
|
+
| `Avo::Current.request` | The Rails `request` (an empty `ActionDispatch::Request` if none). |
|
|
92
|
+
| `Avo::Current.context` | The `context` hash you configured in the initializer, evaluated in `Avo::ApplicationController`. |
|
|
93
|
+
| `Avo::Current.view_context` | An `ActionView` context — call any helper/variable available in your partials, e.g. `Avo::Current.view_context.link_to "Avo", "https://avohq.io"`. |
|
|
94
|
+
| `Avo::Current.locale` | The app locale for the request. |
|
|
95
|
+
| `Avo::Current.tenant` / `Avo::Current.tenant_id` | **Writable.** Avo leaves these for you to set — assign the current tenant early (e.g. in an `Avo::ApplicationController` override) and read it back later in the request. |
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
# Set the tenant for the current admin request (e.g. in a controller concern)
|
|
99
|
+
Avo::Current.tenant = current_account
|
|
100
|
+
Avo::Current.tenant_id = current_account.id
|
|
101
|
+
|
|
102
|
+
# Read the current user anywhere in a resource/field/action
|
|
103
|
+
Avo::Current.user
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For a full multitenancy setup (scoping records, switching tenants, per-tenant menus) use the **avo-multitenancy** skill — `Avo::Current.tenant` is the primitive it builds on.
|
|
107
|
+
|
|
108
|
+
## `Avo::ExecutionContext`
|
|
109
|
+
|
|
110
|
+
Most Avo DSL blocks — computed fields, `visible`/`hide` lambdas, action `handle` context, dynamic options — don't run in a vacuum. Avo wraps them in an `Avo::ExecutionContext`: an object that holds request state and `instance_exec`s your block against it, so the right variables are simply *in scope*.
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
field :full_name, as: :text do
|
|
114
|
+
"#{record.first_name} #{record.last_name}" # `record` is provided by the ExecutionContext
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Always in scope** (defaulted from `Avo::Current`): `context`, `current_user`, `params`, `request`, `view_context`, `locale`, `main_app`, `avo`.
|
|
119
|
+
|
|
120
|
+
**Provided per-block**, depending on what's being evaluated: `record`, `resource`, `view`, and any other variable Avo (or you) passes in. This is why `record` exists in a field block but may be `nil` or absent in a block that isn't tied to a record — the variable is only there if that call site passed it.
|
|
121
|
+
|
|
122
|
+
Two behaviors worth knowing:
|
|
123
|
+
|
|
124
|
+
- **`delegate_missing_to :view_context`.** Any method the block calls that isn't a known accessor is forwarded to `view_context`. That means view helpers — `link_to`, `content_tag`, `image_tag`, and your app's own view helpers — are callable **directly** inside the block, no receiver needed.
|
|
125
|
+
- **Only callables are executed.** `ExecutionContext` runs the target only if it `respond_to?(:call)`; a non-callable value (a plain string, symbol, boolean) is returned as-is. So `visible: false` and `visible: -> { view.show? }` both work — the lambda gets the context, the literal doesn't.
|
|
126
|
+
|
|
127
|
+
You can drive it yourself when needed:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
Avo::ExecutionContext.new(
|
|
131
|
+
target: -> { "#{record.first_name} #{record.last_name}" },
|
|
132
|
+
record: User.first,
|
|
133
|
+
view: :index
|
|
134
|
+
).handle
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### `include:` — mix in modules
|
|
138
|
+
|
|
139
|
+
Pass `include:` an array of modules to make their methods available inside the block:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
Avo::ExecutionContext.new(
|
|
143
|
+
target: -> { sanitize "<script>alert('be careful')</script>#{record.name}" },
|
|
144
|
+
record: record,
|
|
145
|
+
include: [ActionView::Helpers::SanitizeHelper]
|
|
146
|
+
).handle
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Using your app's helpers inside Avo
|
|
150
|
+
|
|
151
|
+
Three ways, from most to least direct:
|
|
152
|
+
|
|
153
|
+
1. **View helpers, directly.** Because of `delegate_missing_to :view_context`, anything defined in `app/helpers` (or standard view helpers) is callable straight from an `ExecutionContext` block:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
field :name, as: :text, format_using: -> { link_to value, main_app.post_path(record) }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
2. **The `helpers` object.** Inside an `ExecutionContext` block, `helpers` returns an object with every module from `app/helpers` mixed in — handy when you want an explicit receiver:
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
# app/helpers/products_helper.rb defines `simple_name`
|
|
163
|
+
field :name, as: :text, format_using: -> { helpers.simple_name(value) }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
3. **`view_context.controller`, from outside a block** (e.g. plain resource code or a controller). Include the helper module in the Avo controller, then reach it through the controller:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
# app/controllers/avo/products_controller.rb
|
|
170
|
+
class Avo::ProductsController < Avo::ResourcesController
|
|
171
|
+
include ApplicationHelper
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# app/avo/resources/product.rb
|
|
175
|
+
field :copyright, as: :text do
|
|
176
|
+
view_context.controller.render_copyright_info
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
For controller overrides in general, see the **avo-controllers** skill.
|
|
181
|
+
|
|
182
|
+
## Reserved names
|
|
183
|
+
|
|
184
|
+
Avo dynamically maps models to internal controllers and routes, so a handful of model names **collide** with Avo's own controllers and will override built-in functionality or break routing. Avoid these model names:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
action appearance_settings application array association attachment
|
|
188
|
+
base base_application chart debug home media_library
|
|
189
|
+
private resource search
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
If you don't already have such a model, rename it (`user_resource` instead of `resource`, `advanced_search` instead of `search`, `graph` instead of `chart`).
|
|
193
|
+
|
|
194
|
+
**Keep the model, rename the Avo resource** — when the model already exists and you can't rename it, generate the Avo resource under a different name but point it at the real model class:
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
bin/rails generate avo:resource user_resource --model-class resource
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Generates `Avo::Resources::UserResource` and `Avo::UserResourcesController`, both backed by the existing `Resource` model — no collision.
|
|
201
|
+
|
|
202
|
+
**Route helper clash with `resources :resources`.** A host-app route like `resources :resources` defines a `resources_path` helper that **overrides Avo's internal `resources_path`** and can break parts of the admin. Keep the URL, rename the helper:
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
resources :resources, as: 'articles' # URL stays /resources, helper becomes articles_path
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## `EncryptionService`
|
|
209
|
+
|
|
210
|
+
`Avo::Services::EncryptionService` encrypts and decrypts values so they can travel safely through params. Avo uses it internally (e.g. select-all serializes the query, encrypted, into the URL), and you can call it anywhere.
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
# Strings
|
|
214
|
+
token = Avo::Services::EncryptionService.encrypt(message: "Secret string", purpose: :demo)
|
|
215
|
+
Avo::Services::EncryptionService.decrypt(message: token, purpose: :demo)
|
|
216
|
+
# => "Secret string"
|
|
217
|
+
|
|
218
|
+
# Objects — pass a serializer (Marshal handles ActiveRecord objects)
|
|
219
|
+
token = Avo::Services::EncryptionService.encrypt(message: Course::Link.first, purpose: :demo, serializer: Marshal)
|
|
220
|
+
Avo::Services::EncryptionService.decrypt(message: token, purpose: :demo, serializer: Marshal)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
- `message:` (required) — the object to encrypt.
|
|
224
|
+
- `purpose:` (required) — any symbol; it **must match** between `encrypt` and `decrypt` or verification fails.
|
|
225
|
+
- Extra kwargs (e.g. `serializer:`) pass straight through to [`ActiveSupport::MessageEncryptor`](https://api.rubyonrails.org/classes/ActiveSupport/MessageEncryptor.html); use `serializer: Marshal` for non-string objects.
|
|
226
|
+
- It derives the key from `Rails.application.secret_key_base`, so a `secret_key_base` must be defined — via `ENV["SECRET_KEY_BASE"]`, `Rails.application.credentials.secret_key_base`, or `Rails.application.secrets.secret_key_base`.
|
|
227
|
+
|
|
228
|
+
## Gotchas
|
|
229
|
+
|
|
230
|
+
- **Missing `avo.` / `main_app.` prefix is the #1 "extending Avo" footgun.** A bare path helper inside the engine raises `undefined method` — or, when both route sets define the same name, silently resolves to the *wrong* route with no error. Always prefix.
|
|
231
|
+
- **Reserved model names collide with Avo's controllers.** A model named `resource`, `chart`, `search`, `home`, `attachment`, `association`, etc. overrides Avo's internal controllers/routes. Keep the model via `--model-class` (renaming only the Avo resource), and rename a conflicting `resources :resources` route helper with `as:`.
|
|
232
|
+
- **`record` (or any per-block variable) only exists if the call site passed it.** `ExecutionContext` accessors like `context`/`current_user`/`request` default from `Avo::Current`, but `record`/`resource`/`view` are supplied per block. Referencing one where it wasn't passed gives `nil` (or `NameError`) — that's not a bug, it's the wrong block.
|
|
233
|
+
- **`ExecutionContext` runs the target only if it `respond_to?(:call)`.** A non-callable value is returned untouched. If you expected your literal to be "evaluated," it wasn't — wrap it in a lambda.
|
|
234
|
+
- **Delegation is to `view_context`, not `controller`.** Missing methods on an `ExecutionContext` go to `view_context`. To reach controller-level helpers you included in an Avo controller, go through `view_context.controller` explicitly.
|
|
235
|
+
- **`EncryptionService` needs matching `purpose:` and a `secret_key_base`.** A mismatched `purpose:` between encrypt and decrypt fails verification; a missing `secret_key_base` raises. Use `serializer: Marshal` for anything that isn't a plain string.
|
|
236
|
+
|
|
237
|
+
## Report
|
|
238
|
+
|
|
239
|
+
When done, tell the user:
|
|
240
|
+
|
|
241
|
+
- Which primitive you used and where (file + line): a path-helper prefix, an `Avo::Current` read/write, an `ExecutionContext` block, a reserved-name fix, or an `EncryptionService` call.
|
|
242
|
+
- For path helpers: which prefix (`avo.` / `main_app.`) and why that route set.
|
|
243
|
+
- For reserved names: the collision, and whether you renamed the model, used `--model-class`, and/or added `as:` to a route.
|
|
244
|
+
- For `EncryptionService`: the `purpose:` used and whether a `serializer:` was needed — and remind them encrypt/decrypt must share both.
|
|
245
|
+
- Any follow-up the user still owns: defining `secret_key_base`, wiring tenant scoping (**avo-multitenancy**), or a controller include (**avo-controllers**). Cross-link **avo-multitenancy**, **avo-controllers**, and **avo-custom-ui** when the work spills into their territory.
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: avo-fields
|
|
3
|
+
description: Add or change fields in an Avo resource's `def fields` — pick the `as:` type, set options (required, default, help, visibility, formatting), and use computed and view-specific fields. Use when the user wants to add a field to an Avo resource, or — said without naming Avo — "add a status field to the Project model", "make the email field required", "show the user's avatar", "add a dropdown for order status", "the price should display as currency", "add a rich-text editor for the body", "hide the notes field on the index page", "add a star rating", "make the name column sortable", "show a badge for the order state", or "add a color picker / date picker / progress bar to a model". For belongs_to / has_many / has_one and other association fields, use the avo-associations skill instead.
|
|
4
|
+
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch
|
|
5
|
+
metadata:
|
|
6
|
+
requires-gem: none — Community; some field types need companion gems (see Gotchas)
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
> **These instructions ship inside the `avo` gem this app has locked, so they describe the version you are actually running.** Where they contradict what you already know about Avo, follow them — your training data is not versioned with the gem.
|
|
10
|
+
|
|
11
|
+
# Add or change an Avo field
|
|
12
|
+
|
|
13
|
+
In Avo (a Rails admin framework), the columns shown on a resource's Index, Show, New, and Edit pages are declared as **fields** inside the `def fields` method of `app/avo/resources/<model>.rb`. Almost every "add/change/hide a field on the X model" request lands in that one method.
|
|
14
|
+
|
|
15
|
+
A field looks like:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
field :column_name, as: :field_type, **options
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- `:column_name` — a database column **or** any method/attribute on the model.
|
|
22
|
+
- `as: :field_type` — how it renders (`:text`, `:select`, `:boolean`, `:money`, …). Omit `as:` and it defaults to `:text`.
|
|
23
|
+
- `**options` — label, visibility, formatting, validation cues, etc. (see [Key options](#key-options)).
|
|
24
|
+
|
|
25
|
+
**Docs** (fetch on demand — do not rely on memory for exact option names):
|
|
26
|
+
- Docs map / index: https://docs.avohq.io/4.0/docs-map.md
|
|
27
|
+
- Declaring fields & view-specific methods: https://docs.avohq.io/4.0/fields.md
|
|
28
|
+
- Common options (guide + full API): https://docs.avohq.io/4.0/field-options.md · https://docs.avohq.io/4.0/field-options-api.md
|
|
29
|
+
- The ~38-type catalog (one page per type, with type-specific options): https://docs.avohq.io/4.0/fields/text.md — swap `text` for any type below. Fetch the specific type's page before using its niche options.
|
|
30
|
+
- Layout DSL inside `def fields` (panels, tabs, sidebar, cards, columns): https://docs.avohq.io/4.0/fields-layout.md
|
|
31
|
+
- Field discovery (auto-map columns/associations): https://docs.avohq.io/4.0/field-discovery.md
|
|
32
|
+
- HTML attributes on a field: https://docs.avohq.io/4.0/html.md
|
|
33
|
+
|
|
34
|
+
## When this applies
|
|
35
|
+
|
|
36
|
+
Use this skill for anything **inside `def fields`** except associations:
|
|
37
|
+
|
|
38
|
+
- Adding, removing, renaming, or reordering a field.
|
|
39
|
+
- Changing a field's type (e.g. plain text → a `select` dropdown or a `badge`).
|
|
40
|
+
- Setting options: required, default, help text, placeholder, disabled/readonly, sortable, visibility per view, formatting, copyable, etc.
|
|
41
|
+
- Computed (block) fields that derive a value not stored in a column.
|
|
42
|
+
- Arranging fields with the layout DSL (`panel`, `card`, `sidebar`, `tabs`/`tab`, `header`, column `width`).
|
|
43
|
+
- Auto-generating fields with `discover_columns` / `discover_associations`.
|
|
44
|
+
|
|
45
|
+
**Not this skill:**
|
|
46
|
+
- `belongs_to`, `has_many`, `has_one`, `has_and_belongs_to_many` and other relationship fields → **avo-associations** skill.
|
|
47
|
+
- Authoring a brand-new *custom field type* (a reusable component you invent) → **avo-custom-fields** skill. Note `rails g avo:field NAME` scaffolds a new custom type; it is **not** how you add a normal field — for that you just edit `def fields`.
|
|
48
|
+
|
|
49
|
+
## Workflow
|
|
50
|
+
|
|
51
|
+
1. **Find the resource file.** It's `app/avo/resources/<model>.rb` (singular, snake_case — e.g. the `Project` model → `app/avo/resources/project.rb`). If unsure, `Glob` `app/avo/resources/*.rb` or `Grep` for the model name. Read the file and locate `def fields` (or the view-specific methods below).
|
|
52
|
+
|
|
53
|
+
2. **Confirm the backing attribute.** The first argument should match a real DB column or a model method. Check `db/schema.rb` (or the model) so you use the right column name and can pick a type that fits its data type. If the value isn't stored anywhere, it must be a [computed block field](#computed-block-fields).
|
|
54
|
+
|
|
55
|
+
3. **Pick the field type** using the [decision table](#choosing-a-field-type). When in doubt, fetch that type's docs page.
|
|
56
|
+
|
|
57
|
+
4. **Check for a gem gate.** A few types need a companion gem or ENV var and will error without it — see [Gotchas](#gotchas). If the user asks for one of those, tell them the gem to add.
|
|
58
|
+
|
|
59
|
+
5. **Place the field** in the intended view(s). By default a field shows on all four views; use `def fields` for all views, or a view-specific method (below) when a view needs a different set. Respect existing ordering and formatting in the file.
|
|
60
|
+
|
|
61
|
+
6. **Add options** — start minimal (`as:` + label if needed), then layer on only what the request asks for.
|
|
62
|
+
|
|
63
|
+
7. **Report** what changed (see [Report](#report)). Don't run the app; if you want to sanity-check syntax, a `ruby -c` on the file is enough.
|
|
64
|
+
|
|
65
|
+
### Which method to edit
|
|
66
|
+
|
|
67
|
+
`def fields` is the catch-all used for every view when no more specific method exists. Override per view or per view-group only when a view genuinely needs a different set:
|
|
68
|
+
|
|
69
|
+
| Method | Applies to |
|
|
70
|
+
| ----------------- | --------------------------------- |
|
|
71
|
+
| `def fields` | any view with no specific method |
|
|
72
|
+
| `def index_fields`| Index |
|
|
73
|
+
| `def show_fields` | Show |
|
|
74
|
+
| `def edit_fields` | Edit **and** Update |
|
|
75
|
+
| `def new_fields` | New **and** Create |
|
|
76
|
+
| `def display_fields` | Index **and** Show |
|
|
77
|
+
| `def form_fields` | New, Create, Edit **and** Update |
|
|
78
|
+
|
|
79
|
+
Specific view methods beat view-group methods, which beat `fields`. Prefer a single `def fields` + per-field `hide_on:`/`only_on:` for small differences; reach for separate methods only when the field lists really diverge.
|
|
80
|
+
|
|
81
|
+
## Choosing a field type
|
|
82
|
+
|
|
83
|
+
Map the need to an `as:` type. All built-in types are **Community (free)**; the ⚠️ ones need a companion gem (see [Gotchas](#gotchas)).
|
|
84
|
+
|
|
85
|
+
| The user wants… | Use | Example |
|
|
86
|
+
| ------------------------------------------------------ | ---------------------------- | ------- |
|
|
87
|
+
| A short single-line string | `:text` | `field :title, as: :text` |
|
|
88
|
+
| A long / multi-line string | `:textarea` | `field :body, as: :textarea` |
|
|
89
|
+
| A number | `:number` | `field :age, as: :number` |
|
|
90
|
+
| A masked password input | `:password` | `field :password, as: :password` |
|
|
91
|
+
| A yes/no checkbox | `:boolean` | `field :active, as: :boolean` |
|
|
92
|
+
| A hash of on/off toggles | `:boolean_group` | `field :roles, as: :boolean_group, options: {admin: "Admin", editor: "Editor"}` |
|
|
93
|
+
| A dropdown of fixed choices | `:select` | `field :type, as: :select, options: {Draft: :draft, Live: :live}` |
|
|
94
|
+
| Multiple choices from a fixed set | `:select` (`multiple: true`) or `:checkbox_list` | `field :tags, as: :select, multiple: true, options: {...}` |
|
|
95
|
+
| Radio buttons | `:radio` | `field :plan, as: :radio, options: {...}` |
|
|
96
|
+
| A country picker | `:country` | `field :country, as: :country` |
|
|
97
|
+
| A date | `:date` | `field :birthday, as: :date` |
|
|
98
|
+
| A date **and** time | `:date_time` | `field :published_at, as: :date_time` |
|
|
99
|
+
| A time only | `:time` | `field :opens_at, as: :time` |
|
|
100
|
+
| A colored status pill (map value → color) | `:badge` | `field :status, as: :badge, options: {success: "done", warning: "pending"}` |
|
|
101
|
+
| A live status dot (loading/failed/success/neutral) | `:status` | `field :state, as: :status, success_when: [:done], loading_when: [:running], failed_when: [:failed]` |
|
|
102
|
+
| A tag input / list | `:tags` | `field :skills, as: :tags` |
|
|
103
|
+
| A star rating | `:stars` | `field :rating, as: :stars` |
|
|
104
|
+
| A progress bar (slider on forms) | `:progress_bar` | `field :progress, as: :progress_bar` |
|
|
105
|
+
| Editable flat JSON key/value pairs | `:key_value` | `field :meta, as: :key_value` |
|
|
106
|
+
| Structured array data | `:array` | `field :items, as: :array` |
|
|
107
|
+
| A code editor (syntax-highlighted) | `:code` | `field :snippet, as: :code, language: "javascript"` |
|
|
108
|
+
| The record id | `:id` | `field :id, as: :id` |
|
|
109
|
+
| A hidden form value | `:hidden` | `field :token, as: :hidden` |
|
|
110
|
+
| A section title between fields | `:heading` | `field :section, as: :heading` (layout, not a DB column) |
|
|
111
|
+
| A hover preview icon on the Index row | `:preview` | `field :preview, as: :preview` |
|
|
112
|
+
| A currency amount ⚠️ | `:money` | `field :price, as: :money, currencies: %w[USD EUR]` |
|
|
113
|
+
| A rich-text WYSIWYG editor ⚠️ (recommended) | `:rhino` | `field :body, as: :rhino` |
|
|
114
|
+
| A GitHub-style Markdown editor ⚠️ | `:markdown` | `field :body, as: :markdown` |
|
|
115
|
+
| A simpler Markdown editor ⚠️ | `:easy_mde` | `field :notes, as: :easy_mde` |
|
|
116
|
+
| The Trix editor (ActionText) | `:trix` | `field :body, as: :trix` |
|
|
117
|
+
| A single file upload (ActiveStorage) | `:file` | `field :cv, as: :file` |
|
|
118
|
+
| Multiple file uploads | `:files` | `field :docs, as: :files` |
|
|
119
|
+
| An uploaded avatar image | `:avatar` | `field :photo, as: :avatar` |
|
|
120
|
+
| A Gravatar from an email column | `:gravatar` | `field :email, as: :gravatar` |
|
|
121
|
+
| An image from a stored URL | `:external_image` | `field :logo, as: :external_image` |
|
|
122
|
+
| A point on a map ⚠️ | `:location` | `field :coordinates, as: :location` |
|
|
123
|
+
| A geographic area on a map ⚠️ | `:area` | `field :zone, as: :area` |
|
|
124
|
+
|
|
125
|
+
For anything relationship-shaped ("show the user's posts", "attach an author") use the **avo-associations** skill.
|
|
126
|
+
|
|
127
|
+
## Key options
|
|
128
|
+
|
|
129
|
+
Every field accepts these common options (full list + types at the field-options-api page). Pass a literal value or, unless noted, a lambda.
|
|
130
|
+
|
|
131
|
+
- **`name`** — override the label (default is the humanized id). `field :is_available, as: :boolean, name: "Availability"`. For localized apps, translate via i18n instead of hardcoding.
|
|
132
|
+
- **Visibility** — `hide_on:`, `show_on:`, `only_on:`, `except_on:` take `:index`, `:show`, `:new`, `:edit`, `:preview` plus shorthands `:forms` (new+edit), `:display` (index+show), and `:all` (only for `hide_on`/`show_on`). Example: `field :notes, as: :textarea, hide_on: [:index, :show]`.
|
|
133
|
+
- **`visible:`** — a lambda for conditional display. It can see `context` and `resource`; the record is `resource.record`. On create the record is `nil`, so **use safe navigation**: `visible: -> { resource.record&.published? }`.
|
|
134
|
+
- **`required:`** — adds an asterisk (cosmetic only; Avo adds it automatically when the model has a presence validator). Real enforcement is model validation.
|
|
135
|
+
- **`disabled:`** vs **`readonly:`** — both render the input disabled on forms. `disabled:` **also ignores the value on save** (safe against DOM tampering). `readonly:` is UI-only — a user can re-enable it and submit; don't rely on it for protection.
|
|
136
|
+
- **`default:`** — pre-fills the New form (and action modals): `default: -> { Time.current }`.
|
|
137
|
+
- **`help:`** / **`label_help:`** — help text (HTML allowed) below the input (forms only) or below the label (every view).
|
|
138
|
+
- **`placeholder:`** — placeholder for empty text-like inputs.
|
|
139
|
+
- **`sortable:`** — makes the Index column sortable. `true` for real columns; a `-> { query.order(...) }` lambda (receives `query` and `direction`) for computed fields.
|
|
140
|
+
- **`format_using:`** — reshape the displayed value. **Runs on form views too**, so branch on the view when the form should stay editable: `format_using: -> { view.form? ? value : value.upcase }`. View-scoped variants exist (`format_display_using`, `format_index_using`, `format_show_using`, `format_form_using`, …) — the most specific one wins, they don't chain.
|
|
141
|
+
- **`update_using:`** — parse the raw form `value` before it's saved.
|
|
142
|
+
- **`nullable:`** / **`null_values:`** — store empty input as `NULL` (optionally define which values count as null).
|
|
143
|
+
- **`copyable:`** — clipboard icon on Show/Index. Copies the **displayed** (formatted) value.
|
|
144
|
+
- **`link_to_record:`** — make the Index cell a link to the record. Only on `:id`, `:text`, `:gravatar`, and `belongs_to`.
|
|
145
|
+
- **`for_attribute:`** — back the field with a different attribute than its id (lets you show one column two ways).
|
|
146
|
+
- **`width:`** / **`stacked:`** — column width (`25/33/50/66/75/100`; any value <100 auto-stacks) and label-above-value layout.
|
|
147
|
+
- **`html:`** — attach `style`/`classes`/`data` to the field's wrapper/label/input per view (e.g. right-align a number: `html: {index: {wrapper: {classes: "text-right"}}}`). See the html page.
|
|
148
|
+
|
|
149
|
+
### Computed (block) fields
|
|
150
|
+
|
|
151
|
+
When the value isn't a stored column, pass a block. Inside it you have `record`, `resource`, and `view`:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
field "Full name", as: :text do
|
|
155
|
+
"#{record.first_name} #{record.last_name}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
field "Has posts", as: :boolean do
|
|
159
|
+
record.posts.any?
|
|
160
|
+
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Block fields render **only on Index and Show** (they have no input) and **can't use `sortable: true`** (pass a sort lambda instead). Give them an explicit label string as the first argument.
|
|
164
|
+
|
|
165
|
+
### Layout inside `def fields`
|
|
166
|
+
|
|
167
|
+
The same method arranges fields. Use `panel`/`card` to group, `sidebar` (inside a panel) for compact fields, `tabs`/`tab` for tabbed sections, `header` to reposition the page chrome, and per-field `width:` for multi-column rows. This overlaps page-level layout — see the fields-layout docs page before building complex structures.
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
def fields
|
|
171
|
+
field :id, as: :id
|
|
172
|
+
panel title: "Details" do
|
|
173
|
+
field :first_name, as: :text, width: 50
|
|
174
|
+
field :last_name, as: :text, width: 50
|
|
175
|
+
sidebar do
|
|
176
|
+
field :active, as: :boolean, only_on: :show
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Field discovery
|
|
183
|
+
|
|
184
|
+
To auto-generate fields from the model instead of listing them, call `discover_columns` (columns, rich text, tags) and `discover_associations` (attachments, relationships). Scope with `only:`/`except:`, and pass any other keyword to forward it to every discovered field:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
def fields
|
|
188
|
+
discover_columns only: [:title, :body, :published_at]
|
|
189
|
+
discover_associations except: [:audit_logs]
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Gotchas
|
|
194
|
+
|
|
195
|
+
- **Gem-gated types break without their companion gem** (the field silently fails to render or raises). If the user asks for one, tell them to add the gem:
|
|
196
|
+
- `:rhino` → `gem "avo-rhino_field"`
|
|
197
|
+
- `:markdown` → `gem "marksmith"` + `gem "commonmarker"`
|
|
198
|
+
- `:money` → `gem "avo-money_field"` + `gem "money-rails", "~> 1.12"` (and `monetize :price_cents` on the model)
|
|
199
|
+
- `:location` / `:area` → `gem "mapkick-rb"` (**not** `mapkick`) + `MAPBOX_ACCESS_TOKEN` env var
|
|
200
|
+
- Reactive fields (`react_on:`) → `gem "avo-reactive_fields"`
|
|
201
|
+
- Some of these gems live on the `packager.dev` source — point the user to the Avo 4 upgrade guide's gems section.
|
|
202
|
+
- **`tip_tap` is deprecated** — use `:rhino` for a WYSIWYG editor.
|
|
203
|
+
- **`markdown` was renamed.** The old `markdown` field is now `easy_mde`; the new `markdown` is the Marksmith editor. Don't confuse them.
|
|
204
|
+
- **`required:` and `readonly:` are cosmetic.** Enforce with model validations (`validates :x, presence: true`) and use `disabled:` when you need the value ignored on save.
|
|
205
|
+
- **`format_using:` runs on form views too** — return the raw `value` when `view.form?` so the input stays editable, or use a `format_display_using:`/`format_*_using:` variant.
|
|
206
|
+
- **Computed block fields** show only on Index/Show and can't be `sortable: true`.
|
|
207
|
+
- **`visible:` lambdas see `resource.record == nil` on create** — always safe-navigate (`resource.record&.foo`).
|
|
208
|
+
- **`select` takes exactly one of `options:`, `grouped_options:`, or `enum:`** — never combine them.
|
|
209
|
+
- **Fields an external tool submits must still be declared** on the form views. If a resource tool renders the input, keep the field with `hide_on: :forms` so Avo can parse and save the value.
|
|
210
|
+
- **`badge` vs `status`:** `badge` maps arbitrary values to colored pills (`options:`); `status` is a live indicator with `success_when:`/`loading_when:`/`failed_when:` arrays (everything else → neutral).
|
|
211
|
+
|
|
212
|
+
## Report
|
|
213
|
+
|
|
214
|
+
After editing, tell the user:
|
|
215
|
+
|
|
216
|
+
- The file and method you changed (`app/avo/resources/<model>.rb` → `def fields`).
|
|
217
|
+
- Each field added/changed: column, `as:` type, and any notable options.
|
|
218
|
+
- Any gem or ENV var they must add for a gated type, and any model change needed (validation for `required`, `monetize` for money, an enum for an enum-backed select).
|
|
219
|
+
- Which views the field now appears on, if you set visibility.
|