plutonium 0.50.0 → 0.51.0
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/.claude/skills/plutonium/SKILL.md +85 -102
- data/.claude/skills/plutonium-app/SKILL.md +572 -0
- data/.claude/skills/plutonium-auth/SKILL.md +163 -300
- data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
- data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
- data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
- data/.claude/skills/plutonium-testing/SKILL.md +6 -5
- data/.claude/skills/plutonium-ui/SKILL.md +900 -0
- data/CHANGELOG.md +27 -2
- data/Rakefile +2 -1
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1009 -1214
- data/app/assets/plutonium.js.map +3 -3
- data/app/assets/plutonium.min.js +52 -51
- data/app/assets/plutonium.min.js.map +3 -3
- data/docs/.vitepress/config.ts +37 -27
- data/docs/getting-started/index.md +22 -29
- data/docs/getting-started/installation.md +37 -80
- data/docs/getting-started/tutorial/index.md +4 -5
- data/docs/guides/adding-resources.md +66 -377
- data/docs/guides/authentication.md +94 -463
- data/docs/guides/authorization.md +124 -370
- data/docs/guides/creating-packages.md +94 -296
- data/docs/guides/custom-actions.md +121 -441
- data/docs/guides/index.md +22 -42
- data/docs/guides/multi-tenancy.md +116 -187
- data/docs/guides/nested-resources.md +103 -431
- data/docs/guides/search-filtering.md +123 -240
- data/docs/guides/testing.md +5 -4
- data/docs/guides/theming.md +157 -407
- data/docs/guides/troubleshooting.md +5 -3
- data/docs/guides/user-invites.md +106 -425
- data/docs/guides/user-profile.md +76 -243
- data/docs/index.md +1 -1
- data/docs/reference/app/generators.md +517 -0
- data/docs/reference/app/index.md +158 -0
- data/docs/reference/app/packages.md +146 -0
- data/docs/reference/app/portals.md +377 -0
- data/docs/reference/auth/accounts.md +230 -0
- data/docs/reference/auth/index.md +88 -0
- data/docs/reference/auth/profile.md +185 -0
- data/docs/reference/behavior/controllers.md +395 -0
- data/docs/reference/behavior/index.md +22 -0
- data/docs/reference/behavior/interactions.md +341 -0
- data/docs/reference/behavior/policies.md +417 -0
- data/docs/reference/index.md +56 -49
- data/docs/reference/resource/actions.md +423 -0
- data/docs/reference/resource/definition.md +508 -0
- data/docs/reference/resource/index.md +50 -0
- data/docs/reference/resource/model.md +348 -0
- data/docs/reference/resource/query.md +305 -0
- data/docs/reference/tenancy/entity-scoping.md +361 -0
- data/docs/reference/tenancy/index.md +36 -0
- data/docs/reference/tenancy/invites.md +393 -0
- data/docs/reference/tenancy/nested-resources.md +267 -0
- data/docs/reference/testing/index.md +287 -0
- data/docs/reference/ui/assets.md +400 -0
- data/docs/reference/ui/components.md +165 -0
- data/docs/reference/ui/displays.md +104 -0
- data/docs/reference/ui/forms.md +284 -0
- data/docs/reference/ui/index.md +30 -0
- data/docs/reference/ui/layouts.md +106 -0
- data/docs/reference/ui/pages.md +189 -0
- data/docs/reference/ui/tables.md +117 -0
- data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
- data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
- data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/update/update_generator.rb +0 -20
- data/lib/generators/pu/invites/install_generator.rb +1 -0
- data/lib/plutonium/definition/base.rb +1 -1
- data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
- data/lib/plutonium/helpers/turbo_helper.rb +11 -0
- data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
- data/lib/plutonium/resource/controller.rb +1 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
- data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
- data/lib/plutonium/resource/policy.rb +7 -0
- data/lib/plutonium/routing/mapper_extensions.rb +15 -0
- data/lib/plutonium/ui/component/methods.rb +4 -0
- data/lib/plutonium/ui/form/base.rb +6 -2
- data/lib/plutonium/ui/form/components/json.rb +58 -0
- data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
- data/lib/plutonium/ui/form/components/secure_association.rb +98 -22
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
- data/lib/plutonium/ui/form/resource.rb +0 -4
- data/lib/plutonium/ui/grid/resource.rb +1 -1
- data/lib/plutonium/ui/layout/base.rb +1 -0
- data/lib/plutonium/ui/page/base.rb +0 -7
- data/lib/plutonium/ui/page/index.rb +4 -4
- data/lib/plutonium/ui/table/resource.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium.rb +8 -0
- data/lib/tasks/release.rake +15 -1
- data/package.json +10 -10
- data/src/css/slim_select.css +4 -0
- data/src/js/controllers/slim_select_controller.js +61 -0
- data/src/js/turbo/turbo_actions.js +33 -0
- data/yarn.lock +553 -543
- metadata +44 -33
- data/.claude/skills/plutonium-assets/SKILL.md +0 -512
- data/.claude/skills/plutonium-controller/SKILL.md +0 -396
- data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
- data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
- data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
- data/.claude/skills/plutonium-forms/SKILL.md +0 -465
- data/.claude/skills/plutonium-installation/SKILL.md +0 -331
- data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
- data/.claude/skills/plutonium-invites/SKILL.md +0 -408
- data/.claude/skills/plutonium-model/SKILL.md +0 -440
- data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
- data/.claude/skills/plutonium-package/SKILL.md +0 -198
- data/.claude/skills/plutonium-policy/SKILL.md +0 -456
- data/.claude/skills/plutonium-portal/SKILL.md +0 -410
- data/.claude/skills/plutonium-views/SKILL.md +0 -651
- data/docs/reference/assets/index.md +0 -496
- data/docs/reference/controller/index.md +0 -412
- data/docs/reference/definition/actions.md +0 -462
- data/docs/reference/definition/fields.md +0 -383
- data/docs/reference/definition/index.md +0 -326
- data/docs/reference/definition/query.md +0 -351
- data/docs/reference/generators/index.md +0 -648
- data/docs/reference/interaction/index.md +0 -449
- data/docs/reference/model/features.md +0 -248
- data/docs/reference/model/index.md +0 -218
- data/docs/reference/policy/index.md +0 -456
- data/docs/reference/portal/index.md +0 -379
- data/docs/reference/views/forms.md +0 -411
- data/docs/reference/views/index.md +0 -544
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# Controller
|
|
2
|
+
|
|
3
|
+
Plutonium controllers ship full CRUD out of the box; nearly all customization belongs elsewhere. The controller stays thin — when in doubt, push the change to the definition (UI) or the policy (auth).
|
|
4
|
+
|
|
5
|
+
## 🚨 Critical
|
|
6
|
+
|
|
7
|
+
- **Don't override CRUD actions.** Use hooks (`resource_params`, `redirect_url_after_submit`, presentation hooks). Overriding `create` / `update` usually breaks authorization, params filtering, or both.
|
|
8
|
+
- **Named custom routes only.** Always pass `as:` — without it, `resource_url_for` can't build URLs (critical for nested resources).
|
|
9
|
+
- **Authorization is verified after every action** — if you write a custom action, you MUST call `authorize_current!` yourself or use `skip_verify_authorize_current` / `skip_verify_authorize_current!`.
|
|
10
|
+
- **Cross-resource queries: use `authorized_resource_scope(OtherModel)`, not raw `where`.** Otherwise you bypass that resource's tenancy and visibility rules.
|
|
11
|
+
|
|
12
|
+
## Base classes
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
# app/controllers/resource_controller.rb — installed once
|
|
16
|
+
class ResourceController < ApplicationController
|
|
17
|
+
include Plutonium::Resource::Controller
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# app/controllers/posts_controller.rb — per resource, generated
|
|
21
|
+
class PostsController < ::ResourceController
|
|
22
|
+
# Empty — all CRUD inherited
|
|
23
|
+
end
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Portal-specific overrides:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# packages/admin_portal/app/controllers/admin_portal/resource_controller.rb
|
|
30
|
+
module AdminPortal
|
|
31
|
+
class ResourceController < ::ResourceController
|
|
32
|
+
include AdminPortal::Concerns::Controller
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# packages/admin_portal/app/controllers/admin_portal/posts_controller.rb
|
|
37
|
+
class AdminPortal::PostsController < ResourceController
|
|
38
|
+
# Portal-specific customizations
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## What you get for free
|
|
43
|
+
|
|
44
|
+
| Action | Method | Path | Purpose |
|
|
45
|
+
|---|---|---|---|
|
|
46
|
+
| `index` | GET | `/posts` | List with pagination, search, filters, sorting |
|
|
47
|
+
| `show` | GET | `/posts/:id` | Display a single record |
|
|
48
|
+
| `new` | GET | `/posts/new` | Form |
|
|
49
|
+
| `create` | POST | `/posts` | Create |
|
|
50
|
+
| `edit` | GET | `/posts/:id/edit` | Form |
|
|
51
|
+
| `update` | PATCH/PUT | `/posts/:id` | Update |
|
|
52
|
+
| `destroy` | DELETE | `/posts/:id` | Delete |
|
|
53
|
+
|
|
54
|
+
Plus interactive-action routes for every action declared in the definition (`/posts/:id/record_actions/publish`, etc.).
|
|
55
|
+
|
|
56
|
+
## Where customization belongs
|
|
57
|
+
|
|
58
|
+
| Concern | Lives in |
|
|
59
|
+
|---|---|
|
|
60
|
+
| Field rendering (inputs, displays, columns) | [Definition](/reference/resource/definition) |
|
|
61
|
+
| Search, filters, scopes, sorting | [Query](/reference/resource/query) |
|
|
62
|
+
| Custom operations (publish, archive, import) | [Interaction](./interactions) + action on definition |
|
|
63
|
+
| Authorization rules | [Policy](./policies) |
|
|
64
|
+
| Form / show / page chrome | Definition (custom page classes — see [UI › Pages](/reference/ui/pages)) |
|
|
65
|
+
| **Custom redirect logic** | **[Controller hook](#redirect-hooks)** |
|
|
66
|
+
| **Param munging** | **[Controller hook](#parameter-hook)** |
|
|
67
|
+
| **Custom index query shape** | **[Controller hook](#index-query-hook)** |
|
|
68
|
+
| **Presentation of parent/entity fields** | **[Controller hook](#presentation-hooks)** |
|
|
69
|
+
|
|
70
|
+
## Override hooks
|
|
71
|
+
|
|
72
|
+
All hooks are private methods. Override only the ones you need.
|
|
73
|
+
|
|
74
|
+
### Redirect hooks
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
class PostsController < ::ResourceController
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# Where to go after create/update: "show" (default), "edit", "new", "index"
|
|
81
|
+
def preferred_action_after_submit = "edit"
|
|
82
|
+
|
|
83
|
+
# Custom URL after create/update (overrides preferred_action_after_submit)
|
|
84
|
+
def redirect_url_after_submit = posts_path
|
|
85
|
+
|
|
86
|
+
# Custom URL after destroy
|
|
87
|
+
def redirect_url_after_destroy = posts_path
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Parameter hook
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
def resource_params
|
|
95
|
+
params = super
|
|
96
|
+
params[:tags] = params[:tags].split(",") if params[:tags].is_a?(String)
|
|
97
|
+
params
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Index query hook
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
def filtered_resource_collection
|
|
105
|
+
base = current_authorized_scope
|
|
106
|
+
base = base.featured if params[:featured]
|
|
107
|
+
current_query_object.apply(base, raw_resource_query_params)
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Presentation hooks
|
|
112
|
+
|
|
113
|
+
Control whether parent / scoped-entity fields appear in forms and displays. Defaults are `false` (hidden, since they're inferred from the URL/portal).
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
def present_parent? = true # show parent field on displays
|
|
117
|
+
def submit_parent? = true # include in forms (defaults to tracking present_parent?)
|
|
118
|
+
def present_scoped_entity? = true
|
|
119
|
+
def submit_scoped_entity? = true
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Conditional pattern — show parent only when accessed standalone:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
def present_parent?
|
|
126
|
+
current_parent.nil?
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Lifecycle callbacks
|
|
131
|
+
|
|
132
|
+
Standard Rails callbacks work:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
class PostsController < ::ResourceController
|
|
136
|
+
before_action :check_quota, only: [:create]
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def check_quota
|
|
141
|
+
if current_user.posts.count >= 100
|
|
142
|
+
redirect_to resource_url_for(resource_class), alert: "Post limit reached"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Custom actions
|
|
149
|
+
|
|
150
|
+
Prefer **interactive actions** (definition + interaction — see [Resource › Actions](/reference/resource/actions)) for anything with business logic. The only reasons to hand-write a controller action: unusual response shapes, external service callbacks, etc.
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
class PostsController < ::ResourceController
|
|
154
|
+
def publish
|
|
155
|
+
authorize_current!(resource_record!, to: :publish?)
|
|
156
|
+
resource_record!.update!(published: true)
|
|
157
|
+
redirect_to resource_url_for(resource_record!), notice: "Published!"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Route must be named:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
register_resource Post do
|
|
166
|
+
member { post :publish, as: :publish } # ← `as:` required
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
::: warning Always name custom routes
|
|
171
|
+
Without `as:`, `resource_url_for` can't build the URL — particularly critical for nested resources.
|
|
172
|
+
:::
|
|
173
|
+
|
|
174
|
+
## Key methods
|
|
175
|
+
|
|
176
|
+
### Resource access
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
resource_class # The model class
|
|
180
|
+
resource_record! # Current record (raises RecordNotFound if not found)
|
|
181
|
+
resource_record? # Current record (nil if not found)
|
|
182
|
+
resource_params # Permitted params for create/update
|
|
183
|
+
current_parent # Parent record for nested routes
|
|
184
|
+
current_scoped_entity # Tenant entity for the current portal (nil if not scoped)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Authorization
|
|
188
|
+
|
|
189
|
+
**Current resource:**
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
authorize_current!(record, to: :action?) # check permission, raises if denied
|
|
193
|
+
current_policy # Policy instance for current resource
|
|
194
|
+
permitted_attributes # Allowed attributes for the current action
|
|
195
|
+
current_authorized_scope # Scoped collection the user can access
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Other resources** — cross-resource auth. Use these, NOT raw `where` / `find`:
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
authorize! other_record, to: :show? # ActionPolicy — raises if denied
|
|
202
|
+
allowed_to?(:show?, other_record) # Boolean check
|
|
203
|
+
policy_for(OtherModel) # Policy instance for class or record
|
|
204
|
+
policy_for(other_record).show?
|
|
205
|
+
|
|
206
|
+
authorized_resource_scope(OtherModel) # Scope on the model class
|
|
207
|
+
authorized_resource_scope(OtherModel, relation: OtherModel.published) # On a relation
|
|
208
|
+
authorized_resource_scope(OtherModel, type: :create) # Different action
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`authorized_resource_scope` applies the *other* resource's `relation_scope` AND the current policy context (entity scope, etc.). **Always prefer it over `OtherModel.all` / raw `where`** in cross-resource controller code — otherwise you bypass that resource's tenancy and visibility rules.
|
|
212
|
+
|
|
213
|
+
### Definition access
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
current_definition
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### UI builders (rarely needed in controllers)
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
build_form
|
|
223
|
+
build_detail
|
|
224
|
+
build_collection
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### URL generation
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
resource_url_for(@post) # show URL
|
|
231
|
+
resource_url_for(@post, action: :edit)
|
|
232
|
+
resource_url_for(Post) # index URL
|
|
233
|
+
resource_url_for(Post, action: :new)
|
|
234
|
+
|
|
235
|
+
# Nested
|
|
236
|
+
resource_url_for(@comment, parent: @post)
|
|
237
|
+
resource_url_for(Comment, action: :new, parent: @post)
|
|
238
|
+
|
|
239
|
+
# Cross-package
|
|
240
|
+
resource_url_for(@post, package: AdminPortal)
|
|
241
|
+
|
|
242
|
+
# Interactive actions
|
|
243
|
+
resource_url_for(@post, interaction: :publish)
|
|
244
|
+
resource_url_for(Post, interaction: :import)
|
|
245
|
+
resource_url_for(Post, interaction: :archive, ids: [1, 2, 3])
|
|
246
|
+
resource_url_for(@post, parent: @user, interaction: :publish)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Nested resources
|
|
250
|
+
|
|
251
|
+
Routes prefixed `nested_` automatically resolve the parent. See [Tenancy › Nested resources](/reference/tenancy/nested-resources) for the full surface; the controller-side methods:
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
current_parent # parent record
|
|
255
|
+
current_nested_association # :posts
|
|
256
|
+
parent_route_param # :user_id
|
|
257
|
+
parent_input_param # :user
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Parent fields are excluded from forms/displays by default. Toggle with the [presentation hooks](#presentation-hooks).
|
|
261
|
+
|
|
262
|
+
Custom parent resolution:
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
def current_parent
|
|
266
|
+
@current_parent ||= Company.friendly.find(params[:company_id])
|
|
267
|
+
end
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Entity scoping (multi-tenancy)
|
|
271
|
+
|
|
272
|
+
When a portal calls `scope_to_entity Organization, strategy: :path`, controllers in that portal automatically:
|
|
273
|
+
|
|
274
|
+
- Scope queries to the entity
|
|
275
|
+
- Exclude the entity field from forms (detected by association class)
|
|
276
|
+
- Inject the entity on create/update
|
|
277
|
+
- Expose `current_scoped_entity`
|
|
278
|
+
|
|
279
|
+
Plutonium auto-detects which `belongs_to` association points to the scoped class, even when `param_key` differs from the association name:
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
# Portal config
|
|
283
|
+
scope_to_entity Competition::Team, param_key: :team
|
|
284
|
+
|
|
285
|
+
# Model — association name differs from param_key, but Plutonium finds by class
|
|
286
|
+
class Match < ApplicationRecord
|
|
287
|
+
belongs_to :competition_team
|
|
288
|
+
end
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Multiple associations to the same class
|
|
292
|
+
|
|
293
|
+
If a model has two associations pointing at the scoped entity class, Plutonium raises:
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
Match has multiple associations to Competition::Team: home_team, away_team.
|
|
297
|
+
Plutonium cannot auto-detect which one to use for entity scoping.
|
|
298
|
+
Override `scoped_entity_association` in your controller to specify the association.
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Override:
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
class MatchesController < ::ResourceController
|
|
305
|
+
private
|
|
306
|
+
def scoped_entity_association = :home_team
|
|
307
|
+
end
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Full mechanics in [Tenancy › Entity scoping](/reference/tenancy/entity-scoping).
|
|
311
|
+
|
|
312
|
+
## Authorization verification
|
|
313
|
+
|
|
314
|
+
After-action callbacks ensure authorization happened:
|
|
315
|
+
|
|
316
|
+
```ruby
|
|
317
|
+
verify_authorize_current # all actions — `authorize_current!` must have been called
|
|
318
|
+
verify_current_authorized_scope # all except :new and :create — scope must have been loaded
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Skip only when handling auth manually. Two forms:
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
# Class-level — across multiple actions
|
|
325
|
+
class PostsController < ::ResourceController
|
|
326
|
+
skip_verify_authorize_current only: [:preview]
|
|
327
|
+
skip_verify_current_authorized_scope only: [:preview]
|
|
328
|
+
|
|
329
|
+
def preview
|
|
330
|
+
# auth handled manually
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Per-action — bang methods, inside the action body
|
|
335
|
+
def preview
|
|
336
|
+
skip_verify_authorize_current!
|
|
337
|
+
skip_verify_current_authorized_scope!
|
|
338
|
+
# auth handled manually
|
|
339
|
+
end
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Prefer the per-action bang form when only one action skips — keeps the exception co-located with the code that needs it.
|
|
343
|
+
|
|
344
|
+
## Response formats
|
|
345
|
+
|
|
346
|
+
Controllers respond to:
|
|
347
|
+
|
|
348
|
+
- HTML (default)
|
|
349
|
+
- JSON (via RABL templates)
|
|
350
|
+
- Turbo Stream (for Hotwire)
|
|
351
|
+
|
|
352
|
+
## Error handling
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
class PostsController < ::ResourceController
|
|
356
|
+
rescue_from ActiveRecord::RecordNotFound do
|
|
357
|
+
redirect_to resource_url_for(resource_class), alert: "Post not found"
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
rescue_from ActionPolicy::Unauthorized do
|
|
361
|
+
redirect_to resource_url_for(resource_class), alert: "Not authorized"
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Specifying resource class
|
|
367
|
+
|
|
368
|
+
The resource class is inferred from the controller name. Override if needed:
|
|
369
|
+
|
|
370
|
+
```ruby
|
|
371
|
+
class LegacyPostsController < ::ResourceController
|
|
372
|
+
controller_for Post
|
|
373
|
+
end
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Portal-specific controllers
|
|
377
|
+
|
|
378
|
+
Each portal can override:
|
|
379
|
+
|
|
380
|
+
```ruby
|
|
381
|
+
class AdminPortal::PostsController < ResourceController
|
|
382
|
+
private
|
|
383
|
+
def preferred_action_after_submit = "index"
|
|
384
|
+
end
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
See [App › Portals](/reference/app/portals) for the full portal controller story.
|
|
388
|
+
|
|
389
|
+
## Related
|
|
390
|
+
|
|
391
|
+
- [Policies](./policies) — authorization called from controllers
|
|
392
|
+
- [Interactions](./interactions) — business logic for custom actions
|
|
393
|
+
- [Resource › Definition](/reference/resource/definition) — UI config (where most "controller-like" tweaks belong)
|
|
394
|
+
- [Resource › Actions](/reference/resource/actions) — registering interactive actions
|
|
395
|
+
- [Tenancy › Nested resources](/reference/tenancy/nested-resources) — parent/child routing
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Behavior Reference
|
|
2
|
+
|
|
3
|
+
The behavior layer is intentionally thin:
|
|
4
|
+
|
|
5
|
+
- **[Controllers](./controllers) route** — handle requests, redirect after submit, transform params.
|
|
6
|
+
- **[Policies](./policies) authorize** — decide who can do what, which fields they can see, which records they can access.
|
|
7
|
+
- **[Interactions](./interactions) act** — encapsulate business logic for custom operations (publish, archive, import, send invitation).
|
|
8
|
+
|
|
9
|
+
Registering an action and rendering it lives in [Resource › Definition](/reference/resource/definition) and [Resource › Actions](/reference/resource/actions). This section covers **writing** the controller hook, policy method, or interaction class behind it.
|
|
10
|
+
|
|
11
|
+
For multi-tenant `relation_scope` and entity scoping, see [Tenancy › Entity scoping](/reference/tenancy/entity-scoping).
|
|
12
|
+
|
|
13
|
+
## At a glance
|
|
14
|
+
|
|
15
|
+
| Concern | Where it lives |
|
|
16
|
+
|---|---|
|
|
17
|
+
| Field rendering (inputs, displays, columns, search/filters) | [Definition](/reference/resource/definition) |
|
|
18
|
+
| Custom operations (publish, archive, import) | [Interaction](./interactions) + [Action](/reference/resource/actions) on the definition |
|
|
19
|
+
| Authorization rules | [Policy](./policies) |
|
|
20
|
+
| Tenant scoping (`relation_scope`) | [Policy](./policies) + [Tenancy](/reference/tenancy/entity-scoping) |
|
|
21
|
+
| Custom redirect logic, param munging, custom index query shape | [Controller hook](./controllers) |
|
|
22
|
+
| Presentation of parent/entity fields | [Controller presentation hooks](./controllers#presentation-hooks) |
|