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,341 @@
|
|
|
1
|
+
# Interaction
|
|
2
|
+
|
|
3
|
+
Encapsulate business logic into testable, reusable units. Registered as [actions](/reference/resource/actions) in definitions and executed by the controller. Built on ActiveModel attributes + validations.
|
|
4
|
+
|
|
5
|
+
## 🚨 Critical
|
|
6
|
+
|
|
7
|
+
- **`ActiveRecord::RecordInvalid` is NOT rescued automatically.** Always rescue when using `create!` / `update!` / `save!`, return `failed(e.record.errors)`.
|
|
8
|
+
- **Return `succeed(...)` or `failed(...)` from `execute`** — the controller can't tell what happened otherwise. Returning anything else raises.
|
|
9
|
+
- **Redirect is automatic on success** — only use `with_redirect_response` for a *different* destination.
|
|
10
|
+
- **Bulk actions use `attribute :resources` (plural).** Policy authorization is checked per record — if any fails, the whole request fails.
|
|
11
|
+
- **The shape of the action (record / bulk / resource) is inferred from the interaction's attributes.** See [Resource › Actions](/reference/resource/actions#inferred-visibility-interactive-actions).
|
|
12
|
+
|
|
13
|
+
## Structure
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# app/interactions/resource_interaction.rb — installed once
|
|
17
|
+
class ResourceInteraction < Plutonium::Resource::Interaction
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# A real interaction
|
|
21
|
+
class PublishPostInteraction < ResourceInteraction
|
|
22
|
+
presents label: "Publish",
|
|
23
|
+
icon: Phlex::TablerIcons::Send,
|
|
24
|
+
description: "Make this post public"
|
|
25
|
+
|
|
26
|
+
attribute :resource
|
|
27
|
+
attribute :publish_date, :datetime, default: -> { Time.current }
|
|
28
|
+
|
|
29
|
+
input :publish_date
|
|
30
|
+
|
|
31
|
+
validates :publish_date, presence: true
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def execute
|
|
36
|
+
resource.update!(published_at: publish_date)
|
|
37
|
+
succeed(resource).with_message("Post published!")
|
|
38
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
39
|
+
failed(e.record.errors)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Attributes
|
|
45
|
+
|
|
46
|
+
ActiveModel-style:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
attribute :resource # single record (record action)
|
|
50
|
+
attribute :resources # array of records (bulk action)
|
|
51
|
+
attribute :email, :string
|
|
52
|
+
attribute :count, :integer, default: 1
|
|
53
|
+
attribute :active, :boolean, default: -> { true } # callable default
|
|
54
|
+
attribute :tags, :array
|
|
55
|
+
attribute :metadata, :hash
|
|
56
|
+
attribute :date, :datetime
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The presence of `:resource` / `:resources` / neither determines the action type — see [Resource › Actions › Inferred visibility](/reference/resource/actions#inferred-visibility-interactive-actions).
|
|
60
|
+
|
|
61
|
+
## Inputs
|
|
62
|
+
|
|
63
|
+
Same DSL as definition `input`. Auto-detection from the attribute type applies — declare `as:` only when overriding.
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
input :email # auto: :email type from name match
|
|
67
|
+
input :role, as: :select, choices: %w[admin user]
|
|
68
|
+
input :content, as: :text
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
See [Resource › Definition](/reference/resource/definition#available-field-types) for all `as:` types, options, and dynamic blocks.
|
|
72
|
+
|
|
73
|
+
## Presentation
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
presents label: "Archive Record",
|
|
77
|
+
icon: Phlex::TablerIcons::Archive,
|
|
78
|
+
description: "Move to archive"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Access:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
MyInteraction.label # => "Archive Record"
|
|
85
|
+
MyInteraction.icon # => Phlex::TablerIcons::Archive
|
|
86
|
+
MyInteraction.description # => "Move to archive"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If `action :foo, interaction: FooInteraction` doesn't override `label:` / `icon:` etc., these `presents` values are used.
|
|
90
|
+
|
|
91
|
+
## `execute` — outcomes
|
|
92
|
+
|
|
93
|
+
`execute` MUST return a `succeed(...)` or `failed(...)` outcome. Validations run automatically before `execute`; if they fail, the interaction short-circuits to `failed()`.
|
|
94
|
+
|
|
95
|
+
### Success
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
succeed(resource) # auto-redirect to resource
|
|
99
|
+
succeed(resource).with_message("Done!")
|
|
100
|
+
succeed(resource).with_message("Heads up!", :alert)
|
|
101
|
+
succeed(resource).with_redirect_response(custom_path) # different destination
|
|
102
|
+
succeed(resource).with_file_response(path, filename: "report.pdf")
|
|
103
|
+
succeed(resource).with_render_response(:custom_template)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Failure
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
failed("Something went wrong")
|
|
110
|
+
failed(resource.errors)
|
|
111
|
+
failed(email: "is invalid", name: "is required") # hash form
|
|
112
|
+
failed("Invalid value", :email) # string + attribute
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Manual error addition
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
def execute
|
|
119
|
+
errors.add(:base, "Post must have content")
|
|
120
|
+
return failure if errors.any?
|
|
121
|
+
|
|
122
|
+
# …continue
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Chaining
|
|
127
|
+
|
|
128
|
+
`and_then` chains interactions. On failure, the chain short-circuits and returns the failure immediately.
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
def execute
|
|
132
|
+
CreateUserInteraction.call(view_context:, **user_params)
|
|
133
|
+
.and_then { |r| SendWelcomeEmail.call(view_context:, user: r.value) }
|
|
134
|
+
.and_then { |r| LogActivity.call(view_context:, user: r.value) }
|
|
135
|
+
.with_message("User created and welcomed!")
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Validations
|
|
140
|
+
|
|
141
|
+
Standard ActiveModel. Run automatically before `execute`:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
validates :email, presence: true, format: {with: URI::MailTo::EMAIL_REGEXP}
|
|
145
|
+
validates :role, inclusion: {in: %w[admin user guest]}
|
|
146
|
+
|
|
147
|
+
validate :custom_check
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def custom_check
|
|
152
|
+
errors.add(:resource, "cannot be modified when archived") if resource.archived?
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Accessing context
|
|
157
|
+
|
|
158
|
+
`current_user` is provided by the base class (`view_context.controller.helpers.current_user`):
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
def execute
|
|
162
|
+
resource.update!(updated_by: current_user)
|
|
163
|
+
succeed(resource)
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Interaction types
|
|
168
|
+
|
|
169
|
+
| Attribute pattern | Action type | Where it shows up |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `attribute :resource` | Record action | Show page + per-row in table |
|
|
172
|
+
| `attribute :resources` (plural) | Bulk action | Bulk toolbar above table |
|
|
173
|
+
| neither | Resource action | Index page header |
|
|
174
|
+
|
|
175
|
+
### Record action
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
class ArchiveInteraction < Plutonium::Resource::Interaction
|
|
179
|
+
attribute :resource
|
|
180
|
+
|
|
181
|
+
def execute
|
|
182
|
+
resource.update!(archived: true)
|
|
183
|
+
succeed(resource).with_message("Archived")
|
|
184
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
185
|
+
failed(e.record.errors)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Bulk action
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
class BulkArchiveInteraction < Plutonium::Resource::Interaction
|
|
194
|
+
attribute :resources
|
|
195
|
+
|
|
196
|
+
def execute
|
|
197
|
+
resources.update_all(archived: true)
|
|
198
|
+
succeed(resources).with_message("Archived #{resources.size} records")
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Per-record authorization details in [Resource › Actions › Bulk action](/reference/resource/actions#bulk-action).
|
|
204
|
+
|
|
205
|
+
### Resource action (no record)
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
class ImportInteraction < Plutonium::Resource::Interaction
|
|
209
|
+
attribute :file
|
|
210
|
+
input :file, as: :file
|
|
211
|
+
validates :file, presence: true
|
|
212
|
+
|
|
213
|
+
def execute
|
|
214
|
+
# …import logic
|
|
215
|
+
succeed(nil).with_message("Import completed.")
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Calling interactions directly
|
|
221
|
+
|
|
222
|
+
The controller handles this for interactive actions. But you can call them manually too — useful in tests, jobs, and rake tasks.
|
|
223
|
+
|
|
224
|
+
### Class method
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
outcome = PublishPost.call(view_context: view_context, resource: post)
|
|
228
|
+
|
|
229
|
+
if outcome.success?
|
|
230
|
+
# …
|
|
231
|
+
else
|
|
232
|
+
# …
|
|
233
|
+
end
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Instance method
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
interaction = PublishPost.new(view_context: view_context, resource: post)
|
|
240
|
+
outcome = interaction.call
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The `view_context:` argument is required — interactions use it to access controller helpers and the current user.
|
|
244
|
+
|
|
245
|
+
## Immediate vs form
|
|
246
|
+
|
|
247
|
+
| Interaction shape | Behavior |
|
|
248
|
+
|---|---|
|
|
249
|
+
| Only `:resource` / `:resources` (no extra `attribute` or `input`) | **Immediate** — browser confirmation (`"#{label}?"`, e.g. `"Archive?"`), then runs. Override with `confirmation: "Custom"` or `confirmation: false` on the action. |
|
|
250
|
+
| Additional `attribute` / `input` declared | **Form** — renders modal form first; no auto-confirmation. |
|
|
251
|
+
|
|
252
|
+
See [Resource › Actions › Immediate vs form](/reference/resource/actions#immediate-vs-form).
|
|
253
|
+
|
|
254
|
+
## Generating interaction URLs
|
|
255
|
+
|
|
256
|
+
`resource_url_for` with the `interaction:` kwarg. The action type (record / bulk / resource) is inferred from the element and the presence of `ids:`:
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
# Record action — instance argument
|
|
260
|
+
resource_url_for(@post, interaction: :publish)
|
|
261
|
+
# => /posts/:id/record_actions/publish
|
|
262
|
+
|
|
263
|
+
# Resource action — class, no ids
|
|
264
|
+
resource_url_for(Post, interaction: :import)
|
|
265
|
+
# => /posts/resource_actions/import
|
|
266
|
+
|
|
267
|
+
# Bulk action — class + ids
|
|
268
|
+
resource_url_for(Post, interaction: :archive, ids: [1, 2, 3])
|
|
269
|
+
# => /posts/bulk_actions/archive?ids[]=1&ids[]=2&ids[]=3
|
|
270
|
+
|
|
271
|
+
# Composes with parent / entity scoping
|
|
272
|
+
resource_url_for(@post, parent: @user, interaction: :publish)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The same URL serves GET (form/confirmation) and POST (commit) — the HTTP verb routes to the right controller action. Passing both `interaction:` and `action:` raises `ArgumentError`.
|
|
276
|
+
|
|
277
|
+
## Complete example
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
class Company::InviteUserInteraction < Plutonium::Resource::Interaction
|
|
281
|
+
presents label: "Invite User",
|
|
282
|
+
icon: Phlex::TablerIcons::UserPlus
|
|
283
|
+
|
|
284
|
+
attribute :resource # the company
|
|
285
|
+
attribute :email, :string
|
|
286
|
+
attribute :role, :string
|
|
287
|
+
|
|
288
|
+
input :email
|
|
289
|
+
input :role, as: :select, choices: -> { UserInvite.roles.keys }
|
|
290
|
+
|
|
291
|
+
validates :email, presence: true, format: {with: URI::MailTo::EMAIL_REGEXP}
|
|
292
|
+
validates :role, presence: true, inclusion: {in: UserInvite.roles.keys}
|
|
293
|
+
validate :not_already_invited
|
|
294
|
+
|
|
295
|
+
private
|
|
296
|
+
|
|
297
|
+
def execute
|
|
298
|
+
invite = UserInvite.create!(
|
|
299
|
+
company: resource, email: email, role: role,
|
|
300
|
+
invited_by: current_user
|
|
301
|
+
)
|
|
302
|
+
UserInviteMailer.invitation(invite).deliver_later
|
|
303
|
+
succeed(resource).with_message("Invitation sent to #{email}")
|
|
304
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
305
|
+
failed(e.record.errors)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def not_already_invited
|
|
309
|
+
return unless email.present?
|
|
310
|
+
if UserInvite.exists?(company: resource, email: email, state: :pending)
|
|
311
|
+
errors.add(:email, "already has a pending invitation")
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Testing
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
RSpec.describe PublishPost do
|
|
321
|
+
let(:view_context) { double("view_context", controller: double(helpers: double(current_user: user))) }
|
|
322
|
+
let(:user) { create(:user) }
|
|
323
|
+
let(:post) { create(:post, user: user, published: false) }
|
|
324
|
+
|
|
325
|
+
it "publishes the post" do
|
|
326
|
+
outcome = described_class.call(view_context: view_context, resource: post)
|
|
327
|
+
|
|
328
|
+
expect(outcome).to be_success
|
|
329
|
+
expect(post.reload).to be_published
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
See [Testing](/reference/testing/) for Plutonium's built-in testing helpers — `ResourceInteraction` concern wraps these patterns.
|
|
335
|
+
|
|
336
|
+
## Related
|
|
337
|
+
|
|
338
|
+
- [Resource › Actions](/reference/resource/actions) — registering interactions, inferred visibility, immediate vs form
|
|
339
|
+
- [Policies](./policies) — `def <action>?` authorization methods
|
|
340
|
+
- [Controllers](./controllers) — `resource_url_for(..., interaction: …)` URL generation
|
|
341
|
+
- [UI › Forms](/reference/ui/forms) — customizing the modal form rendered for actions with inputs
|