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
|
@@ -1,413 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plutonium-interaction
|
|
3
|
-
description: Use BEFORE writing an interaction class, encapsulating business logic, or building multi-step operations beyond basic CRUD. Covers Plutonium::Resource::Interaction.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Plutonium Interactions
|
|
7
|
-
|
|
8
|
-
## 🚨 Critical (read first)
|
|
9
|
-
- **`ActiveRecord::RecordInvalid` is NOT rescued automatically.** Always rescue it when using `create!`/`update!`/`save!` and return `failed(e.record.errors)`.
|
|
10
|
-
- **Return `succeed(...)` or `failed(...)`** from `execute` — the controller won't know what happened otherwise.
|
|
11
|
-
- **Redirect is automatic on success.** Only call `with_redirect_response` for a *different* destination.
|
|
12
|
-
- **Bulk actions use `resources` (plural).** Policy methods are checked per record; if any fails, the whole request fails.
|
|
13
|
-
- **Related skills:** `plutonium-definition` (registering actions), `plutonium-policy` (authorizing actions), `plutonium-forms` (interaction form templates).
|
|
14
|
-
|
|
15
|
-
Interactions encapsulate business logic into reusable, testable units. They handle input validation, execution, and outcomes.
|
|
16
|
-
|
|
17
|
-
## Basic Structure
|
|
18
|
-
|
|
19
|
-
```ruby
|
|
20
|
-
# app/interactions/resource_interaction.rb (generated during install)
|
|
21
|
-
class ResourceInteraction < Plutonium::Resource::Interaction
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# app/interactions/publish_post_interaction.rb
|
|
25
|
-
class PublishPostInteraction < ResourceInteraction
|
|
26
|
-
# Presentation
|
|
27
|
-
presents label: "Publish",
|
|
28
|
-
icon: Phlex::TablerIcons::Send,
|
|
29
|
-
description: "Make this post public"
|
|
30
|
-
|
|
31
|
-
# Attributes (inputs)
|
|
32
|
-
attribute :resource # The record being acted upon
|
|
33
|
-
attribute :publish_date, :datetime, default: -> { Time.current }
|
|
34
|
-
|
|
35
|
-
# Form inputs (what user sees)
|
|
36
|
-
input :publish_date, as: :datetime
|
|
37
|
-
|
|
38
|
-
# Validations
|
|
39
|
-
validates :publish_date, presence: true
|
|
40
|
-
|
|
41
|
-
private
|
|
42
|
-
|
|
43
|
-
def execute
|
|
44
|
-
resource.update!(published_at: publish_date)
|
|
45
|
-
succeed(resource).with_message("Post published!")
|
|
46
|
-
rescue ActiveRecord::RecordInvalid => e
|
|
47
|
-
failed(e.record.errors)
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## Attributes
|
|
53
|
-
|
|
54
|
-
Define inputs using ActiveModel attributes:
|
|
55
|
-
|
|
56
|
-
```ruby
|
|
57
|
-
attribute :resource # Record (for record actions)
|
|
58
|
-
attribute :resources # Collection (for bulk actions)
|
|
59
|
-
attribute :email, :string # String input
|
|
60
|
-
attribute :count, :integer, default: 1 # With default
|
|
61
|
-
attribute :active, :boolean, default: -> { true } # Callable default
|
|
62
|
-
attribute :tags, :array # Array
|
|
63
|
-
attribute :metadata, :hash # Hash
|
|
64
|
-
attribute :date, :datetime # DateTime
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Form Inputs
|
|
68
|
-
|
|
69
|
-
Define form fields with the `input` method (same as definitions):
|
|
70
|
-
|
|
71
|
-
```ruby
|
|
72
|
-
input :email
|
|
73
|
-
input :role, as: :select, choices: %w[admin user]
|
|
74
|
-
input :content, as: :text
|
|
75
|
-
input :date, as: :date
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
See `plutonium-definition` skill for all input types and options.
|
|
79
|
-
|
|
80
|
-
## Presentation
|
|
81
|
-
|
|
82
|
-
Configure how the action appears in the UI:
|
|
83
|
-
|
|
84
|
-
```ruby
|
|
85
|
-
presents label: "Archive Record",
|
|
86
|
-
icon: Phlex::TablerIcons::Archive,
|
|
87
|
-
description: "Move to archive for later reference"
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Access presentation:
|
|
91
|
-
```ruby
|
|
92
|
-
MyInteraction.label # => "Archive Record"
|
|
93
|
-
MyInteraction.icon # => Phlex::TablerIcons::Archive
|
|
94
|
-
MyInteraction.description # => "Move to archive..."
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
## Execution and Outcomes
|
|
98
|
-
|
|
99
|
-
### The execute Method
|
|
100
|
-
|
|
101
|
-
```ruby
|
|
102
|
-
private
|
|
103
|
-
|
|
104
|
-
def execute
|
|
105
|
-
# Your business logic here
|
|
106
|
-
# Must return succeed() or failed()
|
|
107
|
-
end
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### Success Outcomes
|
|
111
|
-
|
|
112
|
-
```ruby
|
|
113
|
-
# Basic success (redirects automatically to resource)
|
|
114
|
-
succeed(resource)
|
|
115
|
-
|
|
116
|
-
# With message
|
|
117
|
-
succeed(resource).with_message("Done!")
|
|
118
|
-
succeed(resource).with_message("Warning!", :alert)
|
|
119
|
-
|
|
120
|
-
# With custom redirect (only if different from default)
|
|
121
|
-
succeed(resource).with_redirect_response(custom_path)
|
|
122
|
-
|
|
123
|
-
# With file download
|
|
124
|
-
succeed(resource).with_file_response(file_path, filename: "report.pdf")
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
**Note:** Redirect is automatic on success - the controller redirects to the resource by default. Only use `with_redirect_response` if you need a different destination.
|
|
128
|
-
|
|
129
|
-
### Failure Outcomes
|
|
130
|
-
|
|
131
|
-
```ruby
|
|
132
|
-
# Basic failure
|
|
133
|
-
failed("Something went wrong")
|
|
134
|
-
|
|
135
|
-
# With ActiveModel errors
|
|
136
|
-
failed(resource.errors)
|
|
137
|
-
|
|
138
|
-
# With hash of errors
|
|
139
|
-
failed(email: "is invalid", name: "is required")
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
### Chaining Interactions
|
|
143
|
-
|
|
144
|
-
```ruby
|
|
145
|
-
def execute
|
|
146
|
-
CreateUserInteraction.call(view_context:, **user_params)
|
|
147
|
-
.and_then { |result| SendWelcomeEmail.call(view_context:, user: result.value) }
|
|
148
|
-
.and_then { |result| LogActivity.call(view_context:, user: result.value) }
|
|
149
|
-
.with_message("User created and welcomed!")
|
|
150
|
-
end
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
On failure, the chain short-circuits and returns the failure immediately.
|
|
154
|
-
|
|
155
|
-
## Validations
|
|
156
|
-
|
|
157
|
-
Use standard ActiveModel validations:
|
|
158
|
-
|
|
159
|
-
```ruby
|
|
160
|
-
validates :email, presence: true
|
|
161
|
-
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
162
|
-
validates :role, inclusion: { in: %w[admin user guest] }
|
|
163
|
-
|
|
164
|
-
validate :custom_validation
|
|
165
|
-
|
|
166
|
-
private
|
|
167
|
-
|
|
168
|
-
def custom_validation
|
|
169
|
-
if resource.archived?
|
|
170
|
-
errors.add(:resource, "cannot be modified when archived")
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
Validations run automatically before `execute`. If invalid, returns `failed()` with errors.
|
|
176
|
-
|
|
177
|
-
## Interaction Types
|
|
178
|
-
|
|
179
|
-
### Record Actions
|
|
180
|
-
|
|
181
|
-
Act on a single record:
|
|
182
|
-
|
|
183
|
-
```ruby
|
|
184
|
-
class ArchiveInteraction < Plutonium::Resource::Interaction
|
|
185
|
-
attribute :resource # Single record
|
|
186
|
-
|
|
187
|
-
def execute
|
|
188
|
-
resource.update!(archived: true)
|
|
189
|
-
succeed(resource)
|
|
190
|
-
rescue ActiveRecord::RecordInvalid => e
|
|
191
|
-
failed(e.record.errors)
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
**Note:** `ActiveRecord::RecordInvalid` is NOT rescued automatically. Always rescue it when using bang methods (`create!`, `update!`, `save!`).
|
|
197
|
-
|
|
198
|
-
### Resource Actions
|
|
199
|
-
|
|
200
|
-
Act at the collection/class level (no specific record):
|
|
201
|
-
|
|
202
|
-
```ruby
|
|
203
|
-
class ImportInteraction < Plutonium::Resource::Interaction
|
|
204
|
-
# No :resource attribute
|
|
205
|
-
attribute :file
|
|
206
|
-
|
|
207
|
-
input :file, as: :file
|
|
208
|
-
|
|
209
|
-
def execute
|
|
210
|
-
records = CSV.parse(file)
|
|
211
|
-
Post.import(records)
|
|
212
|
-
succeed(records)
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### Bulk Actions (Multiple Records)
|
|
218
|
-
|
|
219
|
-
Act on multiple selected records. When registered, the table shows checkboxes and a toolbar appears when records are selected.
|
|
220
|
-
|
|
221
|
-
```ruby
|
|
222
|
-
class BulkArchiveInteraction < Plutonium::Resource::Interaction
|
|
223
|
-
attribute :resources # Collection of records (note: plural)
|
|
224
|
-
|
|
225
|
-
def execute
|
|
226
|
-
resources.update_all(archived: true)
|
|
227
|
-
succeed(resources).with_message("Archived #{resources.count} records")
|
|
228
|
-
end
|
|
229
|
-
end
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
**Authorization:** Bulk actions use per-record authorization. The policy method is checked for each selected record - if any fails, the entire request is rejected. The UI only shows actions that all selected records support.
|
|
233
|
-
|
|
234
|
-
## Connecting to Definitions
|
|
235
|
-
|
|
236
|
-
Register interactions as actions:
|
|
237
|
-
|
|
238
|
-
```ruby
|
|
239
|
-
class PostDefinition < ResourceDefinition
|
|
240
|
-
# Record action (shows on individual records)
|
|
241
|
-
action :publish, interaction: PublishPostInteraction
|
|
242
|
-
|
|
243
|
-
# Resource action (shows at collection level)
|
|
244
|
-
action :import, interaction: ImportInteraction
|
|
245
|
-
|
|
246
|
-
# With options
|
|
247
|
-
action :archive,
|
|
248
|
-
interaction: ArchiveInteraction,
|
|
249
|
-
confirmation: "Are you sure?",
|
|
250
|
-
category: :danger,
|
|
251
|
-
position: 100
|
|
252
|
-
end
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
### Action Options
|
|
256
|
-
|
|
257
|
-
| Option | Description |
|
|
258
|
-
|--------|-------------|
|
|
259
|
-
| `interaction:` | The interaction class |
|
|
260
|
-
| `confirmation:` | Confirmation message before execution |
|
|
261
|
-
| `category:` | `:primary`, `:secondary`, `:danger` |
|
|
262
|
-
| `position:` | Display order (lower = first) |
|
|
263
|
-
| `turbo_frame:` | Turbo frame target (default: `remote_modal`) |
|
|
264
|
-
| `icon:` | Override interaction icon |
|
|
265
|
-
| `label:` | Override interaction label |
|
|
266
|
-
|
|
267
|
-
## Policy Integration
|
|
268
|
-
|
|
269
|
-
Control access with policy methods:
|
|
270
|
-
|
|
271
|
-
```ruby
|
|
272
|
-
class PostPolicy < ResourcePolicy
|
|
273
|
-
def publish?
|
|
274
|
-
update? && record.draft?
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
def archive?
|
|
278
|
-
destroy? && !record.archived?
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
def import?
|
|
282
|
-
create? # Resource-level action
|
|
283
|
-
end
|
|
284
|
-
end
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
The policy method name matches the action name with `?`.
|
|
288
|
-
|
|
289
|
-
## Accessing Context
|
|
290
|
-
|
|
291
|
-
Inside interactions:
|
|
292
|
-
|
|
293
|
-
```ruby
|
|
294
|
-
def execute
|
|
295
|
-
# Access current user via view_context
|
|
296
|
-
current_user = view_context.controller.helpers.current_user
|
|
297
|
-
|
|
298
|
-
# Access the resource
|
|
299
|
-
resource.update!(updated_by: current_user)
|
|
300
|
-
|
|
301
|
-
succeed(resource)
|
|
302
|
-
end
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
## Immediate vs Form Actions
|
|
306
|
-
|
|
307
|
-
Plutonium automatically determines if an action needs a form:
|
|
308
|
-
|
|
309
|
-
- **Has inputs defined** → Shows form first (GET), then executes (POST)
|
|
310
|
-
- **No inputs** → Executes immediately (POST with confirmation)
|
|
311
|
-
|
|
312
|
-
```ruby
|
|
313
|
-
# Shows form (has inputs)
|
|
314
|
-
class InviteUserInteraction < Plutonium::Resource::Interaction
|
|
315
|
-
attribute :resource
|
|
316
|
-
attribute :email
|
|
317
|
-
input :email # This triggers form display
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
# Immediate execution (no inputs)
|
|
321
|
-
class ArchiveInteraction < Plutonium::Resource::Interaction
|
|
322
|
-
attribute :resource
|
|
323
|
-
# No inputs = immediate with confirmation
|
|
324
|
-
end
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
## Complete Example
|
|
328
|
-
|
|
329
|
-
```ruby
|
|
330
|
-
class Company::InviteUserInteraction < Plutonium::Resource::Interaction
|
|
331
|
-
presents label: "Invite User",
|
|
332
|
-
icon: Phlex::TablerIcons::UserPlus,
|
|
333
|
-
description: "Send an invitation email"
|
|
334
|
-
|
|
335
|
-
attribute :resource # The company
|
|
336
|
-
attribute :email, :string
|
|
337
|
-
attribute :role, :string
|
|
338
|
-
|
|
339
|
-
input :email
|
|
340
|
-
input :role, as: :select, choices: -> { UserInvite.roles.keys }
|
|
341
|
-
|
|
342
|
-
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
343
|
-
validates :role, presence: true, inclusion: { in: UserInvite.roles.keys }
|
|
344
|
-
validate :not_already_invited
|
|
345
|
-
|
|
346
|
-
private
|
|
347
|
-
|
|
348
|
-
def execute
|
|
349
|
-
invite = UserInvite.create!(
|
|
350
|
-
company: resource,
|
|
351
|
-
email: email,
|
|
352
|
-
role: role,
|
|
353
|
-
invited_by: current_user
|
|
354
|
-
)
|
|
355
|
-
UserInviteMailer.invitation(invite).deliver_later
|
|
356
|
-
|
|
357
|
-
succeed(resource).with_message("Invitation sent to #{email}")
|
|
358
|
-
rescue ActiveRecord::RecordInvalid => e
|
|
359
|
-
failed(e.record.errors)
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
def not_already_invited
|
|
363
|
-
return unless email.present?
|
|
364
|
-
|
|
365
|
-
if UserInvite.exists?(company: resource, email: email, state: :pending)
|
|
366
|
-
errors.add(:email, "already has a pending invitation")
|
|
367
|
-
end
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
def current_user
|
|
371
|
-
view_context.controller.helpers.current_user
|
|
372
|
-
end
|
|
373
|
-
end
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
## Generating Interaction URLs
|
|
377
|
-
|
|
378
|
-
Use the `interaction:` kwarg on `resource_url_for`. The action type (record/bulk/resource) is inferred from the element and presence of `ids:`:
|
|
379
|
-
|
|
380
|
-
```ruby
|
|
381
|
-
# Record action — instance argument
|
|
382
|
-
resource_url_for(@post, interaction: :publish)
|
|
383
|
-
# => /posts/:id/record_actions/publish
|
|
384
|
-
|
|
385
|
-
# Resource (class-level) action — class with no ids
|
|
386
|
-
resource_url_for(Post, interaction: :import)
|
|
387
|
-
# => /posts/resource_actions/import
|
|
388
|
-
|
|
389
|
-
# Bulk action — class + ids
|
|
390
|
-
resource_url_for(Post, interaction: :archive, ids: [1, 2, 3])
|
|
391
|
-
# => /posts/bulk_actions/archive?ids[]=1&ids[]=2&ids[]=3
|
|
392
|
-
|
|
393
|
-
# Composes with parent / entity scoping
|
|
394
|
-
resource_url_for(@post, parent: @user, interaction: :publish)
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
The same URL serves both GET (form/confirmation) and POST (commit) — the verb determines which controller action runs. Passing both `interaction:` and `action:` raises `ArgumentError`.
|
|
398
|
-
|
|
399
|
-
## Best Practices
|
|
400
|
-
|
|
401
|
-
1. **Keep interactions focused** - One action per interaction
|
|
402
|
-
2. **Use validations** - Validate all inputs before execution
|
|
403
|
-
3. **Handle errors gracefully** - Rescue exceptions and return `failed()`
|
|
404
|
-
4. **Return meaningful messages** - Help users understand what happened
|
|
405
|
-
5. **Use `and_then` for chains** - Compose complex workflows from simple interactions
|
|
406
|
-
6. **Test independently** - Interactions are easy to unit test
|
|
407
|
-
|
|
408
|
-
## Related Skills
|
|
409
|
-
|
|
410
|
-
- `plutonium-definition` - Declaring actions in definitions
|
|
411
|
-
- `plutonium-forms` - Custom interaction form templates
|
|
412
|
-
- `plutonium-policy` - Controlling access to actions
|
|
413
|
-
- `plutonium` - How interactions fit in the architecture
|