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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/assets/builds/avo/application.css +30 -0
  4. data/lib/avo/base_action.rb +1 -1
  5. data/lib/avo/reloader.rb +9 -0
  6. data/lib/avo/skills/avo-actions/SKILL.md +255 -0
  7. data/lib/avo/skills/avo-admin-config/SKILL.md +163 -0
  8. data/lib/avo/skills/avo-associations/SKILL.md +168 -0
  9. data/lib/avo/skills/avo-authentication/SKILL.md +193 -0
  10. data/lib/avo/skills/avo-aware/SKILL.md +74 -0
  11. data/lib/avo/skills/avo-branding-appearance/SKILL.md +270 -0
  12. data/lib/avo/skills/avo-controllers/SKILL.md +236 -0
  13. data/lib/avo/skills/avo-custom-fields/SKILL.md +197 -0
  14. data/lib/avo/skills/avo-custom-ui/SKILL.md +460 -0
  15. data/lib/avo/skills/avo-engine-internals/SKILL.md +245 -0
  16. data/lib/avo/skills/avo-fields/SKILL.md +219 -0
  17. data/lib/avo/skills/avo-filters/SKILL.md +196 -0
  18. data/lib/avo/skills/avo-i18n/SKILL.md +254 -0
  19. data/lib/avo/skills/avo-index-views/SKILL.md +254 -0
  20. data/lib/avo/skills/avo-media-library/SKILL.md +122 -0
  21. data/lib/avo/skills/avo-menu-icons/SKILL.md +135 -0
  22. data/lib/avo/skills/avo-menu-icons/scripts/list_icons.rb +49 -0
  23. data/lib/avo/skills/avo-multitenancy/SKILL.md +186 -0
  24. data/lib/avo/skills/avo-navigation-search/SKILL.md +255 -0
  25. data/lib/avo/skills/avo-performance/SKILL.md +190 -0
  26. data/lib/avo/skills/avo-resources/SKILL.md +273 -0
  27. data/lib/avo/skills/avo-setup/SKILL.md +288 -0
  28. data/lib/avo/skills/avo-testing/SKILL.md +188 -0
  29. data/lib/avo/skills/avo-troubleshoot/SKILL.md +325 -0
  30. data/lib/avo/skills/avo-update/SKILL.md +179 -0
  31. data/lib/avo/skills/bin/avo-skills-resolve +242 -0
  32. data/lib/avo/skills/index.md +53 -0
  33. data/lib/avo/skills/package-map.md +30 -0
  34. data/lib/avo/version.rb +1 -1
  35. data/lib/avo.rb +4 -0
  36. data/lib/generators/avo/skills_generator.rb +231 -0
  37. data/lib/generators/avo/skills_install_panel.rb +169 -0
  38. data/lib/generators/avo/templates/skills/SKILL.md +111 -0
  39. metadata +32 -1
