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,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: avo-associations
|
|
3
|
+
description: >-
|
|
4
|
+
Wire Active Record relationships into Avo resources as association fields (`field :x, as:
|
|
5
|
+
:belongs_to | :has_one | :has_many | :has_and_belongs_to_many`) inside `def fields`, including
|
|
6
|
+
polymorphic and `has_many :through` relations, type-to-search pickers, extra join-table fields
|
|
7
|
+
at attach time, and nested create-in-form. Use when the user wants to link one record to another
|
|
8
|
+
or surface related records — in Avo terms ("add a belongs_to field", "show a has_many panel",
|
|
9
|
+
"make the association searchable", "polymorphic belongs_to", "has_many through with
|
|
10
|
+
attach_fields", "use_resource / attach_scope") or in plain Rails terms ("a post belongs to a
|
|
11
|
+
user", "show all of a user's orders on their page", "connect users and teams with a join table",
|
|
12
|
+
"the user dropdown is too long — make it type-to-search", "a comment can belong to a post or a
|
|
13
|
+
project", "add extra fields when attaching a member to a team", "create the related record right
|
|
14
|
+
from the form", "link these two models in the admin").
|
|
15
|
+
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch
|
|
16
|
+
metadata:
|
|
17
|
+
requires-gem: none — Community; searchable association pickers need avo-advanced_search, which ships its own skill
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
> **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.
|
|
21
|
+
|
|
22
|
+
# Avo Associations
|
|
23
|
+
|
|
24
|
+
> **Searchable pickers live in their own gem.** Turning a long association dropdown into a type-to-search field is `avo-advanced_search`, whose skill ships inside that gem. Declaring and configuring association fields — everything on this page — is Community.
|
|
25
|
+
>
|
|
26
|
+
> Re-run the Avo skills loader before recommending a searchable picker; if the gem is absent from `Gemfile.lock`, `searchable: true` falls back to a plain select with no error.
|
|
27
|
+
|
|
28
|
+
Avo turns Rails [Active Record associations](https://guides.rubyonrails.org/association_basics.html) into fields. You declare one line in the resource's `def fields` and Avo renders it per view: a `belongs_to` becomes a link on Show/Index and a dropdown (or search picker) on the forms; `has_one`, `has_many`, and `has_and_belongs_to_many` render as panels below the record's fields with attach / detach / create controls.
|
|
29
|
+
|
|
30
|
+
These fields live in `app/avo/resources/<model>.rb`, the same place as every other field. This skill is where relationship-shaped requests land — "a post belongs to a user", "show a user's orders", "connect users and teams", "make the picker type-to-search". For non-association fields (text, number, select, file…) use `avo-fields`; for creating or configuring the resource shell itself use `avo-resources`; for who can attach/detach/create use `avo-authorization`.
|
|
31
|
+
|
|
32
|
+
**Docs** (fetch on demand with WebFetch — verify option names and edge cases against the live pages, don't guess):
|
|
33
|
+
- Docs map: https://docs.avohq.io/4.0/docs-map.md
|
|
34
|
+
- Overview & STI: https://docs.avohq.io/4.0/associations.md
|
|
35
|
+
- Belongs to: https://docs.avohq.io/4.0/associations/belongs_to.md
|
|
36
|
+
- Has one: https://docs.avohq.io/4.0/associations/has_one.md
|
|
37
|
+
- Has many (+ `:through`, `attach_fields`): https://docs.avohq.io/4.0/associations/has_many.md
|
|
38
|
+
- Has and belongs to many: https://docs.avohq.io/4.0/associations/has_and_belongs_to_many.md
|
|
39
|
+
- Searchable associations (guide): https://docs.avohq.io/4.0/associations/searchable.md
|
|
40
|
+
- Searchable associations (API): https://docs.avohq.io/4.0/associations/searchable-api.md
|
|
41
|
+
- Authorization (association buttons & policies): https://docs.avohq.io/4.0/authorization.md#associations
|
|
42
|
+
|
|
43
|
+
## When this applies
|
|
44
|
+
|
|
45
|
+
Use this skill when the request is about **one record relating to another**, in any phrasing:
|
|
46
|
+
|
|
47
|
+
- Direct Avo asks: "add a `belongs_to`", "show a `has_many` panel", "make it searchable", "polymorphic `belongs_to`", "`has_many :through` with `attach_fields`".
|
|
48
|
+
- Rails-shaped asks without Avo words: "a post belongs to a user", "a user has one profile", "show all of a user's orders on their page", "connect users and teams with a join table".
|
|
49
|
+
- Product-shaped asks: "the author dropdown is too long — let me type to search", "a comment can belong to a post *or* a project", "add a rating when I attach a member to a team", "create the related record right from the form".
|
|
50
|
+
|
|
51
|
+
If the request is about a scalar attribute (a string, a number, a file, an enum select) rather than a link between records, it's an `avo-fields` task, not this one.
|
|
52
|
+
|
|
53
|
+
## Workflow
|
|
54
|
+
|
|
55
|
+
1. **Confirm the Rails association exists.** Association fields are a *view* of an Active Record association — Avo does not create it. Check the model (`app/models/<model>.rb`) for the matching `belongs_to` / `has_one` / `has_many` / `has_and_belongs_to_many` (and the inverse on the other model). If it's missing, add it to the model first, and set `inverse_of` on both sides.
|
|
56
|
+
|
|
57
|
+
2. **Pick the field type** from the [Association types](#association-types) table — it mirrors the Rails macro one-to-one.
|
|
58
|
+
|
|
59
|
+
3. **Add the field** to `def fields` in `app/avo/resources/<model>.rb`:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
# app/avo/resources/post.rb
|
|
63
|
+
class Avo::Resources::Post < Avo::BaseResource
|
|
64
|
+
def fields
|
|
65
|
+
field :id, as: :id
|
|
66
|
+
field :title, as: :text
|
|
67
|
+
|
|
68
|
+
field :user, as: :belongs_to # Post belongs_to :user
|
|
69
|
+
field :comments, as: :has_many # Post has_many :comments
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The field name is the **association name**, not a column — `field :user` maps to `belongs_to :user` (the `user_id` column is implicit). A `has_many` field's counterpart lives on the *other* resource: `Post has_many :comments` is `field :comments, as: :has_many` on the Post resource, and the reciprocal `field :post, as: :belongs_to` on the Comment resource.
|
|
75
|
+
|
|
76
|
+
4. **Layer options** as needed (see [Key options](#key-options)) — scope the attach list, point at a different resource, make it searchable, allow nested creation, etc.
|
|
77
|
+
|
|
78
|
+
5. **Verify buttons follow the target resource's policy.** Attach/detach/create/destroy visibility comes from the *target* resource's Pundit policy, using **plural** method names (see [Gotchas](#gotchas)). If a button must appear or disappear, that's an `avo-authorization` change, not an option here.
|
|
79
|
+
|
|
80
|
+
## Association types
|
|
81
|
+
|
|
82
|
+
| Rails association on the model | Avo field declaration |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `belongs_to :user` | `field :user, as: :belongs_to` |
|
|
85
|
+
| `belongs_to :commentable, polymorphic: true` | `field :commentable, as: :belongs_to, polymorphic_as: :commentable, types: [::Post, ::Project]` |
|
|
86
|
+
| `has_one :admin` | `field :admin, as: :has_one` |
|
|
87
|
+
| `has_many :comments` | `field :comments, as: :has_many` |
|
|
88
|
+
| `has_many :members, through: :memberships` | `field :members, as: :has_many, through: :memberships` |
|
|
89
|
+
| `has_and_belongs_to_many :teams` | `field :teams, as: :has_and_belongs_to_many` |
|
|
90
|
+
|
|
91
|
+
`has_one`, `has_many`, and `has_and_belongs_to_many` render only on the **Show** view by default; add `show_on: :edit` to also surface them on the edit form (still rendered with the show-view component — for editable-in-form use `nested`).
|
|
92
|
+
|
|
93
|
+
## Key options
|
|
94
|
+
|
|
95
|
+
Most options are shared across the association fields; a handful are type-specific. Confirm exact behavior against the linked docs before relying on an edge case.
|
|
96
|
+
|
|
97
|
+
**Shared by all association fields**
|
|
98
|
+
|
|
99
|
+
| Option | What it does |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `searchable` | Replaces the `<select>` / attach picker with a type-as-you-search input. **Paid add-on** + needs a query source — see [Searchable](#searchable-associations). |
|
|
102
|
+
| `attach_scope` | `-> { query.non_admins }` — scopes the records offered in the **attach modal / dropdown** (locals: `query`, `parent`). Note: does *not* filter the listed rows of a `has_many`/HABTM — use `scope` or a Pundit policy scope for that. |
|
|
103
|
+
| `use_resource` | Render/redirect through a different resource, e.g. `use_resource: Avo::Resources::PhotoComment` (class or string). |
|
|
104
|
+
| `name` | Overrides the panel/label text (`has_many`, HABTM). Also achievable via [field i18n](https://docs.avohq.io/4.0/i18n.md). |
|
|
105
|
+
| `description` | Sub-title text under the panel title. Keep it cheap under `loading: :manual` — the lambda runs on the placeholder too. |
|
|
106
|
+
| `scope` | `-> { query.approved }` — scopes the **rows displayed** in a `has_many`/HABTM table (locals: `query`, `parent`, `resource`, `parent_resource`). |
|
|
107
|
+
| `loading` | `:manual` defers the frame fetch behind a **Load** button (heavy associations); `:lazy` is the default. |
|
|
108
|
+
| `linkable` | Makes the panel title open the association table on its own page. |
|
|
109
|
+
| `reloadable` | Adds a reload icon on the panel (boolean or a lambda gated on e.g. `current_user.admin?`). |
|
|
110
|
+
| `nested` | Create/edit the related record inline in the parent form. **Requires the `avo-nested` gem.** `nested: true` = `{ on: :forms }`; `limit:` caps rows on `has_many`/HABTM. |
|
|
111
|
+
| `attach_using` | `:checkbox_list` renders the attach modal as a multi-select checkbox list instead of a single-select dropdown (`has_many`, HABTM). |
|
|
112
|
+
| `discreet_pagination` | Hides pagination chrome when there's only one page. |
|
|
113
|
+
| `hide_search_input` / `hide_filter_button` | Hide the search box / filters button on the association table. |
|
|
114
|
+
| `link_to_child_resource` | STI: link rows to the child resource instead of the parent (see [Gotchas](#gotchas)). |
|
|
115
|
+
| `for_attribute` | Point a differently-named field at the same association (declare it twice with different scopes/names). |
|
|
116
|
+
|
|
117
|
+
**`belongs_to`-only**
|
|
118
|
+
|
|
119
|
+
| Option | What it does |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| `polymorphic_as:` + `types:` | Turns the field polymorphic. `polymorphic_as: :commentable, types: [::Post, ::Project]` — the two must be used together, matching a `belongs_to :commentable, polymorphic: true` on the model. Renders two dropdowns: pick the type, then the record. |
|
|
122
|
+
| `polymorphic_help` | Help text for the **type** dropdown (use `help:` for the record dropdown). |
|
|
123
|
+
| `can_create` | `true`/`false` — show/hide the "create new" link on the form. Overridden by the target resource's `create?` policy (see [Gotchas](#gotchas)). |
|
|
124
|
+
| `allow_via_detaching` | Keeps the field editable when you reached the record *through* that association (which otherwise disables it). |
|
|
125
|
+
| `link_to_record` | `true` makes the Index cell link to the current row's record instead of the associated one. |
|
|
126
|
+
|
|
127
|
+
### `has_many :through` and `attach_fields`
|
|
128
|
+
|
|
129
|
+
For a join model with its own columns, expose those columns **at attach time** with `attach_fields` (a lambda that declares fields, evaluated against the join model):
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
# Team has_many :members, through: :memberships — Membership has a `role` column
|
|
133
|
+
field :members,
|
|
134
|
+
as: :has_many,
|
|
135
|
+
through: :memberships,
|
|
136
|
+
attach_fields: -> {
|
|
137
|
+
field :role, as: :text
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The extra fields render inside the attach modal and their values persist on the join row. `attach_fields` **only persists on `has_many :through`** — it exists but is a no-op on a plain `has_many` / HABTM. If the through model is polymorphic, add the type as a hidden field: `field :membership_type, as: :hidden, default: "TheType"`.
|
|
142
|
+
|
|
143
|
+
### Searchable associations
|
|
144
|
+
|
|
145
|
+
Turning a long `belongs_to` / `has_many` picker into a type-to-search field is the `avo-advanced_search` add-on, and its instructions ship inside that gem — read the **avo-advanced-search** skill from the path the loader prints. Without the gem `searchable: true` falls back to a plain select with no error, so check before recommending it.
|
|
146
|
+
|
|
147
|
+
## Gotchas
|
|
148
|
+
|
|
149
|
+
- **Set `inverse_of` on the model association.** Avo relies on it to resolve the reciprocal; missing it causes wrong/empty attach lists and save bugs. Set it on both sides.
|
|
150
|
+
- **The Rails association must exist first.** Adding `field :x, as: :has_many` does nothing if the model has no `has_many :x`.
|
|
151
|
+
- **`has_*` fields are hidden on Edit by default.** Add `show_on: :edit` to surface them on the form. For editing the related record *in* the form (not just displaying it), use `nested` — which needs the `avo-nested` gem.
|
|
152
|
+
- **Attach/detach/create/destroy buttons come from the *target* resource's Pundit policy**, and the method names are **plural, matching the association name**: `attach_users?`, `detach_users?`, `create_users?`, `destroy_users?`, `view_users?`, `show_users?` — *not* the singular `detach_user?`. This is the #1 "why isn't my button showing" cause. Cross-link `avo-authorization`.
|
|
153
|
+
- **`can_create: true` is still vetoed by the policy.** If the target resource's `create?` returns `false`, no create link appears regardless of `can_create`.
|
|
154
|
+
- **A `belongs_to` field is disabled when you arrive through that same association.** Editing a Comment reached via a Post's comments panel disables the `post` field. Set `allow_via_detaching: true` to re-enable it.
|
|
155
|
+
- **`polymorphic_as` and `types` are a pair** — using one without the other raises. `types` is an array of model classes (`[::Post, ::Project]`), matching a `polymorphic: true` `belongs_to`.
|
|
156
|
+
- **`attach_scope` does not filter listed rows**, only the attach picker. To hide rows from a `has_many`/HABTM table, use `scope` or a Pundit policy scope.
|
|
157
|
+
- **STI child resources must set `self.model_class`.** When a parent class and STI children share a table, give each child resource `self.model_class = "SuperUser"` so Avo maps it correctly. To route association rows / index clicks to the child resource instead of the parent, use `self.link_to_child_resource = true` on the parent resource or `link_to_child_resource:` on the field.
|
|
158
|
+
- **Searchable can look "broken" but be misconfigured** — no query source means an empty picker, not an error. And it's a paid add-on.
|
|
159
|
+
|
|
160
|
+
## Report
|
|
161
|
+
|
|
162
|
+
After wiring the association, tell the user:
|
|
163
|
+
|
|
164
|
+
- The field(s) added and to which resource file (absolute path), with the `as:` type.
|
|
165
|
+
- Any **model** change you made or that's still required (the Rails association, `inverse_of`), since Avo only *renders* the association.
|
|
166
|
+
- Options applied and why (searchable, scope, nested, `attach_fields`, polymorphic, etc.).
|
|
167
|
+
- Any **paid add-on** dependency introduced (`searchable` → advanced-search; `nested` → `avo-nested` gem) and whether it still needs installing / a query source.
|
|
168
|
+
- If button visibility matters, a pointer that attach/detach/create/destroy are governed by the target resource's **plural** policy methods — hand off to `avo-authorization` if changes are needed.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: avo-authentication
|
|
3
|
+
description: Tell Avo who the current user is, gate access to the admin, and wire up roles, the sidebar profile widget, and the sign-out link — all in config/initializers/avo.rb and routes.rb. Use when the user wants to set config.current_user_method, integrate Devise or the Rails 8 auth scaffold, restrict Avo with authenticate_with or a route-level `authenticate :user`, add lightweight is_admin?/is_developer? roles, or fix the profile/sign-out UI — including Rails-shaped requests with no mention of Avo like "put the admin behind login", "require login to see the admin", "only let admins into the admin panel", "the admin shows 'Avo user' instead of my name", "add a logout button to the admin", "show the signed-in user's avatar in the sidebar", or "the admin doesn't know who's logged in".
|
|
4
|
+
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch
|
|
5
|
+
metadata:
|
|
6
|
+
requires-gem: none — Community
|
|
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
|
+
# Avo Authentication
|
|
12
|
+
|
|
13
|
+
Authentication in Avo answers one question: **who is the current user, and are they allowed in the door?** It is two separate jobs that people constantly conflate — (1) telling Avo how to fetch the signed-in user (`current_user_method`), and (2) restricting who can reach the admin at all (`authenticate :user` in the router, or `authenticate_with` in the initializer). Avo makes no assumptions about your auth stack: Devise, the Rails 8 scaffold, a homegrown session — any of them work once you point Avo at the right method. Almost everything here lives in `config/initializers/avo.rb`, with the door-gating half in `config/routes.rb`.
|
|
14
|
+
|
|
15
|
+
**Setting the current user is a prerequisite for everything else** — the profile widget, the sign-out link, roles, and (most importantly) **authorization policies**. A `current_user` that returns `nil` is the single most common reason Pundit/policy rules silently misbehave. If the user is doing authorization work, do this first. Authentication is "who gets in the door"; **avo-authorization** is "what they can do once inside" — this skill is only the former.
|
|
16
|
+
|
|
17
|
+
## Docs
|
|
18
|
+
|
|
19
|
+
Authoritative docs — fetch on demand rather than guessing, and verify every config name against the docs or the app's installed Avo source before writing it:
|
|
20
|
+
|
|
21
|
+
- Docs map (start here to discover pages): https://docs.avohq.io/4.0/docs-map.md
|
|
22
|
+
- Authentication guide: https://docs.avohq.io/4.0/authentication.md
|
|
23
|
+
- Rails 8 auth scaffold guide: https://docs.avohq.io/4.0/guides/rails-authentication-scaffold.md
|
|
24
|
+
- Authorization (the sibling concern): https://docs.avohq.io/4.0/authorization.md
|
|
25
|
+
|
|
26
|
+
License: **Community** (built into every Avo install — no paid add-on required).
|
|
27
|
+
|
|
28
|
+
## When this applies
|
|
29
|
+
|
|
30
|
+
**Explicit (Avo named):** "set `config.current_user_method`", "hook Avo up to Devise", "get Avo working with the Rails 8 authentication scaffold", "restrict Avo to admins", "use `authenticate_with`", "add `is_admin?` / `is_developer?` roles", "customize the Avo sign-out path", "fix the Avo profile widget".
|
|
31
|
+
|
|
32
|
+
**Implicit (Rails-shaped, no mention of Avo):** "put the admin behind login", "require login to see the admin", "only let admins into the admin panel", "the admin shows 'Avo user' instead of my name", "the admin doesn't know who's logged in", "add a logout button to the admin", "show the signed-in user's avatar in the sidebar", "my admin panel is wide open to anyone", "policies aren't being enforced in the admin" (usually a missing `current_user` — start here, then hand to avo-authorization).
|
|
33
|
+
|
|
34
|
+
## Workflow
|
|
35
|
+
|
|
36
|
+
Read `config/initializers/avo.rb` first to see what's already configured, then apply only what's missing. Identify the app's auth stack (grep the Gemfile / `ApplicationController`): Devise, the Rails 8 scaffold (`Current.user`, an `Authentication` concern), or something custom.
|
|
37
|
+
|
|
38
|
+
### 1. Tell Avo who `current_user` is
|
|
39
|
+
|
|
40
|
+
Avo does **not** guess your auth provider — out of the box `current_user` returns `nil`. Set `config.current_user_method` to a method name (a Symbol) or a block.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
# config/initializers/avo.rb
|
|
44
|
+
Avo.configure do |config|
|
|
45
|
+
# Devise: the method is literally current_user
|
|
46
|
+
config.current_user_method = :current_user
|
|
47
|
+
|
|
48
|
+
# Another provider (e.g. a current_admin helper):
|
|
49
|
+
# config.current_user_method = :current_admin
|
|
50
|
+
|
|
51
|
+
# Or a block — needed when the user comes off a global like the
|
|
52
|
+
# Rails 8 scaffold's Current.user, not a controller helper method:
|
|
53
|
+
# config.current_user_method do
|
|
54
|
+
# Current.user
|
|
55
|
+
# end
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- **Devise** → `:current_user` (or `:current_admin`, matching your Devise model).
|
|
60
|
+
- **Custom helper method** → the Symbol name of that method.
|
|
61
|
+
- **A thread-local / global** (Rails 8 scaffold, `Current` attributes) → the block form returning `Current.user`.
|
|
62
|
+
|
|
63
|
+
### 2. Gate the door — who is allowed to reach Avo
|
|
64
|
+
|
|
65
|
+
Setting `current_user` identifies the user but does **not** keep anyone out. Two ways to actually restrict access:
|
|
66
|
+
|
|
67
|
+
**A. Route-level (preferred with Devise).** Wrap the mount in Devise's `authenticate` in `config/routes.rb`. Nothing inside is even routable unless the user is signed in:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# config/routes.rb — require any signed-in user
|
|
71
|
+
authenticate :user do
|
|
72
|
+
mount_avo
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Only a subset of users (e.g. admins). The lambda receives the user:
|
|
76
|
+
authenticate :user, ->(user) { user.admin? } do
|
|
77
|
+
mount_avo
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**B. Initializer-level via `authenticate_with`.** A block run as a `before_action` inside Avo's controller — use it when you're not on Devise or want the check in the initializer:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
# config/initializers/avo.rb
|
|
85
|
+
Avo.configure do |config|
|
|
86
|
+
config.authenticate_with do
|
|
87
|
+
# Runs inside Avo's ApplicationController — see the gotcha below.
|
|
88
|
+
redirect_to main_app.root_path unless session[:user_id] == 1
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
⚠️ **`authenticate_with` runs inside Avo's own `ApplicationController`, which does NOT inherit from your app's `ApplicationController`.** Your app's helper methods, concerns, and `before_action`s are **not** available in this block — calling `authenticate_admin_user!` or a custom helper will raise `NoMethodError`. Write the check inline against primitives that *are* available (`session`, `request`, `params`, `main_app` URL helpers). To share real app logic instead of inlining it, extend Avo's base controller — that's the **avo-controllers** skill.
|
|
94
|
+
|
|
95
|
+
### 3. Rails 8 authentication scaffold — it counts as "custom auth"
|
|
96
|
+
|
|
97
|
+
The Rails 8 `bin/rails generate authentication` scaffold is **not** plug-and-play; Avo treats it as custom auth and it needs three explicit steps (full walkthrough in the scaffold guide above):
|
|
98
|
+
|
|
99
|
+
1. `config.current_user_method { Current.user }` — the scaffold stores the user on `Current.user`.
|
|
100
|
+
2. `config.sign_out_path_name = :session_path` — the scaffold signs out via `SessionsController`.
|
|
101
|
+
3. Eject Avo's `ApplicationController` and include the scaffold's `Authentication` concern there, then `prepend_before_action :require_authentication` (and `delegate :new_session_path, to: :main_app`). This is the door-gating step — steps 1–2 alone identify the user but don't lock anyone out.
|
|
102
|
+
|
|
103
|
+
If the user mentions the Rails 8 scaffold (or you see `Current.user` + an `Authentication` concern instead of Devise), point them at the scaffold guide and don't try to shortcut step 3.
|
|
104
|
+
|
|
105
|
+
### 4. Lightweight roles — `is_admin?` and `is_developer?`
|
|
106
|
+
|
|
107
|
+
Avo ships two deliberately minimal roles, resolved by calling a method on the `current_user`:
|
|
108
|
+
|
|
109
|
+
- `is_admin?` → the user can reach the private **Avo Status** page (`/avo_private/status`, under your mount path) and sees its sidebar link in production.
|
|
110
|
+
- `is_developer?` → same Status-page access, **plus** full backtraces on non-validation errors (e.g. an API call that blows up during save) instead of a generic "Something went wrong".
|
|
111
|
+
|
|
112
|
+
Check them yourself anywhere with `Avo::Current.user_is_admin?` / `Avo::Current.user_is_developer?`. Point Avo at differently-named predicate methods via the initializer:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
# config/initializers/avo.rb
|
|
116
|
+
Avo.configure do |config|
|
|
117
|
+
config.is_admin_method = :is_admin? # default
|
|
118
|
+
config.is_developer_method = :is_developer? # default
|
|
119
|
+
# e.g. config.is_admin_method = :staff?
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**These roles are not a general authorization system** — they only gate the Status page and developer backtraces. Restricting which resources/actions a user can see is authorization (policies): the **avo-authorization** skill.
|
|
124
|
+
|
|
125
|
+
### 5. The sidebar profile widget
|
|
126
|
+
|
|
127
|
+
The sidebar footer shows a small profile widget for the object returned by `current_user_method`, reading three things off it by duck-typing:
|
|
128
|
+
|
|
129
|
+
- **Name** — calls `name`; if absent/blank, falls back to `email`, then `email_address` (the Rails 8 scaffold's column), and finally the literal **"Avo user"**. So "the admin shows 'Avo user' instead of my name" means the user object responds to none of `name`/`email`/`email_address` — add a `name` (or `email`) method to the model.
|
|
130
|
+
- **Avatar** — calls `avatar`, used as the photo's `src`. Add an `avatar` method returning an image URL to "show the signed-in user's avatar in the sidebar".
|
|
131
|
+
- **Title** — calls `avo_title`, shown under the name. Define `avo_title` on the user model for a role/label line.
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
# app/models/user.rb — give the widget something to show
|
|
135
|
+
class User < ApplicationRecord
|
|
136
|
+
def name = "#{first_name} #{last_name}"
|
|
137
|
+
def avatar = gravatar_url # any image URL
|
|
138
|
+
def avo_title = admin? ? "Administrator" : "Member"
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 6. The sign-out link
|
|
143
|
+
|
|
144
|
+
The sign-out item lives in the sidebar footer's three-dots menu. Avo shows it automatically if your app responds to `destroy_user_session_path` (the Devise default). Otherwise it's hidden until you name the path. Two knobs:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
# config/initializers/avo.rb
|
|
148
|
+
Avo.configure do |config|
|
|
149
|
+
# Customize just the "user" segment: User -> current_user gives
|
|
150
|
+
# destroy_current_user_session_path. current_admin -> destroy_current_admin_session_path.
|
|
151
|
+
config.current_user_resource_name = :current_user # default: "user"
|
|
152
|
+
|
|
153
|
+
# Or specify the whole path helper outright (wins over the above if both set):
|
|
154
|
+
# config.sign_out_path_name = :logout_path # Rails 8 scaffold: :session_path
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
So "add a logout button to the admin" = pick whichever of these matches an existing route helper in `bin/rails routes`. The link fires a `DELETE`, so the target route must accept it.
|
|
159
|
+
|
|
160
|
+
## Key options
|
|
161
|
+
|
|
162
|
+
| Option | Where | Does | Tiny example |
|
|
163
|
+
| --- | --- | --- | --- |
|
|
164
|
+
| `config.current_user_method` | initializer | How Avo fetches the signed-in user (Symbol or block) | `config.current_user_method = :current_user` |
|
|
165
|
+
| `authenticate :user do … end` | routes.rb | Gate the mount to signed-in users (Devise) | `authenticate :user, ->(u){ u.admin? } { mount_avo }` |
|
|
166
|
+
| `config.authenticate_with` | initializer | `before_action` check inside Avo's controller | `config.authenticate_with { redirect_to "/" unless … }` |
|
|
167
|
+
| `config.is_admin_method` | initializer | Predicate for the admin role | `config.is_admin_method = :staff?` |
|
|
168
|
+
| `config.is_developer_method` | initializer | Predicate for the developer role | `config.is_developer_method = :is_developer?` |
|
|
169
|
+
| `Avo::Current.user_is_admin?` | anywhere | Read the admin role at runtime | `redirect_to root_path unless Avo::Current.user_is_admin?` |
|
|
170
|
+
| `config.current_user_resource_name` | initializer | "user" segment of the sign-out path | `config.current_user_resource_name = :current_admin` |
|
|
171
|
+
| `config.sign_out_path_name` | initializer | Full sign-out path helper (wins over the above) | `config.sign_out_path_name = :logout_path` |
|
|
172
|
+
| `name` / `email` / `avatar` / `avo_title` | user model | Feed the sidebar profile widget | `def avo_title = "Administrator"` |
|
|
173
|
+
|
|
174
|
+
## Gotchas
|
|
175
|
+
|
|
176
|
+
- **`current_user` is the prerequisite for authorization.** If it returns `nil`, policies silently misbehave — a user with no identity fails or skips every rule in confusing ways. When "policies aren't working", check `current_user_method` first, *then* go to **avo-authorization**.
|
|
177
|
+
- **Identifying the user ≠ locking the door.** `current_user_method` only tells Avo *who* someone is; it does not keep anyone out. You still need `authenticate :user` (routes) or `authenticate_with` (initializer). It's easy to wire up `current_user` and leave the admin wide open.
|
|
178
|
+
- **`authenticate_with` can't see your app's controller.** Avo's `ApplicationController` does not inherit from the app's `ApplicationController`, so app helper methods/concerns are unavailable inside the block — write the logic inline, or extend Avo's controller via **avo-controllers**.
|
|
179
|
+
- **Rails 8 scaffold is custom auth.** It needs all three steps (current user via `Current.user`, `sign_out_path_name = :session_path`, and ejecting the controller to `include Authentication` + `prepend_before_action :require_authentication`). Steps 1–2 without step 3 leave the admin unguarded.
|
|
180
|
+
- **Roles are minimal by design.** `is_admin?` / `is_developer?` gate only the `/avo_private/status` page and developer backtraces — they are **not** resource-level authorization. Don't reach for them to hide records or actions; that's policies.
|
|
181
|
+
- **"Avo user" in the widget** means the user object responds to none of `name` / `email` / `email_address`. Add one of those methods to the model, not any Avo config.
|
|
182
|
+
- **Sign-out hidden?** The path helper must actually exist and accept `DELETE`. Confirm with `bin/rails routes` before setting `sign_out_path_name` / `current_user_resource_name`; `sign_out_path_name` wins if both are set.
|
|
183
|
+
- **Verify before writing.** Config names drift between versions — check the docs URLs above or the app's installed Avo source (`Avo::Configuration`) rather than trusting memory.
|
|
184
|
+
|
|
185
|
+
## Report
|
|
186
|
+
|
|
187
|
+
When done, tell the user:
|
|
188
|
+
|
|
189
|
+
- Which files you touched (full paths) — typically `config/initializers/avo.rb`, and possibly `config/routes.rb`, an ejected `app/controllers/avo/application_controller.rb`, or the user model.
|
|
190
|
+
- How Avo now resolves `current_user` (Devise `:current_user`, a custom method, or a `Current.user` block), and how the door is gated (route-level `authenticate :user` vs `authenticate_with`) — or flag explicitly if the admin is **not** gated yet.
|
|
191
|
+
- Any roles configured (`is_admin_method` / `is_developer_method`) and what they unlock (Status page, developer backtraces) — noting they are not general authorization.
|
|
192
|
+
- Any profile-widget or sign-out changes (methods added to the model, `current_user_resource_name` / `sign_out_path_name`).
|
|
193
|
+
- Next steps: if this was groundwork for restricting resources/actions, hand off to **avo-authorization** (policies now that `current_user` is set); if `authenticate_with` needs real app logic, hand off to **avo-controllers**; if on the Rails 8 scaffold, confirm all three guide steps are done.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: avo-aware
|
|
3
|
+
description: >-
|
|
4
|
+
Map a Rails change onto the Avo admin surface it affects, and route the edit to the skill that
|
|
5
|
+
owns it. Read this after the Avo skills loader has decided a model change has an admin dimension
|
|
6
|
+
— a new or changed column, a new model, an enum or status column, a new association, or a
|
|
7
|
+
capability like "admins can approve orders". Carries the Rails-change-to-Avo-surface routing
|
|
8
|
+
table and the column-type-to-field-type mapping for this Avo version. Does not decide whether
|
|
9
|
+
Avo is relevant; the loader gates that.
|
|
10
|
+
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch
|
|
11
|
+
metadata:
|
|
12
|
+
requires-gem: none — dispatcher; the target skill states its own gem
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
> **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.
|
|
16
|
+
|
|
17
|
+
# Map a Rails change onto the Avo admin
|
|
18
|
+
|
|
19
|
+
The Avo skills loader has already confirmed this app uses Avo and that the change at hand has an admin dimension. This skill answers the next two questions: **which admin surface** the change maps to, and **which skill owns** that surface.
|
|
20
|
+
|
|
21
|
+
Do the Rails change first, then propose the admin delta and ask before writing any Avo file. Never own the Avo edit here — route it.
|
|
22
|
+
|
|
23
|
+
## Routing table
|
|
24
|
+
|
|
25
|
+
| Rails change | Avo surface to add | Skill to apply |
|
|
26
|
+
| ------------ | ------------------ | -------------- |
|
|
27
|
+
| New column | A `field` on the resource (infer `as:` from the column type) | `avo-fields` |
|
|
28
|
+
| New model | Generate the Avo resource | `avo-resources` |
|
|
29
|
+
| New enum / state column | A badge or select field **and** a filter — often also a state-transition action | `avo-fields` + `avo-filters` + `avo-actions` |
|
|
30
|
+
| New capability (*"admins can approve X"*) | An action **and** the Pundit policy method that authorizes it | `avo-actions` + `avo-authorization` |
|
|
31
|
+
| New association | An association field | `avo-associations` |
|
|
32
|
+
| *"only show active records by default"* | A default scope on the resource | `avo-filters` |
|
|
33
|
+
| *"let admins reorder these"* | `self.ordering` on the resource | `avo-record-reordering` |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Column-type → field mapping
|
|
38
|
+
|
|
39
|
+
When a new column becomes a field, infer the `as:` option from the migration column type:
|
|
40
|
+
|
|
41
|
+
| Column type | Avo field `as:` |
|
|
42
|
+
| ----------- | --------------- |
|
|
43
|
+
| `string` | `:text` |
|
|
44
|
+
| `text` | `:textarea` (or `:trix` / rich text for long-form content) |
|
|
45
|
+
| `integer`, `decimal`, `float` | `:number` (use `:money` for currency amounts) |
|
|
46
|
+
| `boolean` | `:boolean` |
|
|
47
|
+
| `date` | `:date` |
|
|
48
|
+
| `datetime`, `timestamp` | `:date_time` |
|
|
49
|
+
| enum / fixed value set | `:select`, `:badge`, or `:status` |
|
|
50
|
+
| `references` / `belongs_to` | `:belongs_to` |
|
|
51
|
+
| `has_many` / `has_one` association | `:has_many` / `:has_one` |
|
|
52
|
+
| `json`, `jsonb` | `:key_value` (or `:code` for freeform structured data) |
|
|
53
|
+
| Active Storage attachment | `:file` (single) / `:files` (multiple) |
|
|
54
|
+
|
|
55
|
+
These are defaults, not laws. Let the semantics of the column win — a `string` holding an email is better as a text field with an email pattern; a `text` column holding Markdown is better as rich text. When you propose the field in the confirm step, name the `as:` you'll use so the user can redirect it.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Docs
|
|
60
|
+
|
|
61
|
+
Whenever a delegated edit needs current API details, pull them from the Avo docs map so the authoring reflects the installed version rather than stale memory:
|
|
62
|
+
|
|
63
|
+
**`https://docs.avohq.io/4.0/docs-map.md`** — an index of every Avo 4 docs page and its headings, with links. Fetch it, then follow the link to the specific page (fields, resources, actions, filters, associations, authorization) the vertical skill needs.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Report
|
|
68
|
+
|
|
69
|
+
When you're done, tell the user both halves plainly:
|
|
70
|
+
|
|
71
|
+
- **The Rails change you made** — the migration/model/enum/association you wrote.
|
|
72
|
+
- **The Avo change you proposed or made** — which admin surface, which field/filter/action/policy, and which vertical skill handled it. If the user declined the Avo change, say that the admin was left untouched so nothing is silently out of sync.
|
|
73
|
+
|
|
74
|
+
Keep it to a few lines. The user should be able to see, at a glance, that the model and its admin surface still agree.
|