plutonium 0.49.1 → 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.
Files changed (206) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +85 -102
  3. data/.claude/skills/plutonium-app/SKILL.md +572 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +163 -300
  5. data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
  6. data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
  7. data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
  8. data/.claude/skills/plutonium-testing/SKILL.md +6 -5
  9. data/.claude/skills/plutonium-ui/SKILL.md +900 -0
  10. data/CHANGELOG.md +37 -0
  11. data/Rakefile +2 -1
  12. data/app/assets/plutonium.css +1 -11
  13. data/app/assets/plutonium.js +1323 -1184
  14. data/app/assets/plutonium.js.map +4 -4
  15. data/app/assets/plutonium.min.js +50 -49
  16. data/app/assets/plutonium.min.js.map +4 -4
  17. data/app/views/plutonium/_resource_header.html.erb +4 -4
  18. data/app/views/plutonium/_resource_sidebar.html.erb +9 -9
  19. data/app/views/resource/_resource_grid.html.erb +1 -0
  20. data/config/brakeman.ignore +25 -2
  21. data/docs/.vitepress/config.ts +37 -27
  22. data/docs/getting-started/index.md +22 -29
  23. data/docs/getting-started/installation.md +37 -80
  24. data/docs/getting-started/tutorial/index.md +4 -5
  25. data/docs/guides/adding-resources.md +66 -377
  26. data/docs/guides/authentication.md +94 -463
  27. data/docs/guides/authorization.md +124 -370
  28. data/docs/guides/creating-packages.md +94 -296
  29. data/docs/guides/custom-actions.md +121 -441
  30. data/docs/guides/index.md +22 -42
  31. data/docs/guides/multi-tenancy.md +116 -187
  32. data/docs/guides/nested-resources.md +103 -431
  33. data/docs/guides/search-filtering.md +123 -240
  34. data/docs/guides/testing.md +5 -4
  35. data/docs/guides/theming.md +157 -407
  36. data/docs/guides/troubleshooting.md +5 -3
  37. data/docs/guides/user-invites.md +106 -425
  38. data/docs/guides/user-profile.md +76 -243
  39. data/docs/index.md +1 -1
  40. data/docs/reference/app/generators.md +517 -0
  41. data/docs/reference/app/index.md +158 -0
  42. data/docs/reference/app/packages.md +146 -0
  43. data/docs/reference/app/portals.md +377 -0
  44. data/docs/reference/auth/accounts.md +230 -0
  45. data/docs/reference/auth/index.md +88 -0
  46. data/docs/reference/auth/profile.md +185 -0
  47. data/docs/reference/behavior/controllers.md +395 -0
  48. data/docs/reference/behavior/index.md +22 -0
  49. data/docs/reference/behavior/interactions.md +341 -0
  50. data/docs/reference/behavior/policies.md +417 -0
  51. data/docs/reference/index.md +56 -49
  52. data/docs/reference/resource/actions.md +423 -0
  53. data/docs/reference/resource/definition.md +508 -0
  54. data/docs/reference/resource/index.md +50 -0
  55. data/docs/reference/resource/model.md +348 -0
  56. data/docs/reference/resource/query.md +305 -0
  57. data/docs/reference/tenancy/entity-scoping.md +361 -0
  58. data/docs/reference/tenancy/index.md +36 -0
  59. data/docs/reference/tenancy/invites.md +393 -0
  60. data/docs/reference/tenancy/nested-resources.md +267 -0
  61. data/docs/reference/testing/index.md +287 -0
  62. data/docs/reference/ui/assets.md +400 -0
  63. data/docs/reference/ui/components.md +165 -0
  64. data/docs/reference/ui/displays.md +104 -0
  65. data/docs/reference/ui/forms.md +284 -0
  66. data/docs/reference/ui/index.md +30 -0
  67. data/docs/reference/ui/layouts.md +106 -0
  68. data/docs/reference/ui/pages.md +189 -0
  69. data/docs/reference/ui/tables.md +117 -0
  70. data/docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md +841 -0
  71. data/docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md.tasks.json +103 -0
  72. data/docs/superpowers/specs/2026-05-07-ui-layout-overhaul-design.md +270 -0
  73. data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
  74. data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
  75. data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
  76. data/gemfiles/rails_7.gemfile.lock +1 -1
  77. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  78. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  79. data/lib/generators/pu/core/install/templates/config/initializers/plutonium.rb +1 -0
  80. data/lib/generators/pu/invites/install_generator.rb +1 -0
  81. data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +54 -5
  82. data/lib/plutonium/action/base.rb +44 -1
  83. data/lib/plutonium/action/interactive.rb +1 -1
  84. data/lib/plutonium/configuration.rb +4 -0
  85. data/lib/plutonium/definition/actions.rb +3 -0
  86. data/lib/plutonium/definition/base.rb +8 -0
  87. data/lib/plutonium/definition/index_views.rb +95 -0
  88. data/lib/plutonium/definition/metadata.rb +40 -0
  89. data/lib/plutonium/helpers/turbo_helper.rb +12 -1
  90. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
  91. data/lib/plutonium/interaction/response/redirect.rb +1 -1
  92. data/lib/plutonium/query/base.rb +8 -0
  93. data/lib/plutonium/query/filters/association.rb +30 -8
  94. data/lib/plutonium/query/filters/boolean.rb +5 -0
  95. data/lib/plutonium/resource/controller.rb +1 -0
  96. data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
  97. data/lib/plutonium/resource/controllers/presentable.rb +11 -2
  98. data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
  99. data/lib/plutonium/resource/definition.rb +42 -0
  100. data/lib/plutonium/resource/policy.rb +7 -0
  101. data/lib/plutonium/resource/query_object.rb +64 -6
  102. data/lib/plutonium/routing/mapper_extensions.rb +15 -0
  103. data/lib/plutonium/testing/resource_definition.rb +2 -2
  104. data/lib/plutonium/ui/action_button.rb +4 -2
  105. data/lib/plutonium/ui/component/kit.rb +12 -0
  106. data/lib/plutonium/ui/component/methods.rb +4 -0
  107. data/lib/plutonium/ui/display/base.rb +3 -1
  108. data/lib/plutonium/ui/display/resource.rb +109 -25
  109. data/lib/plutonium/ui/display/theme.rb +2 -1
  110. data/lib/plutonium/ui/dyna_frame/content.rb +8 -14
  111. data/lib/plutonium/ui/empty_card.rb +1 -1
  112. data/lib/plutonium/ui/form/base.rb +35 -3
  113. data/lib/plutonium/ui/form/components/hidden_wrapper.rb +25 -0
  114. data/lib/plutonium/ui/form/components/json.rb +58 -0
  115. data/lib/plutonium/ui/form/components/resource_select.rb +133 -1
  116. data/lib/plutonium/ui/form/components/secure_association.rb +105 -24
  117. data/lib/plutonium/ui/form/components/sticky_footer.rb +17 -0
  118. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
  119. data/lib/plutonium/ui/form/resource.rb +45 -10
  120. data/lib/plutonium/ui/form/theme.rb +1 -1
  121. data/lib/plutonium/ui/frame_navigator_panel.rb +7 -4
  122. data/lib/plutonium/ui/grid/card.rb +235 -0
  123. data/lib/plutonium/ui/grid/resource.rb +149 -0
  124. data/lib/plutonium/ui/layout/base.rb +38 -1
  125. data/lib/plutonium/ui/layout/header.rb +1 -2
  126. data/lib/plutonium/ui/layout/icon_rail.rb +212 -0
  127. data/lib/plutonium/ui/layout/resource_layout.rb +10 -3
  128. data/lib/plutonium/ui/layout/sidebar.rb +12 -24
  129. data/lib/plutonium/ui/layout/topbar.rb +100 -0
  130. data/lib/plutonium/ui/modal/base.rb +109 -0
  131. data/lib/plutonium/ui/modal/centered.rb +21 -0
  132. data/lib/plutonium/ui/modal/slideover.rb +26 -0
  133. data/lib/plutonium/ui/page/base.rb +18 -6
  134. data/lib/plutonium/ui/page/edit.rb +13 -1
  135. data/lib/plutonium/ui/page/index.rb +40 -1
  136. data/lib/plutonium/ui/page/interactive_action.rb +8 -39
  137. data/lib/plutonium/ui/page/new.rb +13 -1
  138. data/lib/plutonium/ui/page/show.rb +8 -1
  139. data/lib/plutonium/ui/page_header.rb +8 -13
  140. data/lib/plutonium/ui/panel.rb +10 -19
  141. data/lib/plutonium/ui/sidebar_menu.rb +2 -25
  142. data/lib/plutonium/ui/tab_list.rb +29 -7
  143. data/lib/plutonium/ui/table/base.rb +106 -0
  144. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +12 -4
  145. data/lib/plutonium/ui/table/components/filter_form.rb +171 -0
  146. data/lib/plutonium/ui/table/components/filter_pills.rb +89 -0
  147. data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +13 -12
  148. data/lib/plutonium/ui/table/components/scopes_pills.rb +67 -0
  149. data/lib/plutonium/ui/table/components/selection_column.rb +2 -11
  150. data/lib/plutonium/ui/table/components/toolbar.rb +104 -0
  151. data/lib/plutonium/ui/table/components/view_switcher.rb +81 -0
  152. data/lib/plutonium/ui/table/resource.rb +158 -89
  153. data/lib/plutonium/ui/table/theme.rb +14 -5
  154. data/lib/plutonium/version.rb +1 -1
  155. data/lib/plutonium.rb +14 -0
  156. data/lib/tasks/release.rake +15 -1
  157. data/package.json +10 -10
  158. data/src/css/components.css +304 -131
  159. data/src/css/slim_select.css +4 -0
  160. data/src/css/tokens.css +101 -85
  161. data/src/js/controllers/autosubmit_controller.js +24 -0
  162. data/src/js/controllers/bulk_actions_controller.js +15 -16
  163. data/src/js/controllers/capture_url_controller.js +14 -0
  164. data/src/js/controllers/filter_panel_controller.js +77 -19
  165. data/src/js/controllers/frame_navigator_controller.js +34 -6
  166. data/src/js/controllers/icon_rail_controller.js +22 -0
  167. data/src/js/controllers/icon_rail_flyout_controller.js +128 -0
  168. data/src/js/controllers/register_controllers.js +16 -0
  169. data/src/js/controllers/resource_tab_list_controller.js +56 -3
  170. data/src/js/controllers/row_click_controller.js +21 -0
  171. data/src/js/controllers/slim_select_controller.js +61 -0
  172. data/src/js/controllers/table_column_menu_controller.js +43 -0
  173. data/src/js/controllers/table_header_controller.js +16 -0
  174. data/src/js/controllers/view_switcher_controller.js +29 -0
  175. data/src/js/turbo/turbo_actions.js +33 -0
  176. data/yarn.lock +553 -543
  177. metadata +71 -32
  178. data/.claude/skills/plutonium-assets/SKILL.md +0 -512
  179. data/.claude/skills/plutonium-controller/SKILL.md +0 -396
  180. data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
  181. data/.claude/skills/plutonium-definition/SKILL.md +0 -1138
  182. data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
  183. data/.claude/skills/plutonium-forms/SKILL.md +0 -465
  184. data/.claude/skills/plutonium-installation/SKILL.md +0 -325
  185. data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
  186. data/.claude/skills/plutonium-invites/SKILL.md +0 -408
  187. data/.claude/skills/plutonium-model/SKILL.md +0 -440
  188. data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
  189. data/.claude/skills/plutonium-package/SKILL.md +0 -198
  190. data/.claude/skills/plutonium-policy/SKILL.md +0 -456
  191. data/.claude/skills/plutonium-portal/SKILL.md +0 -410
  192. data/.claude/skills/plutonium-views/SKILL.md +0 -592
  193. data/docs/reference/assets/index.md +0 -496
  194. data/docs/reference/controller/index.md +0 -412
  195. data/docs/reference/definition/actions.md +0 -449
  196. data/docs/reference/definition/fields.md +0 -383
  197. data/docs/reference/definition/index.md +0 -268
  198. data/docs/reference/definition/query.md +0 -351
  199. data/docs/reference/generators/index.md +0 -648
  200. data/docs/reference/interaction/index.md +0 -449
  201. data/docs/reference/model/features.md +0 -248
  202. data/docs/reference/model/index.md +0 -218
  203. data/docs/reference/policy/index.md +0 -456
  204. data/docs/reference/portal/index.md +0 -379
  205. data/docs/reference/views/forms.md +0 -411
  206. data/docs/reference/views/index.md +0 -501
@@ -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) |