@@ -0,0 +1,270 @@
1
+ ---
2
+ name: avo-branding-appearance
3
+ description: Brand and theme an Avo admin panel — logos, favicon, color scheme, neutral/accent palettes, chart colors, per-user theme persistence, deep CSS re-skinning, and menu/action icons — starting from the no-build `config.appearance` path in `config/initializers/avo.rb`. Use when the user wants to "brand the admin with our logo and colors", "make the admin match our brand", "add our company logo", "add a favicon", "make the admin default to / support dark mode", "let users switch themes" or "lock the theme", "remember each user's theme", "change the accent/primary color" or "make the buttons blue", "change the sidebar/navbar background", "give the admin a coastal/rose/sunset theme", "our admin looks too generic", "change the dashboard chart colors", or "use a custom icon for this menu item" — whether or not they name Avo.
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 Branding & Appearance
12
+
13
+ Make the Avo admin look like your product — logos, favicon, color scheme, brand colors, chart colors, and the deeper CSS chrome (navbar, sidebar, tables). Avo gives you a ladder: start with a few lines of Ruby in `config/initializers/avo.rb` (`config.appearance = { … }`, **no build step**), and only drop to CSS or ejected views when the Ruby layer can't express what you want. **Almost every branding request is satisfied by `config.appearance` alone** — reach for CSS last.
14
+
15
+ Files you'll touch, from shallowest to deepest:
16
+
17
+ - `config/initializers/avo.rb` → `config.appearance = { … }` — logos, favicon, scheme, palettes, picker/lock, persistence, chart colors. **The default answer.**
18
+ - `app/assets/stylesheets/avo-overrides.css` — no-build CSS-variable re-skin (eject with `rails g avo:eject --partial :avo_overrides_css`). Served as-is.
19
+ - `app/views/avo/partials/_head.html.erb` — inline `<style>` for component variables (eject with `rails g avo:eject --partial :head`).
20
+ - `app/assets/svgs/` — your own SVG icons for menu items / actions.
21
+ - A JSONB column + `load_settings`/`save_settings` procs — only when persisting each user's theme to the database.
22
+
23
+ ## Docs
24
+
25
+ Authoritative docs — fetch on demand, verify option names against them (and the app's installed Avo source) before writing; don't inline whole pages:
26
+
27
+ - Docs map (discover pages): https://docs.avohq.io/4.0/docs-map.md
28
+ - Theming overview (the ladder): https://docs.avohq.io/4.0/theming.md
29
+ - Appearance guide: https://docs.avohq.io/4.0/appearance.md — API reference (every option, defaults, CSS variables): https://docs.avohq.io/4.0/appearance-api.md
30
+ - Icons (Tabler / Heroicons / your own SVGs): https://docs.avohq.io/4.0/icons.md
31
+ - Branding → Appearance (Avo 3 `config.branding` was renamed to `config.appearance` in Avo 4): https://docs.avohq.io/4.0/branding.md
32
+
33
+ ## When this applies
34
+
35
+ **Explicit (Avo named):** "set `config.appearance`", "change Avo's logo / logomark / favicon", "set the Avo accent/neutral palette", "lock the Avo theme", "restrict the appearance picker", "persist Avo appearance to the database", "override Avo CSS variables", "eject the `:head` partial", "set an icon on this Avo menu item".
36
+
37
+ **Implicit (product-shaped, no mention of Avo):** "brand the admin with our logo and colors", "make the admin match our brand", "add our company logo / a favicon", "the admin should default to dark mode" / "support dark mode", "let users switch themes" / "lock it so they can't", "remember each user's theme across devices", "change the accent/primary color" / "make the buttons blue", "change the sidebar/navbar background color", "give the admin a coastal / rose / 80s-sunset theme", "our admin looks too generic", "change the dashboard chart colors", "use a custom icon for this menu item".
38
+
39
+ ## Workflow
40
+
41
+ Read `config/initializers/avo.rb` first. If a `config.appearance = { … }` hash already exists, **merge** new keys into it rather than adding a second block. Work top-down through this ladder and stop at the shallowest layer that does the job.
42
+
43
+ ### 1. Brand assets — logo, logomark, favicon, placeholder
44
+
45
+ Asset paths resolve through the Rails asset pipeline (e.g. `app/assets/images/my_company/logo.png` → `"my_company/logo.png"`). Each has an optional `*_dark` variant used in dark mode; omit the dark variant and the light file is used in both schemes.
46
+
47
+ ```ruby
48
+ # config/initializers/avo.rb
49
+ Avo.configure do |config|
50
+ config.appearance = {
51
+ logo: "my_company/logo.png", # main navbar logo
52
+ logo_dark: "my_company/logo-dark.png", # dark-mode variant (optional)
53
+ logomark: "my_company/logomark.png", # compact square mark for collapsed navbar
54
+ logomark_dark: "my_company/logomark-dark.png",
55
+ favicon: "my_company/favicon.ico",
56
+ favicon_dark: "my_company/favicon-dark.ico",
57
+ placeholder: "my_company/placeholder.svg" # fallback for records with no cover image
58
+ }
59
+ end
60
+ ```
61
+
62
+ **The top navbar is dark in BOTH light and dark mode** — `logo` always sits on a dark surface, so pick a file that reads on dark. `logo_dark` only swaps when the whole UI is in dark mode, not to fix contrast on the navbar.
63
+
64
+ ### 2. Color scheme — light / dark / auto
65
+
66
+ ```ruby
67
+ config.appearance = {
68
+ scheme: :auto # :auto (follow system, default) | :light | :dark
69
+ }
70
+ ```
71
+
72
+ Unless locked (step 4), users can flip the scheme from the navbar switcher. "Default to dark mode" → `scheme: :dark`. "Force dark, no toggle" → `scheme: :dark` + `lock: [:scheme]`.
73
+
74
+ ### 3. Brand colors — neutral & accent palettes
75
+
76
+ Two palettes drive the whole UI. **Neutral** = surfaces, borders, chrome. **Accent** = interactive emphasis: primary buttons, links, focus rings, selected rows. "Make the buttons blue" / "change the primary color" → set `accent`.
77
+
78
+ **Built-in presets (fastest):**
79
+
80
+ ```ruby
81
+ config.appearance = {
82
+ neutral: :slate, # :slate :stone :gray :zinc :neutral :taupe :mauve :mist :olive (or :brand)
83
+ accent: :blue # :red :orange :amber :yellow :lime :green :emerald :teal :cyan :sky
84
+ # :blue :indigo :violet :purple :fuchsia :pink :rose (or :brand)
85
+ }
86
+ ```
87
+
88
+ `neutral:` and `accent:` **must be Symbols** — a String or Hash raises `ArgumentError`.
89
+
90
+ **Custom brand colors** — define your own palette, then select it with the `:brand` preset:
91
+
92
+ ```ruby
93
+ config.appearance = {
94
+ # Neutral needs all 12 shades; the same scale is used in both light and dark mode.
95
+ neutral: :brand,
96
+ neutral_colors: {
97
+ 25 => "oklch(98.5% 0.005 60)", 50 => "oklch(97% 0.008 60)", 100 => "oklch(93% 0.012 60)",
98
+ 200 => "oklch(86% 0.015 60)", 300 => "oklch(76% 0.015 60)", 400 => "oklch(63% 0.014 60)",
99
+ 500 => "oklch(53% 0.013 60)", 600 => "oklch(48% 0.012 60)", 700 => "oklch(43% 0.011 60)",
100
+ 800 => "oklch(39% 0.010 60)", 900 => "oklch(28% 0.008 60)", 950 => "oklch(20% 0.005 60)"
101
+ },
102
+
103
+ # Accent needs exactly three tokens.
104
+ accent: :brand,
105
+ accent_colors: {
106
+ color: "oklch(55% 0.2 280)", # main accent: button bg, link color
107
+ content: "oklch(45% 0.2 280)", # hover surfaces, soft variants
108
+ foreground: "oklch(99% 0 0)" # text/icons rendered on top of the accent
109
+ }
110
+ }
111
+ ```
112
+
113
+ Palette values accept any CSS color string (`oklch()`, `#hex`, `rgb()`, `hsl()`, `var()`). `neutral_colors` and `accent_colors` are independent — set either, both, or neither. Defining the colors only creates the palette; you still need `neutral: :brand` / `accent: :brand` (or `"brand"` listed in `neutrals:`/`accents:`) to actually select it. If the user gives you only one brand color, set `accent_colors` and leave the neutral as a preset like `:slate`.
114
+
115
+ ### 4. Picker exposure — restrict, lock, and switcher layout
116
+
117
+ By default the navbar picker lets users change scheme, neutral, and accent. Trim the options, lock some down, or change the layout:
118
+
119
+ ```ruby
120
+ config.appearance = {
121
+ scheme: :light, neutral: :slate, accent: :blue,
122
+
123
+ neutrals: %w[brand slate stone olive], # subset shown in the picker (Strings, no colon)
124
+ accents: %w[brand blue indigo violet],
125
+
126
+ lock: [:scheme], # any subset of [:scheme, :neutral, :accent] — hides that switcher, forces the value
127
+ picker_layout: :inline # :inline (default; collapses to dropdown on small screens) | :dropdown
128
+ }
129
+ ```
130
+
131
+ A value **not** in `lock:` is a default the user can still override. Lock all three to fully pin the theme.
132
+
133
+ ### 5. Persist each user's picks
134
+
135
+ By default picks live in a **cookie** (per browser, zero setup). For cross-device persistence, switch to the database and supply both blocks; you need a JSON/JSONB column on the model backing `current_user`.
136
+
137
+ ```ruby
138
+ # db/migrate/..._add_avo_preferences_to_users.rb
139
+ class AddAvoPreferencesToUsers < ActiveRecord::Migration[8.0]
140
+ def change
141
+ add_column :users, :avo_preferences, :jsonb, default: {}
142
+ end
143
+ end
144
+ ```
145
+
146
+ ```ruby
147
+ config.appearance = {
148
+ persistence: :database, # :cookie (default) | :database
149
+ load_settings: -> {
150
+ current_user&.avo_preferences&.dig("appearance")&.symbolize_keys || {}
151
+ },
152
+ save_settings: -> {
153
+ next unless current_user
154
+
155
+ current_user.update!(
156
+ avo_preferences: current_user.avo_preferences.to_h.deep_merge(
157
+ "appearance" => settings.stringify_keys
158
+ )
159
+ )
160
+ }
161
+ }
162
+ ```
163
+
164
+ Both blocks run in Avo's execution context with `current_user` (plus `params`, `request`, `view_context`, `main_app`). `load_settings` returns a Hash with any subset of `:color_scheme`, `:neutral`, `:accent`; missing keys fall back to defaults. `save_settings` receives a `settings` local that is a **partial** Hash — only the keys the user just changed — so always `deep_merge` into existing preferences, never overwrite the whole blob.
165
+
166
+ ### 6. Chart colors
167
+
168
+ Recolor dashboard charts. Values are forwarded straight to Chart.js, so they **must be hex** (not `oklch`/`rgb`):
169
+
170
+ ```ruby
171
+ config.appearance = {
172
+ chart_colors: ["#0B8AE2", "#34C683", "#FFBE4F", "#FF7676", "#2AB1EE"]
173
+ }
174
+ ```
175
+
176
+ ### 7. Deep re-skin — CSS variables (only when `config.appearance` can't reach it)
177
+
178
+ Navbar background, sidebar surfaces, table row hover/selected, focus ring, and motion speeds aren't routed through Ruby — they're CSS custom properties. Avo's whole look is variable-driven, so overriding a handful re-skins everything with **no build step**. Put light-mode values on `:root`, dark-mode overrides on `.dark`.
179
+
180
+ Preferred surface — eject `avo-overrides.css` (loaded after Avo's stylesheet, so it wins the cascade):
181
+
182
+ ```bash
183
+ rails g avo:eject --partial :avo_overrides_css
184
+ ```
185
+
186
+ ```css
187
+ /* app/assets/stylesheets/avo-overrides.css */
188
+ :root {
189
+ --color-accent: var(--color-fuchsia-500);
190
+ --color-navbar-background: #1e3a5f;
191
+ --color-sidebar-background: #f5f7fa;
192
+ --color-table-row-hover: #eef4fb;
193
+ --radius-card: 1.5rem;
194
+ }
195
+ .dark {
196
+ --color-accent: var(--color-fuchsia-400);
197
+ --color-navbar-background: #0b1a2b;
198
+ --color-sidebar-background: #11161c;
199
+ }
200
+ ```
201
+
202
+ The navbar and sidebar expose **scoped** palette contracts on the `.top-navbar` and `.avo-sidebar` selectors (e.g. `--top-navbar-content`, `--sidebar-link-active-background`) so you can recolor them without leaking into popovers or the main content. If you'd rather keep the `<style>` next to the rest of the head, eject `:head` instead and append a `<style>` block there:
203
+
204
+ ```bash
205
+ bin/rails generate avo:eject --partial :head
206
+ ```
207
+
208
+ The full variable list (navbar, sidebar, table, focus ring, motion) with defaults lives in the CSS variables section of `appearance-api.md` — fetch it before writing component-level overrides. For named-theme requests ("coastal", "rose", "80s sunset"), work in `avo-overrides.css` with matching `:root` and `.dark` values.
209
+
210
+ ### 8. Icons (menu items, actions)
211
+
212
+ Anywhere Avo takes an `icon:`, pass a path string. **Prefer Tabler in v4** (`tabler/outline/<name>` or `tabler/filled/<name>`); Heroicons (`heroicons/outline/<name>`, also `solid`/`mini`/`micro`) are legacy-supported. Your own SVGs go in `app/assets/svgs/` and are referenced by filename.
213
+
214
+ ```ruby
215
+ # a menu item in config.main_menu
216
+ link "Reports", path: "/reports", icon: "tabler/outline/chart-bar"
217
+ # your own file at app/assets/svgs/my-icons/rocket.svg
218
+ dashboard :sales, icon: "my-icons/rocket.svg"
219
+ ```
220
+
221
+ For **populating menu/resource icons at scale** (migrations, whole-sidebar passes), that's the **avo-navigation-search** skill — defer to it rather than hand-picking here. Avo's own `avo/*` icons are a **private API** — don't rely on them.
222
+
223
+ ## Key options (`config.appearance`)
224
+
225
+ | Option | Does | Type / values |
226
+ | --- | --- | --- |
227
+ | `logo` / `logo_dark` | Main navbar logo (+ dark variant) | String path; default `"avo/logo.png"` |
228
+ | `logomark` / `logomark_dark` | Compact mark for collapsed navbar | String path |
229
+ | `favicon` / `favicon_dark` | Browser favicon (+ dark variant) | String path |
230
+ | `placeholder` | Fallback image for record with no cover | String path |
231
+ | `scheme` | Default color mode | `:auto` (default) `:light` `:dark` |
232
+ | `neutral` / `accent` | Palette presets | **Symbol** only (or `:brand`) |
233
+ | `neutral_colors` | Custom 12-shade neutral | Hash of all 12 shades (`25`…`950`) |
234
+ | `accent_colors` | Custom accent | Hash of `:color`, `:content`, `:foreground` |
235
+ | `neutrals` / `accents` | Restrict picker options | Array of **Strings** (no colon) |
236
+ | `lock` | Force values, hide switchers | Array subset of `[:scheme, :neutral, :accent]` |
237
+ | `picker_layout` | Navbar switcher layout | `:inline` (default) `:dropdown` |
238
+ | `persistence` | Where picks are stored | `:cookie` (default) `:database` |
239
+ | `load_settings` / `save_settings` | DB persistence blocks | Proc; needs a JSON/JSONB column |
240
+ | `chart_colors` | Dashboard chart palette | Array of **hex** Strings |
241
+
242
+ CSS-only knobs (navbar/sidebar/table/focus/motion variables) are not in this hash — see step 7 and `appearance-api.md`.
243
+
244
+ ## Gotchas
245
+
246
+ - **`neutral:` / `accent:` must be Symbols.** A String or Hash raises `ArgumentError`. Custom colors go through `neutral_colors:` / `accent_colors:`, and you must also set `neutral: :brand` / `accent: :brand` (or list `"brand"` in `neutrals:`/`accents:`) to select the custom palette.
247
+ - **`neutral_colors` needs all 12 shades; `accent_colors` needs all 3 tokens.** A missing or `nil` value raises `ArgumentError`.
248
+ - **The navbar is dark in both modes.** The `logo` must read on a dark surface. `logo_dark` is for whole-UI dark mode, not navbar contrast.
249
+ - **`chart_colors` must be hex.** They're passed straight to Chart.js — `oklch()`/`rgb()` won't work there (even though the palettes accept them).
250
+ - **`avo-overrides.css` is served as-is, NOT through the Tailwind build.** Only put plain CSS + variable overrides there — no `@apply`, no arbitrary values. Tailwind directives for your *own* custom UI belong in `app/assets/stylesheets/avo/` (which IS built). See the avo-custom-ui skill.
251
+ - **DB persistence needs a JSONB column and BOTH blocks**, and `save_settings` gets a **partial** `settings` Hash (only the changed keys) — `deep_merge`, never overwrite the whole preferences blob.
252
+ - **`config.branding` was renamed to `config.appearance` in Avo 4.** On a v3 app you may find `config.branding = { … }` — migrate it into `config.appearance`.
253
+ - **Avo's `avo/*` icons are a private API.** Use `tabler/*` (preferred), `heroicons/*` (legacy), or your own SVGs in `app/assets/svgs/`.
254
+ - **Verify before writing.** Option names drift between versions — check the docs URLs above or the installed Avo source rather than trusting memory.
255
+
256
+ ## Cross-links
257
+
258
+ - **avo-admin-config** — other global `Avo.configure` knobs beyond appearance.
259
+ - **avo-navigation-search** — populating menu/resource icons and menu structure at scale.
260
+ - **avo-custom-ui** — deeper CSS/Tailwind for your own tools/fields, and ejecting views for full markup control.
261
+
262
+ ## Report
263
+
264
+ When done, tell the user:
265
+
266
+ - Which file(s) you edited (full paths) and, for CSS/eject work, which generator command(s) to run (`rails g avo:eject --partial :avo_overrides_css` / `:head`, or the migration).
267
+ - Which `config.appearance` keys you set and why (assets, scheme, palettes, picker/lock, persistence, chart colors).
268
+ - Which layer of the ladder you used and why you didn't go deeper (Ruby vs `avo-overrides.css` vs ejected `:head`).
269
+ - Anything the user must still do: add the asset files under `app/assets/`, run the migration for DB persistence, or restart the server so initializer changes take effect.
270
+ - For custom palettes, remind them the same scale applies in both light and dark mode, and confirm the `:brand` preset is selected.
@@ -0,0 +1,236 @@
1
+ ---
2
+ name: avo-controllers
3
+ description: Override the per-resource CRUD controller hooks Avo generates (redirect paths, flash messages, custom responses, save/destroy behavior) and safely extend Avo's private ApplicationController. Use when the user wants to redirect somewhere else after creating/updating/deleting in the admin, change the "successfully created/updated" flash message, soft-delete or archive instead of destroying, swap `@record.save!` for a service object, run code before every admin request, set a `Current` attribute or tenant per admin request, override `fill_record`, or fix `ActionDispatch::MissingController`. Covers `Avo::CoursesController < Avo::ResourcesController`, the `avo:controller` generator, `after_create_path`/`after_update_path`/`after_destroy_path`, `*_success_message`/`*_fail_message`, `create_/update_/destroy_success_action`/`*_fail_action`, `save_record_action`/`destroy_record_action`, and extending `Avo::ApplicationController` with a concern in `to_prepare`.
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 Resource Controllers
12
+
13
+ Avo generates a Rails controller alongside every resource so each one plugs into Rails' REST architecture. Each generated controller inherits from `Avo::ResourcesController`, which inherits from `Avo::BaseController` — and that base is where every CRUD action (`index`, `show`, `new`, `create`, `edit`, `update`, `destroy`) lives:
14
+
15
+ ```ruby
16
+ # app/controllers/avo/courses_controller.rb
17
+ class Avo::CoursesController < Avo::ResourcesController
18
+ end
19
+ ```
20
+
21
+ Most of the time you never touch this file — resource-level config on `app/avo/resources/<name>.rb` does the job. Reach for the controller when you need **granular per-action control**: where the user lands after a save, what the flash says, how the response is rendered, or how the record is actually persisted or removed. You do that by overriding **hook methods** in the generated controller, the same way Devise lets you override `after_sign_in_path_for`. For app-wide behavior that isn't tied to one resource (a `Current` attribute, a tenant, a `before_action` on every admin request), you extend `Avo::ApplicationController` instead — but **never by copying it** (see below).
22
+
23
+ ## Docs
24
+
25
+ Authoritative docs — fetch on demand rather than guessing, and verify every method name against the docs or the app's installed Avo source before writing it:
26
+
27
+ - Docs map (start here to discover pages): https://docs.avohq.io/4.0/docs-map.md
28
+ - Controllers guide: https://docs.avohq.io/4.0/controllers.md — API reference (each hook + its default): https://docs.avohq.io/4.0/controllers-api.md
29
+ - Extending `Avo::ApplicationController`: https://docs.avohq.io/4.0/avo-application-controller.md
30
+
31
+ ## When this applies
32
+
33
+ **Explicit (Avo named):** "override the `Courses` controller", "override `after_create_path`/`after_update_path`/`after_destroy_path`", "change `create_success_message`", "override `save_record_action` / `destroy_record_action`", "generate a controller with `avo:controller`", "set `config.resource_parent_controller`", "extend `Avo::ApplicationController`", "override `fill_record`".
34
+
35
+ **Implicit (Rails/product-shaped, no mention of Avo):** "redirect somewhere else after saving/creating/deleting in the admin", "send the user to the dashboard after they create a record", "change the 'successfully created' message", "soft-delete instead of destroying in the admin", "archive instead of delete", "run a service object when I save instead of `save!`", "run code before every admin request", "set a `Current` attribute on each admin request", "set the tenant / add multitenancy in the admin", "customize what happens after I save a record", "I get `ActionDispatch::MissingController` when I open a resource".
36
+
37
+ ## Workflow
38
+
39
+ ### 1. Know which lever to pull
40
+
41
+ Before opening a controller, check whether a **resource-level option** already covers it — it's less code and survives upgrades better:
42
+
43
+ - Just want to land on <Index /> or <Edit /> after a save? Set [`self.after_create_path`](https://docs.avohq.io/4.0/resources-api.md) / `self.after_update_path` (`:show` | `:edit` | `:index`) on the resource — no controller needed. (There is **no** resource-level `after_destroy_path`; use the controller hook for that.)
44
+ - Need a computed path, a different flash, a custom render, or different persistence? Override the controller hook (steps below).
45
+ - Behavior that should apply to **every** resource/request, not one? Extend `Avo::ApplicationController` (see "Extending `Avo::ApplicationController`").
46
+
47
+ ### 2. Make sure the controller exists
48
+
49
+ Every resource **must** have a paired controller at `app/controllers/avo/<name>s_controller.rb`, or opening it raises `ActionDispatch::MissingController`. The resource generator creates it for you; if it's missing (deleted, or the resource was hand-written), generate one:
50
+
51
+ ```bash
52
+ bin/rails generate avo:controller course # → app/controllers/avo/courses_controller.rb
53
+ bin/rails generate avo:controller galaxy/planet # namespaced → Avo::Galaxy::PlanetsController
54
+ ```
55
+
56
+ The last name segment is pluralized (`course` → `Avo::CoursesController`). To make generated controllers inherit from a shared base (e.g. one that adds authentication), pass `--parent-controller` or set it once in the initializer:
57
+
58
+ ```bash
59
+ bin/rails g avo:controller city --parent-controller Avo::BaseResourcesController
60
+ ```
61
+
62
+ ```ruby
63
+ # config/initializers/avo.rb
64
+ Avo.configure do |config|
65
+ config.resource_parent_controller = "Avo::BaseResourcesController" # default: "Avo::ResourcesController"
66
+ end
67
+ ```
68
+
69
+ ### 3. Override the hook(s) you need
70
+
71
+ Add the method(s) to the generated controller. Each hook has a documented default; calling `super` runs it. **Guard your override with `super`** whenever the default handles a flow you want to keep (this is the #1 source of breakage — see Gotchas).
72
+
73
+ ## Hooks
74
+
75
+ Every write action (`create`, `update`, `destroy`) exposes the same three flavors, plus two persistence methods shared by create/update and destroy. All are overridden in the generated resource controller.
76
+
77
+ ### Redirect path — where the user lands
78
+
79
+ `after_create_path`, `after_update_path`, `after_destroy_path` return the path used after a successful action.
80
+
81
+ ```ruby
82
+ class Avo::CoursesController < Avo::ResourcesController
83
+ def after_create_path
84
+ "/avo/resources/users"
85
+ end
86
+ end
87
+ ```
88
+
89
+ Defaults you may be dropping:
90
+
91
+ - **`after_create_path`** — when the record was created **through an association**, redirects back to the parent record's page; otherwise honors the resource's `self.after_create_path`, falling back to the new record's <Show /> (or <Edit /> when `config.resource_default_view` is `:edit`). Return `super` when `params[:via_relation_class]` and `params[:via_record_id]` are present to keep the parent redirect.
92
+ - **`after_update_path`** — returns `params[:return_to]`, then `params[:referrer]`, before honoring `self.after_update_path` and the <Show />/<Edit /> fallback. Return `super` when `params[:return_to]` is present so "send the user back where they came from" keeps working.
93
+ - **`after_destroy_path`** — returns `params[:referrer]` when present, otherwise the resource's <Index />.
94
+
95
+ ```ruby
96
+ def after_update_path
97
+ return super if params[:return_to].present?
98
+
99
+ "/avo/resources/courses"
100
+ end
101
+ ```
102
+
103
+ ### Flash message — what the user reads
104
+
105
+ Override `*_success_message` / `*_fail_message` (one per outcome) to change the flash text:
106
+
107
+ ```ruby
108
+ class Avo::CoursesController < Avo::ResourcesController
109
+ def create_success_message
110
+ "Course saved. Off you go! 🚀"
111
+ end
112
+
113
+ def destroy_fail_message
114
+ "This course could not be removed."
115
+ end
116
+ end
117
+ ```
118
+
119
+ The six: `create_success_message`, `create_fail_message`, `update_success_message`, `update_fail_message`, `destroy_success_message`, `destroy_fail_message`. Defaults are i18n-backed (e.g. `"#{@resource.name} #{t("avo.was_successfully_created")}."`); `destroy_fail_message` defaults to the record's joined validation errors, or `t("avo.failed")` when there are none.
120
+
121
+ ### Custom response — full control over the render
122
+
123
+ When path + message aren't enough (different format, extra headers, a completely different render), override the `*_action` methods. These are **Turbo-stream aware** and their defaults drive real UI flows, so fall back to `super` for the cases you aren't customizing:
124
+
125
+ ```ruby
126
+ class Avo::CoursesController < Avo::ResourcesController
127
+ def create_success_action
128
+ return super if params[:via_belongs_to_resource_class].present?
129
+
130
+ respond_to do |format|
131
+ format.html { redirect_to "/dashboard", flash: {success: create_success_message} }
132
+ end
133
+ end
134
+ end
135
+ ```
136
+
137
+ Defaults that carry Turbo behavior:
138
+
139
+ - **`create_success_action`** — when a record is created through a `belongs_to` modal (`params[:via_belongs_to_resource_class]` present), renders Turbo Streams that close the modal and select the new record in the field; otherwise redirects to `after_create_path` with the success flash. Keep the `super` guard or the "Create new record" flow inside `belongs_to` fields breaks.
140
+ - **`destroy_success_action`** — when the delete happens inside a Turbo Frame (`params[:turbo_frame]`, e.g. an association list) it reloads that frame via Turbo Streams; otherwise flashes and redirects to `after_destroy_path`. Keep the `super` guard or deleting from association lists breaks.
141
+ - **`create_fail_action` / `update_fail_action`** — flash the fail message and re-render `:new` / `:edit` with `:unprocessable_content` status (`:unprocessable_entity` on Rails < 7.1), plus a `turbo_stream` format.
142
+ - **`destroy_fail_action`** — flashes and renders a `turbo_stream` alert without leaving the page.
143
+
144
+ ### Persistence — how the record is saved or destroyed
145
+
146
+ Avo saves with `@record.save!` and removes with `@record.destroy!`. Override `save_record_action` / `destroy_record_action` for soft deletes, archiving, a service object, or extra bookkeeping:
147
+
148
+ ```ruby
149
+ class Avo::CoursesController < Avo::ResourcesController
150
+ def destroy_record_action
151
+ @record.archive! # soft-delete instead of destroying
152
+ end
153
+
154
+ def save_record_action
155
+ CourseSaver.new(@record).call # service object instead of save!
156
+ end
157
+ end
158
+ ```
159
+
160
+ **Errors raised inside these methods are caught, logged, and added to the record's errors** (`errors.add(:base, ...)`) — which automatically triggers the matching `*_fail_action` and `*_fail_message`. So you don't rescue in here yourself; raising *is* how you signal failure, and the record's validation errors surface in the fail flash.
161
+
162
+ ## Extending `Avo::ApplicationController`
163
+
164
+ For behavior that isn't tied to one resource — setting a `Current` attribute, selecting a tenant, a `before_action` on every admin request — you want `Avo::ApplicationController`, not a resource controller.
165
+
166
+ **Do NOT copy Avo's `application_controller.rb` into your app.** Avo's `ApplicationController` (and the `BaseApplicationController` it inherits) is a **private API**: methods, before/after actions, and helpers change between versions **without a changelog or upgrade-guide entry**, and a copied file silently drifts and breaks on the next upgrade. Use a concern merged in at boot instead.
167
+
168
+ **Add** behavior — `include` the concern, use an `included` block:
169
+
170
+ ```ruby
171
+ # app/controllers/concerns/multitenancy.rb
172
+ module Multitenancy
173
+ extend ActiveSupport::Concern
174
+
175
+ included do
176
+ before_action :set_tenant
177
+ # or, to run BEFORE all of Avo's own before_actions:
178
+ prepend_before_action :set_tenant
179
+ end
180
+
181
+ def set_tenant
182
+ # your logic here
183
+ end
184
+ end
185
+
186
+ # config/initializers/avo.rb
187
+ Rails.configuration.to_prepare do
188
+ Avo::ApplicationController.include Multitenancy
189
+ end
190
+ ```
191
+
192
+ **Override** an existing method (e.g. `fill_record`) — `prepend` the concern and use a `prepended` block so it wins in the ancestor chain, calling `super` to keep Avo's behavior:
193
+
194
+ ```ruby
195
+ # app/controllers/concerns/application_controller_overrides.rb
196
+ module ApplicationControllerOverrides
197
+ extend ActiveSupport::Concern
198
+
199
+ prepended do
200
+ before_action :some_hook
201
+ end
202
+
203
+ def fill_record
204
+ # your logic here
205
+ super
206
+ end
207
+ end
208
+
209
+ # config/initializers/avo.rb
210
+ Rails.configuration.to_prepare do
211
+ Avo::ApplicationController.prepend ApplicationControllerOverrides
212
+ end
213
+ ```
214
+
215
+ Rules of thumb: **`include` + `included`** to add; **`prepend` + `prepended`** to override an existing method; **`prepend_before_action`** to run *before* Avo's own filters (set a tenant/account early); always wrap it in `Rails.configuration.to_prepare` so it re-applies on every code reload.
216
+
217
+ ## Gotchas
218
+
219
+ - **Guard overrides with `super` — the defaults do more than redirect.** `after_create_path` handles the via-association parent redirect (`params[:via_relation_class]`/`via_record_id`), `after_update_path` honors `return_to`/`referrer`, `create_success_action` drives the `belongs_to`-modal Turbo flow (`via_belongs_to_resource_class`), and `destroy_success_action` reloads the Turbo Frame for association-list deletes (`params[:turbo_frame]`). Drop the `super` branch and you break that flow. Pattern: `return super if params[:x].present?`, then your custom behavior.
220
+ - **Never copy `Avo::ApplicationController` wholesale.** It's a private API that changes without changelog and breaks on upgrade. Use a concern + `Rails.configuration.to_prepare` (`include`/`included` to add, `prepend`/`prepended` to override, `prepend_before_action` to run first).
221
+ - **Raising is how you fail in persistence hooks.** Errors from `save_record_action` / `destroy_record_action` are caught and added to `@record.errors[:base]` → the fail action + message fire automatically. Don't swallow the exception; let it raise (or `errors.add` yourself) to signal failure. A foreign-key constraint on delete surfaces this way instead of 500-ing.
222
+ - **Prefer the resource option when one exists.** For a plain "land on index/edit after save", `self.after_create_path` / `self.after_update_path` on the resource beats a controller override. There's no resource-level `after_destroy_path` — that one's controller-only.
223
+ - **Missing controller → `ActionDispatch::MissingController`.** Every resource needs its controller; generate it with `bin/rails g avo:controller <name>`. Use `--parent-controller` (or `config.resource_parent_controller`) to inherit from a shared base.
224
+ - **Verify before writing.** Method names and defaults drift between versions — check the docs URLs above or the app's installed Avo source (`Avo::BaseController`, `Avo::BaseApplicationController`) rather than trusting memory.
225
+
226
+ **Related skills:** setting the tenant per request → **avo-multitenancy**; `Avo::Current` and other engine internals → **avo-engine-internals**; authenticating admin requests / `config.authenticate_with` → **avo-authentication**; resource-level `self.after_create_path` and friends → **avo-resources**.
227
+
228
+ ## Report
229
+
230
+ When done, tell the user:
231
+
232
+ - Which controller file(s) you created or edited (full paths) and any generator command(s) run.
233
+ - Which hook(s) you overrode and what each now does — and, for any hook whose default carried a Turbo/association/`return_to` flow, confirm you kept the `super` guard (or explicitly note you dropped it and why).
234
+ - For persistence overrides (`save_record_action`/`destroy_record_action`): what now persists/removes the record, and that failures still surface through the normal fail action/message.
235
+ - For `Avo::ApplicationController` work: the concern file, whether you used `include`/`prepend`, and that it's wired in `Rails.configuration.to_prepare`.
236
+ - Anything still needed: run pending migrations, generate a missing controller, add the `archived_at`/soft-delete column, or reach for a related skill (multitenancy, authentication).