plutonium 0.33.1 → 0.34.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/# Plutonium: The pre-alpha demo.md +4 -2
- data/.claude/skills/assets/SKILL.md +416 -0
- data/.claude/skills/connect-resource/SKILL.md +112 -0
- data/.claude/skills/controller/SKILL.md +302 -0
- data/.claude/skills/create-resource/SKILL.md +240 -0
- data/.claude/skills/definition/SKILL.md +218 -0
- data/.claude/skills/definition-actions/SKILL.md +386 -0
- data/.claude/skills/definition-fields/SKILL.md +474 -0
- data/.claude/skills/definition-query/SKILL.md +334 -0
- data/.claude/skills/forms/SKILL.md +439 -0
- data/.claude/skills/installation/SKILL.md +300 -0
- data/.claude/skills/interaction/SKILL.md +382 -0
- data/.claude/skills/model/SKILL.md +267 -0
- data/.claude/skills/model-features/SKILL.md +286 -0
- data/.claude/skills/nested-resources/SKILL.md +274 -0
- data/.claude/skills/package/SKILL.md +191 -0
- data/.claude/skills/policy/SKILL.md +352 -0
- data/.claude/skills/portal/SKILL.md +400 -0
- data/.claude/skills/resource/SKILL.md +281 -0
- data/.claude/skills/rodauth/SKILL.md +452 -0
- data/.claude/skills/views/SKILL.md +563 -0
- data/Appraisals +46 -4
- data/CHANGELOG.md +32 -1
- data/app/assets/plutonium.css +2 -2
- data/config/brakeman.ignore +239 -0
- data/config/initializers/action_policy.rb +1 -1
- data/docs/.vitepress/config.ts +132 -47
- data/docs/concepts/architecture.md +226 -0
- data/docs/concepts/auto-detection.md +254 -0
- data/docs/concepts/index.md +61 -0
- data/docs/concepts/packages-portals.md +304 -0
- data/docs/concepts/resources.md +224 -0
- data/docs/cookbook/blog.md +412 -0
- data/docs/cookbook/index.md +289 -0
- data/docs/cookbook/saas.md +481 -0
- data/docs/getting-started/index.md +56 -0
- data/docs/getting-started/installation.md +146 -0
- data/docs/getting-started/tutorial/01-setup.md +118 -0
- data/docs/getting-started/tutorial/02-first-resource.md +180 -0
- data/docs/getting-started/tutorial/03-authentication.md +246 -0
- data/docs/getting-started/tutorial/04-authorization.md +170 -0
- data/docs/getting-started/tutorial/05-custom-actions.md +202 -0
- data/docs/getting-started/tutorial/06-nested-resources.md +147 -0
- data/docs/getting-started/tutorial/07-customizing-ui.md +254 -0
- data/docs/getting-started/tutorial/index.md +64 -0
- data/docs/guides/adding-resources.md +420 -0
- data/docs/guides/authentication.md +551 -0
- data/docs/guides/authorization.md +468 -0
- data/docs/guides/creating-packages.md +380 -0
- data/docs/guides/custom-actions.md +523 -0
- data/docs/guides/index.md +45 -0
- data/docs/guides/multi-tenancy.md +302 -0
- data/docs/guides/nested-resources.md +411 -0
- data/docs/guides/search-filtering.md +266 -0
- data/docs/guides/theming.md +321 -0
- data/docs/index.md +67 -26
- data/docs/public/CLAUDE.md +64 -21
- data/docs/reference/assets/index.md +496 -0
- data/docs/reference/controller/index.md +363 -0
- data/docs/reference/definition/actions.md +400 -0
- data/docs/reference/definition/fields.md +350 -0
- data/docs/reference/definition/index.md +252 -0
- data/docs/reference/definition/query.md +342 -0
- data/docs/reference/generators/index.md +469 -0
- data/docs/reference/index.md +49 -0
- data/docs/reference/interaction/index.md +445 -0
- data/docs/reference/model/features.md +248 -0
- data/docs/reference/model/index.md +219 -0
- data/docs/reference/policy/index.md +385 -0
- data/docs/reference/portal/index.md +382 -0
- data/docs/reference/views/forms.md +396 -0
- data/docs/reference/views/index.md +479 -0
- data/gemfiles/rails_7.gemfile +9 -2
- data/gemfiles/rails_7.gemfile.lock +146 -111
- data/gemfiles/rails_8.0.gemfile +20 -0
- data/gemfiles/rails_8.0.gemfile.lock +417 -0
- data/gemfiles/rails_8.1.gemfile +20 -0
- data/gemfiles/rails_8.1.gemfile.lock +419 -0
- data/lib/generators/pu/gem/dotenv/templates/.env +2 -0
- data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -1
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +13 -16
- data/lib/generators/pu/pkg/portal/USAGE +65 -0
- data/lib/generators/pu/pkg/portal/portal_generator.rb +22 -9
- data/lib/generators/pu/res/conn/USAGE +71 -0
- data/lib/generators/pu/res/model/USAGE +106 -110
- data/lib/generators/pu/res/model/templates/model.rb.tt +6 -2
- data/lib/generators/pu/res/scaffold/USAGE +85 -0
- data/lib/generators/pu/rodauth/install_generator.rb +2 -6
- data/lib/generators/pu/rodauth/templates/config/initializers/url_options.rb +17 -0
- data/lib/generators/pu/skills/sync/USAGE +14 -0
- data/lib/generators/pu/skills/sync/sync_generator.rb +66 -0
- data/lib/plutonium/action_policy/sti_policy_lookup.rb +1 -1
- data/lib/plutonium/core/controller.rb +2 -2
- data/lib/plutonium/interaction/base.rb +1 -0
- data/lib/plutonium/package/engine.rb +2 -2
- data/lib/plutonium/query/adhoc_block.rb +6 -2
- data/lib/plutonium/query/model_scope.rb +1 -1
- data/lib/plutonium/railtie.rb +4 -0
- data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
- data/lib/plutonium/resource/query_object.rb +38 -8
- data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
- data/lib/plutonium/version.rb +1 -1
- data/lib/tasks/release.rake +19 -4
- data/package.json +1 -1
- metadata +76 -39
- data/brakeman.ignore +0 -28
- data/docs/api-examples.md +0 -49
- data/docs/guide/claude-code-guide.md +0 -74
- data/docs/guide/deep-dive/authorization.md +0 -189
- data/docs/guide/deep-dive/multitenancy.md +0 -256
- data/docs/guide/deep-dive/resources.md +0 -390
- data/docs/guide/getting-started/01-installation.md +0 -165
- data/docs/guide/index.md +0 -28
- data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
- data/docs/guide/introduction/02-core-concepts.md +0 -440
- data/docs/guide/tutorial/01-project-setup.md +0 -75
- data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
- data/docs/guide/tutorial/03-defining-resources.md +0 -90
- data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
- data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
- data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
- data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
- data/docs/markdown-examples.md +0 -85
- data/docs/modules/action.md +0 -244
- data/docs/modules/authentication.md +0 -236
- data/docs/modules/configuration.md +0 -599
- data/docs/modules/controller.md +0 -443
- data/docs/modules/core.md +0 -316
- data/docs/modules/definition.md +0 -1308
- data/docs/modules/display.md +0 -759
- data/docs/modules/form.md +0 -495
- data/docs/modules/generator.md +0 -400
- data/docs/modules/index.md +0 -167
- data/docs/modules/interaction.md +0 -642
- data/docs/modules/package.md +0 -151
- data/docs/modules/policy.md +0 -176
- data/docs/modules/portal.md +0 -710
- data/docs/modules/query.md +0 -297
- data/docs/modules/resource_record.md +0 -618
- data/docs/modules/routing.md +0 -690
- data/docs/modules/table.md +0 -301
- data/docs/modules/ui.md +0 -631
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
# Custom Actions
|
|
2
|
+
|
|
3
|
+
This guide covers adding custom actions beyond standard CRUD operations.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Custom actions let you add buttons like "Publish", "Archive", or "Send Invoice" to your resources. Actions can be:
|
|
8
|
+
|
|
9
|
+
- **Simple** - Navigation to another page
|
|
10
|
+
- **Interactive** - Execute business logic with optional user input
|
|
11
|
+
|
|
12
|
+
## Action Types
|
|
13
|
+
|
|
14
|
+
| Type | Shows In | Use Case |
|
|
15
|
+
|------|----------|----------|
|
|
16
|
+
| `resource_action` | Index page | Import, Export, Create new |
|
|
17
|
+
| `record_action` | Show page | Edit, Delete, Archive |
|
|
18
|
+
| `collection_record_action` | Table rows | Quick actions per row |
|
|
19
|
+
| `bulk_action` | Selected records | Bulk operations |
|
|
20
|
+
|
|
21
|
+
## Simple Actions (Navigation)
|
|
22
|
+
|
|
23
|
+
For actions that just navigate somewhere (the target route must already exist):
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
class PostDefinition < ResourceDefinition
|
|
27
|
+
# Link to external URL
|
|
28
|
+
action :documentation,
|
|
29
|
+
label: "Documentation",
|
|
30
|
+
route_options: {url: "https://docs.example.com"},
|
|
31
|
+
icon: Phlex::TablerIcons::Book,
|
|
32
|
+
resource_action: true
|
|
33
|
+
|
|
34
|
+
# Link to custom controller action
|
|
35
|
+
action :reports,
|
|
36
|
+
route_options: {action: :reports},
|
|
37
|
+
icon: Phlex::TablerIcons::ChartBar,
|
|
38
|
+
resource_action: true
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Note:** For custom operations with business logic, use Interactive Actions with an Interaction class.
|
|
43
|
+
|
|
44
|
+
## Interactive Actions with Interactions
|
|
45
|
+
|
|
46
|
+
For actions that execute business logic, use Interactions.
|
|
47
|
+
|
|
48
|
+
### Creating an Interaction
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
# app/interactions/resource_interaction.rb (generated during install)
|
|
52
|
+
class ResourceInteraction < Plutonium::Resource::Interaction
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# app/interactions/publish_post_interaction.rb
|
|
56
|
+
class PublishPostInteraction < ResourceInteraction
|
|
57
|
+
# UI configuration
|
|
58
|
+
presents label: "Publish Post",
|
|
59
|
+
icon: Phlex::TablerIcons::Send,
|
|
60
|
+
description: "Make this post public"
|
|
61
|
+
|
|
62
|
+
# The record being acted on
|
|
63
|
+
attribute :resource
|
|
64
|
+
|
|
65
|
+
# Validation
|
|
66
|
+
validate :not_already_published
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Main logic
|
|
71
|
+
def execute
|
|
72
|
+
resource.update!(
|
|
73
|
+
published: true,
|
|
74
|
+
published_at: Time.current
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
succeed(resource)
|
|
78
|
+
.with_message("Post published successfully!")
|
|
79
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
80
|
+
failed(e.record.errors)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def not_already_published
|
|
84
|
+
if resource.published?
|
|
85
|
+
errors.add(:base, "Post is already published")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Registering the Action
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
class PostDefinition < ResourceDefinition
|
|
95
|
+
action :publish, interaction: PublishPostInteraction
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Authorizing the Action
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
class PostPolicy < ResourcePolicy
|
|
103
|
+
def publish?
|
|
104
|
+
update? && !record.published?
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Actions with User Input
|
|
110
|
+
|
|
111
|
+
Interactions can accept user input via attributes:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
class SchedulePostInteraction < ResourceInteraction
|
|
115
|
+
presents label: "Schedule Publication",
|
|
116
|
+
icon: Phlex::TablerIcons::Calendar
|
|
117
|
+
|
|
118
|
+
# The record
|
|
119
|
+
attribute :resource
|
|
120
|
+
|
|
121
|
+
# User inputs
|
|
122
|
+
attribute :publish_at, :datetime
|
|
123
|
+
attribute :notify_subscribers, :boolean, default: true
|
|
124
|
+
|
|
125
|
+
# Configure form inputs
|
|
126
|
+
input :publish_at, as: :datetime
|
|
127
|
+
input :notify_subscribers, as: :boolean
|
|
128
|
+
|
|
129
|
+
# Validations
|
|
130
|
+
validates :publish_at, presence: true
|
|
131
|
+
validate :publish_at_in_future
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def execute
|
|
136
|
+
resource.update!(
|
|
137
|
+
scheduled_at: publish_at,
|
|
138
|
+
notify_on_publish: notify_subscribers
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
SchedulePublicationJob.perform_at(publish_at, resource.id)
|
|
142
|
+
|
|
143
|
+
succeed(resource)
|
|
144
|
+
.with_message("Post scheduled for #{publish_at.strftime('%B %d at %I:%M %p')}")
|
|
145
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
146
|
+
failed(e.record.errors)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def publish_at_in_future
|
|
150
|
+
if publish_at.present? && publish_at <= Time.current
|
|
151
|
+
errors.add(:publish_at, "must be in the future")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Register with the definition:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
action :schedule, interaction: SchedulePostInteraction
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Now users see a form with date picker and checkbox before execution.
|
|
164
|
+
|
|
165
|
+
## Immediate vs Form Actions
|
|
166
|
+
|
|
167
|
+
Plutonium automatically determines if an action needs a form:
|
|
168
|
+
|
|
169
|
+
- **Has inputs defined** → Shows form first
|
|
170
|
+
- **No inputs** → Executes immediately (with optional confirmation)
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
# Shows form (has inputs)
|
|
174
|
+
class InviteUserInteraction < ResourceInteraction
|
|
175
|
+
attribute :resource
|
|
176
|
+
attribute :email
|
|
177
|
+
input :email # This triggers form display
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Immediate execution (no inputs)
|
|
181
|
+
class ArchiveInteraction < ResourceInteraction
|
|
182
|
+
attribute :resource
|
|
183
|
+
# No inputs = immediate with confirmation
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Action Visibility
|
|
188
|
+
|
|
189
|
+
Control where actions appear:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
action :publish,
|
|
193
|
+
interaction: PublishPostInteraction,
|
|
194
|
+
record_action: true, # Show on show page
|
|
195
|
+
collection_record_action: true # Show in table rows
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Record Actions (Single Records)
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
action :publish, interaction: PublishPostInteraction
|
|
202
|
+
action :archive, interaction: ArchiveInteraction, record_action: true
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Bulk Actions (Multiple Records)
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
action :bulk_publish, interaction: BulkPublishInteraction
|
|
209
|
+
action :bulk_archive, interaction: BulkArchiveInteraction
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Resource Actions (No Record)
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
action :import, interaction: ImportInteraction, resource_action: true
|
|
216
|
+
action :export, interaction: ExportInteraction, resource_action: true
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Bulk Action Interaction
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
class BulkPublishInteraction < ResourceInteraction
|
|
223
|
+
presents label: "Publish Selected",
|
|
224
|
+
icon: Phlex::TablerIcons::Send
|
|
225
|
+
|
|
226
|
+
# Note: plural 'resources' for bulk actions
|
|
227
|
+
attribute :resources
|
|
228
|
+
|
|
229
|
+
private
|
|
230
|
+
|
|
231
|
+
def execute
|
|
232
|
+
count = resources.update_all(
|
|
233
|
+
published: true,
|
|
234
|
+
published_at: Time.current
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
succeed(resources)
|
|
238
|
+
.with_message("#{count} posts published")
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Resource Action (No Record)
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
class ImportInteraction < ResourceInteraction
|
|
247
|
+
presents label: "Import CSV",
|
|
248
|
+
icon: Phlex::TablerIcons::Upload
|
|
249
|
+
|
|
250
|
+
# No :resource or :resources = resource action
|
|
251
|
+
attribute :file
|
|
252
|
+
|
|
253
|
+
input :file, as: :file
|
|
254
|
+
|
|
255
|
+
validates :file, presence: true
|
|
256
|
+
|
|
257
|
+
private
|
|
258
|
+
|
|
259
|
+
def execute
|
|
260
|
+
# Import logic...
|
|
261
|
+
succeed(nil).with_message("Import completed.")
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Action Options
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
action :name,
|
|
270
|
+
interaction: MyInteraction,
|
|
271
|
+
|
|
272
|
+
# Display
|
|
273
|
+
label: "Custom Label", # Override interaction label
|
|
274
|
+
icon: Phlex::TablerIcons::Star, # Override interaction icon
|
|
275
|
+
color: :danger, # :primary, :secondary, :danger
|
|
276
|
+
|
|
277
|
+
# Visibility
|
|
278
|
+
resource_action: true, # Show on index page
|
|
279
|
+
record_action: true, # Show on show page
|
|
280
|
+
collection_record_action: true, # Show in table rows
|
|
281
|
+
bulk_action: true, # For selected records
|
|
282
|
+
|
|
283
|
+
# Grouping
|
|
284
|
+
category: :danger, # :primary, :secondary, :danger
|
|
285
|
+
position: 50, # Order (lower = first)
|
|
286
|
+
|
|
287
|
+
# Behavior
|
|
288
|
+
confirmation: "Are you sure?", # Confirmation dialog
|
|
289
|
+
turbo_frame: "_top" # Turbo frame target
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Confirmation Dialogs
|
|
293
|
+
|
|
294
|
+
Require confirmation before executing:
|
|
295
|
+
|
|
296
|
+
```ruby
|
|
297
|
+
action :delete,
|
|
298
|
+
interaction: DeleteInteraction,
|
|
299
|
+
confirmation: "Are you sure you want to delete this post?"
|
|
300
|
+
|
|
301
|
+
action :bulk_delete,
|
|
302
|
+
interaction: BulkDeleteInteraction,
|
|
303
|
+
confirmation: "Delete all selected posts? This cannot be undone."
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Interaction Outcomes
|
|
307
|
+
|
|
308
|
+
### Success
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
def execute
|
|
312
|
+
# ... do work ...
|
|
313
|
+
|
|
314
|
+
# Basic success
|
|
315
|
+
succeed(resource)
|
|
316
|
+
|
|
317
|
+
# With message
|
|
318
|
+
succeed(resource).with_message("Success!")
|
|
319
|
+
|
|
320
|
+
# With redirect
|
|
321
|
+
succeed(resource)
|
|
322
|
+
.with_redirect_response(posts_path)
|
|
323
|
+
.with_message("Post created!")
|
|
324
|
+
|
|
325
|
+
# With file download
|
|
326
|
+
succeed(resource)
|
|
327
|
+
.with_file_response(pdf_path, filename: "invoice.pdf")
|
|
328
|
+
end
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Failure
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
def execute
|
|
335
|
+
# From ActiveModel errors
|
|
336
|
+
failed(resource.errors)
|
|
337
|
+
|
|
338
|
+
# With custom message
|
|
339
|
+
failed("Something went wrong")
|
|
340
|
+
|
|
341
|
+
# With specific field
|
|
342
|
+
failed("is invalid", :email)
|
|
343
|
+
|
|
344
|
+
# With hash of errors
|
|
345
|
+
failed(email: "is invalid", name: "is required")
|
|
346
|
+
end
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Chaining Interactions
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
def execute
|
|
353
|
+
CreateUserInteraction.call(view_context:, **user_params)
|
|
354
|
+
.and_then { |result| SendWelcomeEmail.call(view_context:, user: result.value) }
|
|
355
|
+
.and_then { |result| LogActivity.call(view_context:, user: result.value) }
|
|
356
|
+
.with_message("User created and welcomed!")
|
|
357
|
+
end
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
On failure, the chain short-circuits and returns the failure immediately.
|
|
361
|
+
|
|
362
|
+
## Accessing Context
|
|
363
|
+
|
|
364
|
+
Interactions have access to `current_user` and `view_context`:
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
class PublishPostInteraction < ResourceInteraction
|
|
368
|
+
attribute :resource
|
|
369
|
+
|
|
370
|
+
private
|
|
371
|
+
|
|
372
|
+
def execute
|
|
373
|
+
resource.update!(
|
|
374
|
+
published: true,
|
|
375
|
+
published_by: current_user # Built-in helper
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
succeed(resource)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
For advanced access:
|
|
384
|
+
|
|
385
|
+
```ruby
|
|
386
|
+
def execute
|
|
387
|
+
# Access helpers via view_context
|
|
388
|
+
view_context.controller.helpers.some_helper
|
|
389
|
+
|
|
390
|
+
# Access params
|
|
391
|
+
view_context.params
|
|
392
|
+
|
|
393
|
+
succeed(resource)
|
|
394
|
+
end
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Complete Example: Send Invoice
|
|
398
|
+
|
|
399
|
+
```ruby
|
|
400
|
+
class SendInvoiceInteraction < ResourceInteraction
|
|
401
|
+
presents label: "Send Invoice",
|
|
402
|
+
icon: Phlex::TablerIcons::Mail,
|
|
403
|
+
description: "Email invoice to recipient"
|
|
404
|
+
|
|
405
|
+
attribute :resource # The invoice
|
|
406
|
+
attribute :recipient_email, :string
|
|
407
|
+
attribute :message, :text
|
|
408
|
+
attribute :attach_pdf, :boolean, default: true
|
|
409
|
+
|
|
410
|
+
input :recipient_email, as: :email, hint: "Recipient's email address"
|
|
411
|
+
input :message, as: :text, hint: "Optional message to include"
|
|
412
|
+
input :attach_pdf, as: :boolean
|
|
413
|
+
|
|
414
|
+
validates :recipient_email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
415
|
+
|
|
416
|
+
private
|
|
417
|
+
|
|
418
|
+
def execute
|
|
419
|
+
# Generate PDF if requested
|
|
420
|
+
pdf = attach_pdf ? generate_pdf : nil
|
|
421
|
+
|
|
422
|
+
# Send email
|
|
423
|
+
InvoiceMailer.send_invoice(
|
|
424
|
+
invoice: resource,
|
|
425
|
+
to: recipient_email,
|
|
426
|
+
message: message,
|
|
427
|
+
attachment: pdf
|
|
428
|
+
).deliver_later
|
|
429
|
+
|
|
430
|
+
# Update invoice status
|
|
431
|
+
resource.update!(
|
|
432
|
+
sent_at: Time.current,
|
|
433
|
+
sent_to: recipient_email
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
succeed(resource)
|
|
437
|
+
.with_message("Invoice sent to #{recipient_email}")
|
|
438
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
439
|
+
failed(e.record.errors)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def generate_pdf
|
|
443
|
+
InvoicePdfGenerator.new(resource).generate
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## Inherited Actions
|
|
449
|
+
|
|
450
|
+
Define common actions in your base definition:
|
|
451
|
+
|
|
452
|
+
```ruby
|
|
453
|
+
# app/definitions/resource_definition.rb
|
|
454
|
+
class ResourceDefinition < Plutonium::Resource::Definition
|
|
455
|
+
action :archive,
|
|
456
|
+
interaction: ArchiveInteraction,
|
|
457
|
+
color: :danger,
|
|
458
|
+
position: 1000
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# All definitions inherit the archive action
|
|
462
|
+
class PostDefinition < ResourceDefinition
|
|
463
|
+
# Already has :archive action
|
|
464
|
+
end
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## Portal-Specific Actions
|
|
468
|
+
|
|
469
|
+
Override or add actions for specific portals:
|
|
470
|
+
|
|
471
|
+
```ruby
|
|
472
|
+
# packages/admin_portal/app/definitions/admin_portal/post_definition.rb
|
|
473
|
+
class AdminPortal::PostDefinition < ::PostDefinition
|
|
474
|
+
# Add admin-only actions
|
|
475
|
+
action :feature, interaction: FeaturePostInteraction
|
|
476
|
+
action :bulk_publish, interaction: BulkPublishInteraction
|
|
477
|
+
end
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
## Testing Interactions
|
|
481
|
+
|
|
482
|
+
```ruby
|
|
483
|
+
RSpec.describe PublishPostInteraction do
|
|
484
|
+
let(:user) { create(:user) }
|
|
485
|
+
let(:post) { create(:post, user: user, published: false) }
|
|
486
|
+
let(:view_context) { double(controller: double(helpers: double(current_user: user))) }
|
|
487
|
+
|
|
488
|
+
subject { described_class.new(view_context: view_context, resource: post) }
|
|
489
|
+
|
|
490
|
+
describe '#call' do
|
|
491
|
+
it 'publishes the post' do
|
|
492
|
+
result = subject.call
|
|
493
|
+
|
|
494
|
+
expect(result).to be_success
|
|
495
|
+
expect(post.reload.published?).to be true
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
context 'when already published' do
|
|
499
|
+
before { post.update!(published: true) }
|
|
500
|
+
|
|
501
|
+
it 'fails with error' do
|
|
502
|
+
result = subject.call
|
|
503
|
+
|
|
504
|
+
expect(result).to be_failure
|
|
505
|
+
expect(subject.errors[:base]).to include("Post is already published")
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
## Best Practices
|
|
513
|
+
|
|
514
|
+
1. **Keep interactions focused** - One action per interaction
|
|
515
|
+
2. **Use validations** - Validate all inputs before execution
|
|
516
|
+
3. **Handle errors gracefully** - Rescue exceptions and return `failed()`
|
|
517
|
+
4. **Return meaningful messages** - Help users understand what happened
|
|
518
|
+
5. **Use `and_then` for chains** - Compose complex workflows from simple interactions
|
|
519
|
+
|
|
520
|
+
## Related
|
|
521
|
+
|
|
522
|
+
- [Authorization](./authorization)
|
|
523
|
+
- [Adding Resources](./adding-resources)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Guides
|
|
2
|
+
|
|
3
|
+
Task-oriented guides for common Plutonium operations.
|
|
4
|
+
|
|
5
|
+
## Getting Things Done
|
|
6
|
+
|
|
7
|
+
These guides show you how to accomplish specific tasks, with complete examples.
|
|
8
|
+
|
|
9
|
+
### Setup & Resources
|
|
10
|
+
|
|
11
|
+
- [Adding Resources](./adding-resources) - Create and connect resources to portals
|
|
12
|
+
- [Creating Packages](./creating-packages) - Organize code into feature and portal packages
|
|
13
|
+
|
|
14
|
+
### Authentication & Authorization
|
|
15
|
+
|
|
16
|
+
- [Authentication](./authentication) - Set up user authentication with Rodauth
|
|
17
|
+
- [Authorization](./authorization) - Implement policies for access control
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
- [Custom Actions](./custom-actions) - Add interactive actions with Interactions
|
|
22
|
+
- [Nested Resources](./nested-resources) - Parent/child resource relationships
|
|
23
|
+
- [Multi-tenancy](./multi-tenancy) - Scope data to organizations or accounts
|
|
24
|
+
- [Search and Filtering](./search-filtering) - Implement search, filters, and scopes
|
|
25
|
+
|
|
26
|
+
### Customization
|
|
27
|
+
|
|
28
|
+
- [Theming](./theming) - Customize colors, styles, and branding
|
|
29
|
+
|
|
30
|
+
## Finding What You Need
|
|
31
|
+
|
|
32
|
+
| I want to... | Guide |
|
|
33
|
+
|--------------|-------|
|
|
34
|
+
| Add a new model to my app | [Adding Resources](./adding-resources) |
|
|
35
|
+
| Protect pages with login | [Authentication](./authentication) |
|
|
36
|
+
| Control who can edit what | [Authorization](./authorization) |
|
|
37
|
+
| Add a "Publish" button | [Custom Actions](./custom-actions) |
|
|
38
|
+
| Show comments under posts | [Nested Resources](./nested-resources) |
|
|
39
|
+
| Separate data by company | [Multi-tenancy](./multi-tenancy) |
|
|
40
|
+
| Add search to a list | [Search and Filtering](./search-filtering) |
|
|
41
|
+
| Change the color scheme | [Theming](./theming) |
|
|
42
|
+
|
|
43
|
+
## Looking for Reference Docs?
|
|
44
|
+
|
|
45
|
+
For complete API documentation, see the [Reference](/reference/) section.
|